多線程
多線程技術(shù)大家都很了解,而且在項(xiàng)目中也比較常用岛宦。比如開(kāi)啟一個(gè)子線程來(lái)處理一些耗時(shí)的計(jì)算台丛,然后返回主線程刷新UI等。首先我們先簡(jiǎn)單的梳理一下常用到的多線程方案砾肺。具體的用法這里我就不說(shuō)了挽霉,每一種方案大家可以去查一下,網(wǎng)上教程很多变汪。
常見(jiàn)的多線程方案
我們比較常用的是GCD和NSOperation侠坎,當(dāng)然還有NSThread,pthread裙盾。他們的具體區(qū)別我們不詳細(xì)說(shuō)实胸,給出下面這一個(gè)表格,大家自行對(duì)比一下番官。
容易混淆的術(shù)語(yǔ)
提到多線程庐完,有一個(gè)術(shù)語(yǔ)是經(jīng)常能聽(tīng)到的,同步徘熔,異步门躯,串行,并發(fā)酷师。
同步和異步的區(qū)別讶凉,就是是否有開(kāi)啟新的線程的能力。異步具備開(kāi)啟線程的能力窒升,同步不具備開(kāi)啟線程的能力缀遍。注意,異步只是具備開(kāi)始新線程的能力饱须,具體開(kāi)啟與否還要跟隊(duì)列的屬性有關(guān)系域醇。
串行和并發(fā),是指的任務(wù)的執(zhí)行方式蓉媳。并發(fā)是任務(wù)可以多個(gè)同時(shí)執(zhí)行譬挚,串行之能是一個(gè)執(zhí)行完成后在執(zhí)行下一個(gè)。
在面試的過(guò)程中可能被問(wèn)到什么網(wǎng)情況下會(huì)出現(xiàn)死鎖的問(wèn)題酪呻,總結(jié)一下就是使用sync函數(shù)(同步)往當(dāng)前的串行對(duì)列中添加任務(wù)時(shí)减宣,會(huì)出現(xiàn)死鎖。
鎖
多線程的安全隱患
多線程和安全問(wèn)題是分不開(kāi)的玩荠,因?yàn)樵谑褂枚鄠€(gè)線程訪問(wèn)同一塊數(shù)據(jù)的時(shí)候漆腌,如果同時(shí)有讀寫(xiě)操作贼邓,就可能產(chǎn)生數(shù)據(jù)安全問(wèn)題。
所以這時(shí)候我們就用到了鎖這個(gè)東西闷尿。
其實(shí)使用鎖也是為了在使用多線程的過(guò)程中保障數(shù)據(jù)安全塑径,除了鎖,然后一些其他的實(shí)現(xiàn)線程同步來(lái)保證數(shù)據(jù)安全的方案填具,我們一起來(lái)了解一下统舀。
線程同步方案
下面這些是我們常用來(lái)實(shí)現(xiàn)線程同步方案的。
OSSpinLock
os_unfair_lock
pthread_mutex
NSLock
NSRecursiveLock
NSCondition
NSConditinLock
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
@synchronized
可以看出來(lái)劳景,實(shí)現(xiàn)線程同步的方案包括各種鎖誉简,還有信號(hào)量,串行隊(duì)列盟广。
我們只挑其中不常用的來(lái)說(shuō)一下使用方法闷串。
下面是我們模擬了存錢(qián)取錢(qián)的場(chǎng)景,下面是加鎖之前的代碼衡蚂,運(yùn)行之后肯定是有數(shù)據(jù)問(wèn)題的窿克。
/**
存錢(qián)、取錢(qián)演示
*/
- (void)moneyTest {
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self __saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self __drawMoney];
}
});
}
/**
存錢(qián)
*/
- (void)__saveMoney {
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50毛甲,還剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
/**
取錢(qián)
*/
- (void)__drawMoney {
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20年叮,還剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
加鎖的代碼,涉及到鎖的初始化玻募、加鎖只损、解鎖這么三部分。我們從OSSpinLock
開(kāi)始說(shuō)七咧。
OSSpinLock自旋鎖
OSSpinLock
叫做自旋鎖跃惫。那什么叫自旋鎖呢?其實(shí)我們可以從大類上面把鎖分為兩類艾栋,一類是自旋鎖爆存,一類是互斥鎖。我們通過(guò)一個(gè)例子來(lái)區(qū)分這兩類鎖蝗砾。
如果線程A率先到達(dá)加鎖的部分先较,并成功加鎖,線程B到達(dá)的時(shí)候會(huì)因?yàn)橐呀?jīng)被A加鎖而等待悼粮。如果是自旋鎖闲勺,線程B會(huì)通過(guò)執(zhí)行一個(gè)循環(huán)來(lái)實(shí)現(xiàn)等待,我們不用管它循環(huán)執(zhí)行了什么扣猫,只要知道他在那"轉(zhuǎn)圈圈"等著就行菜循。如果是互斥鎖,那線程B在等待的時(shí)候會(huì)休眠申尤。
使用OSSpinLock
需要導(dǎo)入頭文件#import <libkern/OSAtomic.h>
//聲明一個(gè)鎖
@property (nonatomic, assign) OSSpinLock lock;
// 鎖的初始化
self.lock = OS_SPINLOCK_INIT;
在我們這個(gè)例子中癌幕,存錢(qián)取錢(qián)都是訪問(wèn)了money衙耕,所以我們要在存和取的操作中使用同一個(gè)鎖。
/**
存錢(qián)
*/
- (void)__saveMoney {
OSSpinLockLock(&_lock);
//....省去中間的邏輯代碼
OSSpinLockUnlock(&_lock);
}
/**
取錢(qián)
*/
- (void)__drawMoney {
OSSpinLockLock(&_lock);
//....省去中間的邏輯代碼
OSSpinLockUnlock(&_lock);
}
這就是簡(jiǎn)單的自旋鎖的使用勺远,我們發(fā)現(xiàn)在使用的過(guò)程中臭杰,Xcode一直提醒我們這個(gè)OSSpinLock
被廢棄了,讓我們使用os_unfair_lock
代替谚中。OSSpinLock
之所以會(huì)被廢棄是因?yàn)樗赡軙?huì)產(chǎn)生一個(gè)優(yōu)先級(jí)反轉(zhuǎn)的問(wèn)題。
具體來(lái)說(shuō)寥枝,如果一個(gè)低優(yōu)先級(jí)的線程獲得了鎖并訪問(wèn)共享資源宪塔,那高優(yōu)先級(jí)的線程只能忙等,從而占用大量的CPU囊拜。低優(yōu)先級(jí)的線程無(wú)法和高優(yōu)先級(jí)的線程競(jìng)爭(zhēng)(CPU會(huì)給高優(yōu)先級(jí)的線程分配更多的時(shí)間片)某筐,所以會(huì)導(dǎo)致低優(yōu)先級(jí)的線程的任務(wù)一直完不成,從而無(wú)法釋放鎖冠跷。
os_unfair_lock
的用法跟OSSpinLock
很像南誊,就不單獨(dú)說(shuō)了。
pthread_mutex
Default
一看到這個(gè)pthread我們應(yīng)該就能知道這是一種跨平臺(tái)的方案了蜜托。首先還是來(lái)看用法抄囚。
//聲明一個(gè)鎖
@property (nonatomic, assign) pthread_mutex_t lock;
//初始化
pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)
我們可以看到在初始化鎖的時(shí)候,第一個(gè)參數(shù)是鎖的地址橄务,第二個(gè)參數(shù)是一個(gè)pthread_mutexattr_t
類型的地址幔托,如果我們不傳pthread_mutexattr_t
,直接傳一個(gè)NULL蜂挪,相當(dāng)于創(chuàng)建一個(gè)默認(rèn)的互斥鎖重挑。
//方式一
pthread_mutex_init(mutex, NULL);
//方式二
// - 創(chuàng)建attr
pthread_mutexattr_t attr;
// - 初始化attr
pthread_mutexattr_init(&attr);
// - 設(shè)置attr類型
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);
// - 使用attr初始化鎖
pthread_mutex_init(&_lock, &attr);
// - 銷毀attr
pthread_mutexattr_destroy(&attr);
上面兩個(gè)方式是一個(gè)效果,那為什么使用attr棠涮,那就說(shuō)明除了default類型的還有其他類型谬哀,我們后面再說(shuō)。
在使用的時(shí)候用pthread_mutex_lock(&_lock);
和 pthread_mutex_unlock(&_lock);
加鎖解鎖严肪。
NSLock就是對(duì)這種普通互斥鎖的OC層面的封裝史煎。
RECURSIVE 遞歸鎖
調(diào)用pthread_mutexattr_settype的時(shí)候如果類型傳入PTHREAD_MUTEX_RECURSIVE
,會(huì)創(chuàng)建一個(gè)遞歸鎖诬垂。舉個(gè)例子吧劲室。
// 偽代碼
-(void)test {
lock;
[self test];
unlock;
}
如果是普通的鎖结窘,當(dāng)我們?cè)趖est方法中很洋,遞歸調(diào)用test,應(yīng)該會(huì)出現(xiàn)死鎖隧枫,因?yàn)楸籰ock喉磁,在遞歸調(diào)用時(shí)無(wú)法調(diào)用谓苟,一直等待。但是如果鎖是遞歸鎖协怒,他會(huì)允許同一個(gè)線程多次加鎖和解鎖檐迟,就可以解決這個(gè)問(wèn)題了。
NSRecursiveLock是對(duì)遞歸鎖的封裝甜熔。
Condition 條件鎖
我們直接上這種鎖的使用方法只搁,
- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}
// 線程1
// 刪除數(shù)組中的元素
- (void)__remove {
pthread_mutex_lock(&_mutex);
NSLog(@"__remove - begin");
if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}
[self.data removeLastObject];
NSLog(@"刪除了元素");
pthread_mutex_unlock(&_mutex);
}
// 線程2
// 往數(shù)組中添加元素
- (void)__add {
pthread_mutex_lock(&_mutex);
sleep(1);
[self.data addObject:@"Test"];
NSLog(@"添加了元素");
// 信號(hào)
pthread_cond_signal(&_cond);
// 廣播
// pthread_cond_broadcast(&_cond);
pthread_mutex_unlock(&_mutex);
}
我們創(chuàng)建了兩個(gè)線程,一個(gè)往數(shù)組中添加數(shù)據(jù)妖滔,一個(gè)刪除數(shù)據(jù)隧哮,我們通過(guò)這個(gè)條件鎖實(shí)現(xiàn)的效果就是在數(shù)組中還沒(méi)有數(shù)據(jù)的時(shí)候等待,數(shù)組中添加了一個(gè)數(shù)據(jù)之后在進(jìn)行刪除座舍。
條件鎖就是互斥鎖+條件沮翔。我們聲明一個(gè)條件并初始化。
@property (assign, nonatomic) pthread_cond_t cond;
//使用完后也要pthread_cond_destroy(&_cond);
pthread_cond_init(&_cond, NULL);
在__remove
方法中
if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}
如果線程1率先拿到所并加鎖曲秉,執(zhí)行到上面代碼這里發(fā)現(xiàn)數(shù)組中還沒(méi)有數(shù)據(jù)采蚀,就執(zhí)行pthread_cond_wait,此時(shí)線程1會(huì)暫時(shí)放開(kāi)_mutex這個(gè)鎖承二,并在這休眠等待榆鼠。
線程2在__add
方法中最開(kāi)始因?yàn)槟貌坏芥i,所以等待矢洲,在線程1休眠放開(kāi)鎖之后拿到鎖璧眠,加鎖,并執(zhí)行為數(shù)組添加數(shù)據(jù)的代碼读虏。添加完了之后會(huì)發(fā)個(gè)信號(hào)通知等待條件的線程责静,并解鎖。
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_mutex);
線程2執(zhí)行了pthread_cond_signal
之后盖桥,線程1就收到了通知灾螃,退出休眠狀態(tài),繼續(xù)執(zhí)行下面的代碼揩徊。
這個(gè)地方可能有人會(huì)有疑問(wèn)腰鬼,是不是線程2應(yīng)該先unlock再cond_dingnal,其實(shí)這個(gè)地方順序沒(méi)有太大差別塑荒,因?yàn)榫€程2執(zhí)行了pthread_cond_signal
之后熄赡,會(huì)繼續(xù)執(zhí)行unlock代碼,線程1收到signal通知后會(huì)推出休眠狀態(tài)齿税,同時(shí)線程1需要再一次持有這個(gè)鎖彼硫,就算此時(shí)線程2還沒(méi)有unlock,線程1等到線程2 unlock 的時(shí)間間隔很短,等到線程2 unlock 后線程1會(huì)再去持有這個(gè)鎖拧篮,并加鎖词渤。
NSCondition就是OC層面的條件鎖,內(nèi)部把mutex互斥鎖和條件封裝到了一起串绩。NSConditionLock其實(shí)也差不多缺虐,NSConditionLock可以指定具體的條件,這兩個(gè)OC層面的類的用法大家可以自行上網(wǎng)搜索礁凡。
dispatch_semaphore 信號(hào)量
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
//初始化
self.semaphore = dispatch_semaphore_create(5);
在初始化一個(gè)信號(hào)的的過(guò)程中傳入dispatch_semaphore_create
的值高氮,其實(shí)就代表了允許幾個(gè)線程同時(shí)訪問(wèn)。
再回到之前我們存錢(qián)取錢(qián)這個(gè)例子顷牌。
self.moneySemaphore = dispatch_semaphore_create(1);
我們一次只允許一個(gè)線程訪問(wèn)纫溃,所以在初始化的時(shí)候傳1。下面就是使用方法韧掩。
- (void)__drawMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
// ... 省略代碼
dispatch_semaphore_signal(self.moneySemaphore);
}
- (void)__saveMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
// ... 省略代碼
dispatch_semaphore_signal(self.moneySemaphore);
}
dispatch_semaphore_wait
是怎么上鎖的呢?
如果信號(hào)量>0的時(shí)候窖铡,讓信號(hào)量-1疗锐,并繼續(xù)往下執(zhí)行。
如果信號(hào)量<=0的時(shí)候费彼,休眠等待滑臊。
就這么簡(jiǎn)單。
dispatch_semaphore_signal
讓信號(hào)量+1箍铲。
小提示
在我們平時(shí)使用這種方法的時(shí)候雇卷,可以把信號(hào)量的代碼提取出來(lái)定義一個(gè)宏。
#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);
讀寫(xiě)安全方案
上面我們講到的線程同步方案都是每次只允許一個(gè)線程訪問(wèn)颠猴,在實(shí)際的情況中关划,讀寫(xiě)的同步方案應(yīng)該下面這樣:
- 每次只能有一個(gè)線程寫(xiě)
- 可以有多個(gè)線程同時(shí)讀
- 讀和寫(xiě)不能同時(shí)進(jìn)行
這就是多讀單寫(xiě),用于文件讀寫(xiě)的操作翘瓮。在我們的iOS中可以用下面這兩種解決方案贮折。
pthread_rwlock 讀寫(xiě)鎖
這個(gè)讀寫(xiě)鎖的用法很簡(jiǎn)單,跟之前的普通互斥鎖都差不多资盅,大家隨便搜一下應(yīng)該就能搜到调榄,我就不拿出來(lái)寫(xiě)了,這里主要是提一下這種鎖呵扛,大家以后有需要的時(shí)候可以用每庆。
dispatch_barrier_async 異步柵欄
首先在使用這個(gè)函數(shù)的時(shí)候,我們要用自己創(chuàng)建的并發(fā)隊(duì)列今穿。
如果傳入的是一個(gè)串行隊(duì)列或者全局的并發(fā)隊(duì)列缤灵,那dispatch_barrier_async
等同于dispatch_async
的效果。
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(self.queue, ^{
[self read];
});
dispatch_barrier_async(self.queue, ^{
[self write];
});
在讀取數(shù)據(jù)的時(shí)候,使用dispatch_async
往對(duì)列中添加任務(wù)凤价,在寫(xiě)數(shù)據(jù)時(shí)鸽斟,用dispatch_barrier_async
添加任務(wù)。
dispatch_barrier_async
添加的任務(wù)會(huì)等前面所有的任務(wù)都執(zhí)行完利诺,他再執(zhí)行富蓄,而且他執(zhí)行的時(shí)候,不允許有別的任務(wù)同時(shí)執(zhí)行慢逾。
atomic
我們都知道這個(gè)atomic是原子性的意思立倍。他保證了屬性setter和getter的原子性操作,相當(dāng)于在set和get方法內(nèi)部加鎖侣滩。
atomic修飾的屬性是讀/寫(xiě)安全的口注,但不是線程安全。
假設(shè)有一個(gè) atomic 的屬性 "name"君珠,如果線程 A 調(diào)用 [self setName:@"A"]寝志,線程 B 調(diào)用 [self setName:@"B"],線程 C 調(diào)用 [self name]策添,那么所有這些不同線程上的操作都將依次順序執(zhí)行——也就是說(shuō)材部,如果一個(gè)線程正在執(zhí)行 getter/setter,其他線程就得等待唯竹。因此乐导,屬性 name 是讀/寫(xiě)安全的。
但是浸颓,如果有另一個(gè)線程 D 同時(shí)在調(diào)[name release]物臂,那可能就會(huì)crash,因?yàn)?release 不受 getter/setter 操作的限制产上。也就是說(shuō)棵磷,這個(gè)屬性只能說(shuō)是讀/寫(xiě)安全的,但并不是線程安全的晋涣,因?yàn)閯e的線程還能進(jìn)行讀寫(xiě)之外的其他操作泽本。線程安全需要開(kāi)發(fā)者自己來(lái)保證。