SDWebImage学习笔记之@synchronized和semaphore

概述

多线程处理一直是网络请求中的重要部分,为了保证线程安全,即同一时刻只允许有一个线程访问资源,常见的处理方式有关键字@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互斥锁机制。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,505评论 0 6
  • 2016年国庆假期终于把此书过完,整理笔记和体会于此。 关于书名 书名源于俄罗斯的演员斯坦尼斯拉夫斯基创作的《演员...
    李剑飞的简书阅读 7,220评论 2 65
  • 一、前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized、NSLock...
    稻春阅读 464评论 0 0
  • Managing Units of Work(管理工作单位) 调度块允许您直接配置队列中各个工作单元的属性。它们还...
    edison0428阅读 7,937评论 0 1
  • 我坐在电脑前,领导要求的报告,在修改了不知道多少遍以后,终于保存提交。取掉眼镜,揉揉发晕的脑袋,顺手去拿桌上的水杯...
    尹向阳阅读 383评论 0 0