- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
sleep(5);
NSLog(@"1");
});
dispatch_async(queue, ^{
sleep(4);
NSLog(@"4");
});
dispatch_sync(queue, ^{
sleep(3);
NSLog(@"2");
});
sleep(1);
NSLog(@"3");
}
-
在主线程执行, 输出顺序是 1 4 2 3 ,因为在输出2时,是dispatch_sync ,需要立即执行,而所在队列是串行的,将优先执行排在队列前面的任务。
-
如果将队列换成并发队列,输出顺序为 2 3 4 1 ,因为并发队列任务可以随意出队,这时候sync任务可以提前出队,同时等进程任务执行完,再执行异步任务,异步任务执行也是并发执行,执行时间短的先结束
GNU源码 : GNUStep
iOS中的常见多线程方案
GCD
各种队列的执行效果
队列组的使用
思考:如何用gcd实现以下功能
- 异步并发执行任务1、任务2
- 等任务1、任务2都执行完毕后,再回到主线程执行任务3
队列组的可用方案
- (void)groupSync
{
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务完成");
});
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});
// 等前面的任务执行完毕后,会自动执行这个任务
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
});
// *************** *************** ***************
// dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3-%@", [NSThread currentThread]);
// }
// });
// *************** *************** ***************
// dispatch_group_notify(group, queue, ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3-%@", [NSThread currentThread]);
// }
// });
//
// dispatch_group_notify(group, queue, ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务4-%@", [NSThread currentThread]);
// }
// });
}
死锁
主队列执行主线程任务-- 死锁
- (void)interview
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// dispatch_sync立马在当前线程同步执行任务
}
死锁原因
- 主线程正在执行的任务,需要等
sync()任务执行完才能执行,sync()任务需要等主线程空闲才能执行
- (void)interview03
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?会!
NSLog(@"执行任务1");
//如果创建的是并发队列,不会死锁
//dispatch_queue_t queue = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2,%@",[NSThread currentThread]);
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3,%@",[NSThread currentThread]);
});
NSLog(@"执行任务4,%@",[NSThread currentThread]);
});
NSLog(@"执行任务5");
}
死锁原因
产生死锁条件: 使用sync函数往当前的串行队列中添加任务,会卡住当前的串行队列(产生死锁)
多线程题解
one
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:self afterDelay:.0];
// 如果跑起来子线程的runLoop,可以打印
// runLoop在处理定时器是在唤醒的时候, 在runLoop睡觉之前之前肯定要先去处理点击事件,所以打印顺序会改变( 1 3 Test )
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"2");
});
}
- (void)test {
NSLog(@"%s",__func__);
}
/*
打印结果是 :
2018-10-17 13:19:06.213307+0800 多线程[4284:708545] 1
2018-10-17 13:19:06.213499+0800 多线程[4284:708545] 2
*/
原因:
performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器- 子线程默认没有启动
Runloop
参考GNUStep提供的源码(路径 Source -> Foundation -> NSRunLoop.m )
- (void) performSelector: (SEL)aSelector
withObject: (id)argument
afterDelay: (NSTimeInterval)seconds
{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
// GSTimedPerformer 封装了NSTimer
GSTimedPerformer *item;
item = [[GSTimedPerformer alloc] initWithSelector: aSelector
target: self
argument: argument
delay: seconds];
[[loop _timedPerformers] addObject: item];
RELEASE(item);
// 向runLoop中添加timer
// 虽然添加了timer,但是runLoop需要调用run,才能执行
[loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}
two
- (void)test
{
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
// 向runLoop中添加任务可以进行保活操作,注意runLoop中必须要port、source、timer、observer才能进行run操作
// [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
// 结果 : 打印 1 之后崩溃,
原因test函数也要在线程thread中执行,但是thread在NSLog完之后,就会进行释放操作,子线程没有runLoop无法保活,子线程销毁,再次调用造成崩溃
线程组的使用
多线程安全隐患
- 资源共享
- 1块资源可能会被多个线程共享,也就是
多个线程可能会访问同一块资源 - 比如多个线程访问同一个对象、同一个变量、同一个文件
- 1块资源可能会被多个线程共享,也就是
- 当多个线程访问同一块资源时,很容易引发
数据错乱和数据安全问题
iOS中的线程同步方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
OSSpinLock
OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源(自旋锁)- 目前已经不再安全,可能会出现
优先级反转问题- 可能
thred1级别高,但是进入的比thred2晚,一直忙等(while 1),占用了CPU资源,造成死锁
- 可能
- 如果等待锁的线程优先级较高,它会一直占用着
CPU资源,优先级低的线程就无法释放锁. - 需要导入头文件
#import <libkern/OSAtomic.h>
os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持- 从底层调用看,等待
os_unfair_lock锁的线程会处于休眠状态,并非忙等 (互斥锁) - 需要导入头文件
#import <os/lock.h>
pthread_mutex (pthread 一般都是跨平台的)
mutex叫做互斥锁,等待锁的线程会处于休眠状态- 需要导入头文件
#import <pthread.h>
pthread_mutex – 递归锁
下面代码会造成死锁,因为otherTest2的加锁,会休眠等待otherTest的锁解锁,但是otherTest要等otherTest2结束才会解锁,可以换一把锁,otherTest2使用mutex2
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
[self otherTest2];
pthread_mutex_unlock(&_mutex);
}
- (void)otherTest2
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
pthread_mutex_unlock(&_mutex);
}
但是递归的情况,只能使用递归锁
递归锁: 允许同一个线程对一把锁重复加锁(不同线程不行)
/**
线程1:otherTest(+-)
otherTest(+-)
otherTest(+-)
线程2:otherTest(等待)
*/
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
[self otherTest];
pthread_mutex_unlock(&_mutex);
}
pthread_mutex – 条件
生产者、消费者模式
// 模拟删除、添加在不同线程
- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}
// 生产者-消费者模式
// 线程1
// 删除数组中的元素
- (void)__remove
{
pthread_mutex_lock(&_mutex);
NSLog(@"__remove - begin");
if (self.data.count == 0) { // 有数据才删除
NSLog(@"唤醒");
// 带有条件(_cond)的等待,同时放开锁mutex,等待信号过来,
// 这时候等待添加数据解锁,又会再次加锁
// 所以这里是 ----- 解锁 ------加锁
pthread_cond_wait(&_cond, &_mutex);
}
[self.data removeLastObject];
NSLog(@"删除了元素");
pthread_mutex_unlock(&_mutex);
NSLog(@"删除结束");
}
// 线程2
// 往数组中添加元素
- (void)__add
{
pthread_mutex_lock(&_mutex);
sleep(1);
[self.data addObject:@"Test"];
NSLog(@"添加了元素");
// 信号 (会唤醒刚刚带有条件的等待pthread_cond_wait)
pthread_cond_signal(&_cond);
// 广播 (唤醒所有的等待线程)
// pthread_cond_broadcast(&_cond);
sleep(3);
pthread_mutex_unlock(&_mutex);
NSLog(@"添加结束");
}
打印
2021-04-13 17:12:53.685170+0800 Interview04-线程同步[6627:375249] __remove - begin
2021-04-13 17:12:53.685282+0800 Interview04-线程同步[6627:375249] 唤醒
2021-04-13 17:12:54.688471+0800 Interview04-线程同步[6627:375250] 添加了元素
2021-04-13 17:12:57.694049+0800 Interview04-线程同步[6627:375250] 添加结束
2021-04-13 17:12:57.694099+0800 Interview04-线程同步[6627:375249] 删除了元素
2021-04-13 17:12:57.694416+0800 Interview04-线程同步[6627:375249] 删除结束
NSLock、NSRecursiveLock
NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition
NSCondition是对pthread_mutex和cond(mutex条件)的封装
生产者-消费者模式
NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
可以根据条件值,使得子线程依赖执行
- (instancetype)init
{
if (self = [super init]) {
// 条件值是1
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}
return self;
}
- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}
- (void)__one
{
// 不管条件值,直接加锁
[self.conditionLock lock];
NSLog(@"__one");
sleep(1);
// 设置条件值为2,并解锁
[self.conditionLock unlockWithCondition:2];
}
- (void)__two
{
// 条件值是2才能加锁
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
sleep(1);
// 设置条件值为3,并解锁
[self.conditionLock unlockWithCondition:3];
}
- (void)__three
{
// 条件值是3才能加锁
[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
// 不管条件值,直接解锁
[self.conditionLock unlock];
}
// 打印
2021-04-14 09:43:59.040652+0800 -线程同步[1667:43984] __one
2021-04-14 09:44:00.045292+0800 -线程同步[1667:43985] __two
2021-04-14 09:44:01.048857+0800 -线程同步[1667:43986] __three
dispatch_semaphore
semaphore叫做”信号量”- 信号量的初始值,可以用来控制线程并发访问的最大数量
- 信号量的初始值为1,代表同时只允许1条线程访问资源,保证
线程同步
宏定义信号量
#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);
- (void)test1
{
SemaphoreBegin;
// .....
SemaphoreEnd;
}
控制最大线程数
// 线程10、7、6、9、8
self.semaphore = dispatch_semaphore_create(5);
- (void)test
{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
dispatch_queue
直接使用GCD的串行队列,也是可以实现线程同步的
@synchronized
并没有代码提示,性能较差
@synchronized是对mutex递归锁的封装- 源码查看:
objc4中的objc-sync.mm文件 @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
iOS线程同步方案性能比较
自旋锁、互斥锁比较
-
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
-
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作(比较占用CPU资源)
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
atomic
atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁- 可以参考源码
objc4的objc-accessors.mm
// set 方法
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
// [self.array mutableCopy]
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
// get 方法
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
spinlock_t的源码,可以看出atomic内部是os_unfair_lock
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
}
它并不能保证使用属性的过程是线程安全的(比如创建数组添加元素)
创建一个类 Person,有一个属性 NSMutableArray *array, 在ViewController中添加元素 [p.array addobject: @"xxx"],相当于
[[p array] addObject @"xxx"] , 但是只有前半部分[p array]是线程安全的,后半部分不是
iOS中的读写安全方案
思考如何实现以下场景
同一时间,只能有1个线程进行写的操作 同一时间,允许有多个线程进行读的操作 同一时间,不允许既有写的操作,又有读的操作
上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有
pthread_rwlock:读写锁
dispatch_barrier_async:异步栅栏调用
pthread_rwlock
dispatch_barrier_async
- 这个函数传入的并发队列必须是自己通过
dispatch_queue_cretate创建的 - 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于
dispatch_async函数的效果
实现效果类似: