概述
多线程处理一直是网络请求中的重要部分,为了保证线程安全,即同一时刻只允许有一个线程访问资源,常见的处理方式有关键字@synchronized和信号量semaphore。
@synchronized
@synchronized会创建一个互斥锁,对传入的对象加锁,保证该对象在@synchronized的作用域中只会被一个线程访问,代码结构如下:
// 对self对象加锁
@synchronized(self) {
// 锁的作用域
......
}
传入的参数self表示当前类的实例,表示对当前类的实例加锁,传入的参数也可以是属性或者OC类型的变量,但不能是基本类型。
大括号内的代码代表了锁的作用域,在该作用域内,同时只允许由一个线程访问。也就是说,该作用域内的代码,同一时间只能由一个线程执行。下面来看具体的例子。
// 打印日志
- (void)printAfterSleep
{
@synchronized(self) {
sleep(5);
NSLog(@"printAfterSleep");
}
}
// 创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
// 异步执行,创建一个新的线程
dispatch_async(queue, ^{
[self printAfterSleep];
});
// 异步执行,创建一个新的线程
dispatch_async(queue, ^{
[self printAfterSleep];
});
日志输出:
// 等待5s后打印
printAfterSleep
// 再等待5s后打印
printAfterSleep
代码创建了一个并发队列,并发对列在异步执行时,会创建创建两个线程,这两个线程同时访问self,由于@synchronized对self加上了互斥锁,使得同一时间只有一个线程可以访问,另一个线程被阻塞,直到前一个线程执行结束,后一个线程才可以继续访问。所以,前一个线程休眠5s后打印第一个“printAfterSleep”,后一个线程休眠5s再打印第二个“printAfterSleep”。
Tips:假如创建的是串行队列,异步执行只会创建一个新线程,所有的block都会插入到这个队列中去,代码会按先进先出的顺序执行,所以跟加锁后的效果是一样的,但是这并不算多线程处理。
假如去掉这个@synchronized,允许两个线程同时访问self,那么两个线程同时休眠5s后会同时打印“printAfterSleep”。
- (void)printAfterSleep
{
sleep(5);
NSLog(@"printAfterSleep");
}
日志输出:
// 等待5s后同时打印
printAfterSleep
printAfterSleep
同理,可以推广到两个及以上的多线程处理中去。
信号量semaphore
semaphore是GCD中用于保证线程安全的处理方式。
假设一个房间最大只能容纳n人,有m(m>n)个人想进这个房间,他们可能是同时到达房间门口,也可能是先后到达,不管哪种情况,最多只允许n个人进去,其他人只能在门口等着,里面的人不出来,外面的人的就进不去,直到里面有人出来,出来一个,才能进去一个。
对照这个例子,把资源看作房间,信号量看作房间最大容纳人数,线程看作人,信号量的初始值为n,线程数量为m(m>n),每有一个线程访问资源,信号量就会-1,信号量为0时,其他线程被阻塞,不允许访问资源,直到有线程释放资源,信号量+1,才允许新的线程访问。
参照上面@synchronized,来看基于信号量对资源加锁的方法,代码如下:
// 打印日志
- (void)printAfterSleep
{
sleep(5);
NSLog(@"printAfterSleep");
}
// 创建一个初始值为2的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
// 创建一个串行队列
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
// 异步执行,创建一个新线程
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self printAfterSleep];
dispatch_semaphore_signal(semaphore);
});
// 异步执行,创建一个新线程
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self printAfterSleep];
dispatch_semaphore_signal(semaphore);
});
// 异步执行,创建一个新线程
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self printAfterSleep];
dispatch_semaphore_signal(semaphore);
});
日志输出:
// 等待5s后后打印两次
printAfterSleep
printAfterSleep
// 再等5s后打印
printAfterSleep
dispatch_semaphore_create创建一个初始值为n=2(n>=0)的信号量,表示允许2个线程同时访问资源。
dispatch_semaphore_wait方法传入两个参数(信号量和超时时间),方法判断当前信号量是否大于0,大于0时继续执行后续代码,并使信号量-1;如果信号量的值为0,就阻塞当前线程并等待超时,阻塞期间发现信号量的值大于0,或者超时结束,会自动执行后续代码。
Tips:超时时间的类型为dispatch_time_t,默认有两个值可选:DISPATCH_TIME_NOW(立即超时)和DISPATCH_TIME_FOREVER(永不超时)。
dispatch_semaphore_signal方法会使信号量+1。
dispatch_semaphore_wait方法和dispatch_semaphore_signal方法都有返回值,且都是long类型的。dispatch_semaphore_wait返回值为0表示超时时间内信号量不为0,当前线程被唤醒;返回值不为0表示超时后信号量的值依然为0。dispatch_semaphore_signal返回值为0表示当前没有线程在等待该线程拥有的信号量,释放信号量后,信号量只需要+1,返回值不为0表示当前有一个或多个线程在等待该线程拥有的信号量,它还需要根据优先级顺序(或随机)唤醒一个等待的线程。
假如去掉信号量的逻辑,就会出现同时打印三次“printAfterSleep”的情况。
@synchronized和semaphore在SDWebImage中的应用
@synchronized和semaphore在SDWebImage中的应用有很多,本文以SDWebImageDownloaderOperation类来阐述。
SDWebImage库Downloader模块中的SDWebImageDownloaderOperation类负责执行下载任务,它定义了一个属性callbackBlocks
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
callbackBlocks是一个可变素组,其中每个元素是SDCallbacksDictionary类型的字典,用键值对的方式保存每个下载任务的progressBlock和completedBlock。progressBlock和completedBlock由外部传入,负责下载过程中和下载完成时或下载异常情况的处理。
在对callbackBlocks属性访问的过程中,不管是添加元素,还是获取元素,都使用了信号量对callbackBlocks加锁,且信号量的初始值为1,即同时只允许一个线程访问,保证了数据的一致性和线程安全。
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
// addHandlersForProgress:completed:方法
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
// callbacksForKey方法
LOCK(self.callbacksLock);
NSMutableArray<id> *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
UNLOCK(self.callbacksLock);
在start方法中,用@synchronized对self对象加锁,保证同时只允许一个线程访问。
// 开始加载数据
- (void)start {
@synchronized (self) {
......
}
}
总结
笔者猜想,SDWebImage库作者这么设计的目的是,SDWebImageDownloaderOperatio对象由init方法创建,对象可以在任意时机多次调用addHandlersForProgress:completed:来修改callbackBlocks,为了避免访问(读和写)异常,设计了信号量机制。而且start方法也可能会被多次调用,为了防止一段代码被同时执行多次而引起的异常,设计了@synchronized互斥锁机制。