正文:首先看一下問題引出,先看一些經(jīng)典的問題.
多線程的隱患
首先我們利用多線程的話肯定是好處多多,因為我們可以同時去做一些事情,大大的提高了效率.像我們下載視頻的時候就可以同時下載多個視頻,這樣是節(jié)省了很多時間,用戶體驗也會更好.但是用得時候也會存在一些安全隱患,比如同一塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源,這樣會出現(xiàn)一些數(shù)據(jù)錯亂和數(shù)據(jù)安全的問題.下面我們就看一些例子.
存錢取錢案例
比如我現(xiàn)在有1000元,同時有2個線程去處理,一個線程是取錢100元,一個線程是存錢100元,我制作了一張示意圖如下:
存錢取錢示意圖
我們從上面的圖應(yīng)該很清楚,存100、取100的最終結(jié)果就是還是剩余1000元,但是看我們上面的示意圖,最終結(jié)果要么是900要么是1100,這就與1000的結(jié)果對不上,所以很明顯用多線程是會存在隱患,下面我們用代碼演示一下上面的結(jié)果:
賣票案例
這個和上面的稍微有點(diǎn)差別,因為上面的是2個操作,而賣票呢,它是1個操作.同樣的比如我現(xiàn)在有1000張票,同時有2個線程去處理賣票,一個線程是一個線程賣票100張,另一個線程也是賣票100張,同時操作的話,也會出現(xiàn)異常,我制作了一張示意圖如下:
我們同樣的也可以用代碼演示一下效果:
上面的兩個,大家可以試試.接下來針對上面的問題,我們就引出了今天的主角,線程同步技術(shù)
線程同步技術(shù)
解決方案:使用線程同步技術(shù) (同步,就是協(xié)同步調(diào),按照預(yù)定的先后次序進(jìn)行運(yùn)行),我們先來看下線程同步技術(shù)有哪些方案:線程同步方案最常見的技術(shù)就是:加鎖.大概方案如下(大致這么多方案,當(dāng)然還有其他的)
OSSpinLock (自旋鎖)
os_unfair_lock (互斥鎖)
pthread_mutex (互斥鎖、遞歸鎖)(里面3種類型,目前只說2種對我們有用的)
dispatch_queue (DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
以上的這些都是可以做到線程同步方案,我會一個一個介紹,并且介紹它們的優(yōu)缺點(diǎn),性能怎么樣,我們怎么去選擇等等.我們就以上面的例子講解.
OSSpinLock (自旋鎖)
OSSpinLock 叫做 "自旋鎖", 等待鎖的狀態(tài)會處于忙等( busy-wait )狀態(tài),一直占用著CPU內(nèi)存.頭文件導(dǎo)入,而且這個鎖是過期鎖,iOS10以后就過期了,但是我們還是來看看,因為面試中可能會遇到,用法如下:
初始化鎖:OSSpinLock spinLock = OS_SPINLOCK_INIT;
加鎖: OSSpinLockLock(&_spinLock);
解鎖: OSSpinLockUnlock(&_spinLock);
我們先看賣票:
我是加鎖和解鎖了,為什么上面的代碼還有問題,有發(fā)現(xiàn)原因的嗎?這是因為SpinLock是局部變量,所以我們進(jìn)去都是初始化了一把新鎖,這把鎖并沒有被使用過,是達(dá)不到加鎖的目的.所以所有的線程都是用同一把鎖才能達(dá)到加鎖的目的.請看下面代碼:
確實是剩下85張,沒有問題.原理是這樣,每次執(zhí)行saleTicket都會進(jìn)入 //加鎖: OSSpinLockLock(&_SpinLock) 這個代碼,第一次進(jìn)來是正常給_SpinLock加鎖,第二次進(jìn)來的時候,發(fā)現(xiàn)_SpinLock已經(jīng)被人加了鎖,它會在這邊等待,等待這把鎖被解鎖,解鎖完了以后然后它再去加鎖,就這樣依次進(jìn)行,就保證了里面的那段代碼同時只有一個線程在處理.這就解決了線程同步的問題.
接下來我們就看存錢和取錢的問題.存錢、取錢是2個操作,我們是用同一把鎖還是2把鎖?思考了這個問題我們就知道怎么做了,因為存錢和取錢是不能同時進(jìn)行,所以我們就用同一把鎖即可(2把鎖是有問題的大家可以自己試試),請看下面的代碼,我們驗證一下:
自旋鎖"忙等"狀態(tài)是怎么等呢?忙等就是一直忙碌,而且還在等待,類似這樣while(鎖還沒有解開),就會一直執(zhí)行,占用cpu,直到鎖被放開.而且OSSpinLock是已經(jīng)過期了,而且目前已經(jīng)不再安全,可能會出現(xiàn)優(yōu)先級反轉(zhuǎn)的問題.
下面說一下線程的調(diào)度問題
其實你看上面的圖,如果隨著時間的推移,操作系統(tǒng)把時間給thread1一點(diǎn)時間,再給thread2一點(diǎn)時間,再給thread3一點(diǎn)時間,而且這個時間周期非常短,就這樣一直非??斓那袚Q,這樣下來給我們的感覺就是同時執(zhí)行.這就是實現(xiàn)多線程的一個方案.也就是多線程的原理,我們也可以說這是時間片輪轉(zhuǎn)調(diào)度算法.調(diào)用進(jìn)程或者線程都是用這套算法
還有個就是線程的優(yōu)先級問題,比如thread1的優(yōu)先級比較高,那么操作系統(tǒng)就會給thread1多一點(diǎn)時間去執(zhí)行.其他的線程就少一點(diǎn)時間去執(zhí)行.這樣的話,我們使用自旋鎖就會存在一個優(yōu)先級反轉(zhuǎn)的問題.比如thread1優(yōu)先級非常高,thread2優(yōu)先級很低.首先是thread2先進(jìn)去加鎖,thread1再進(jìn)來就會等thread2解鎖,由于thread1的優(yōu)先級非常高,CPU就把大量的時間給了thread1,此時可能導(dǎo)致thread2沒有時間執(zhí)行解鎖,thread1就會一直執(zhí)行等待,有點(diǎn)死鎖的感覺.
這樣大家想一想:如果優(yōu)先級高的不是忙等,而是休眠,休息就不會占用CPU,那不就是解決了這個問題.
os_unfair_lock (互斥鎖)
os_unfair_lock用于取代不安全OSSpinLock,是從iOS10開始支持.
從底層調(diào)用看,等待os_unfair_lock鎖的線程會處于休眠狀態(tài),并非忙等(后面會證明一下)
它的用法和OSSpinLock非常像,需要倒入頭文件,用法如下:
初始化鎖:os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;
加鎖: os_unfair_lock_lock(&_unfairLock);
解鎖:os_unfair_lock_unlock(&_unfairLock);
下面我們就去看一下用法
存錢和取錢也是一樣的道理,我們可以自己試試.
pthread_mutex (互斥鎖)
像這種pthread開頭的一般都是跨平臺的Windows、linux等等都是可以用的,mutex叫做"互斥鎖",等待鎖的線程會處于休眠狀態(tài)
其實用法都是差不多,我們先來看下怎么用,這個稍微代碼多一點(diǎn)點(diǎn)
//初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
//初始化鎖
pthread_mutex_init(&_mutex, &attr);(&attr也可以傳NULL,這樣的話,上面的都是默認(rèn)的,上面可以都不用寫)
//加鎖
pthread_mutex_lock(&_mutex);
//解鎖
pthread_mutex_unlock(&_mutex);
//銷毀相關(guān)資源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&_mutex);
其中PTHREAD_MUTEX_NORMAL是鎖的類型,后面會細(xì)說,先傳默認(rèn)PTHREAD_MUTEX_NORMAL
先看運(yùn)行結(jié)果:
沒有問題,記得銷毀哈,之前說的2個鎖,沒有提供銷毀的方法,那我們就不寫,如果提供了,我們還是寫一下的好!
pthread_mutex (遞歸鎖)
我們再看另一種情況,請看下面的代碼:
上面這些代碼會出現(xiàn)什么情況?死鎖,會出現(xiàn)相互等待的情況,只會輸出第一個NSLog,遇到這種情況我們怎么解決才好呢?2把不同的鎖即可解決問題,就是otherMutexTest里面一把鎖,otherMutexTest2里面另一把鎖就可以解決了,這個我就不截圖了,我們可以自己試試
再看下面另一種情況:出現(xiàn)遞歸怎么辦?如下圖
遇到上面的這種情況我們又怎么處理,如果就像截圖那樣的話,就會出現(xiàn)休眠等待.如果我們想執(zhí)行下去,我們就是可以設(shè)置鎖的類型來解決這個問題:一共3種類型如下
我們只要把鎖的類型換成遞歸鎖,立刻就能解決這個問題,我們加一個遞歸停止條件,不然會一直運(yùn)行
還有一個注意的,這個允許重復(fù)加鎖,一定是在同一個線程,如果是多個線程的話,就不行.遞歸鎖:允許同一個線程對一把鎖重復(fù)加鎖.
自旋鎖、互斥鎖匯編分析
自旋鎖:一直忙等,占用CPU內(nèi)存,一直在執(zhí)行代碼;互斥鎖:不等待,休眠.不執(zhí)行代碼.我們怎么去證明這個問題呢?我們可以從匯編實現(xiàn)上去證明這個問題,我們先看OSSpinLock自旋鎖:
首先我們?nèi)绻眠@個上面的來調(diào)試的話,是看不出來什么效果的,因為這里面都是一大段一大段匯編代碼執(zhí)行的,我們需要一句一句的執(zhí)行匯編指令.就需要敲si,s是step的意思,代碼一行一行的執(zhí)行,如果只用s的話,就是一行oc代碼執(zhí)行,一行oc對應(yīng)可能一大段匯編,所以我們還需要加i,i是instruction的意思是一行一行匯編指令執(zhí)行,簡稱si. 還有個是nexti,它也是一行一行匯編指令執(zhí)行,只是nexti它是遇到函數(shù)就會一下執(zhí)行過去.因為我們要看函數(shù)實現(xiàn),所以我們用si.
我們再看一下,我代碼是怎么寫的:
我是創(chuàng)建了10個線程去執(zhí)行賣票,而且在賣票中間sleep(100),這樣是為了,第一條線程進(jìn)去,我們不管,我們主要看第二條線程在這等待的時間,到底做了什么事.所以我們主要看第二條的匯編代碼.sleep(100)是為了時間長點(diǎn),方便我們能看出做什么事.如果時間太短,直接第二條線程就不等待,那我們就看不到效果,請看下面的結(jié)果
從上面的結(jié)果看,進(jìn)入OSSpinLockLock函數(shù),它會一直在81aef那里一直循環(huán)執(zhí)行,這是外循環(huán),我們所說的自旋鎖就是這樣,一直循環(huán)執(zhí)行,占用CPU內(nèi)存.一旦有人放開這把鎖就會條件循環(huán)結(jié)束,不會再執(zhí)行循環(huán).
接下來我們看看互斥鎖pthread_mutex
查找的方法和上面的一樣,我就截圖最關(guān)鍵的圖即可,請看下面:
執(zhí)行到最后,直接是callsys,調(diào)用系統(tǒng)的方法,是不是類似我之前說的runloop里面的休眠的方法,而且我們知道休眠是任何事情都不會做,不占用CPU內(nèi)存,所以我們最后看到,我的模擬器立刻又彈出來了,說明確實是睡眠,不占用任何CPU內(nèi)存.
os_unfair_lock_lock我們可以用上面的方法嘗試,它的結(jié)果也是互斥鎖.
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7073瀏覽量
89138 -
操作系統(tǒng)
+關(guān)注
關(guān)注
37文章
6840瀏覽量
123407 -
代碼
+關(guān)注
關(guān)注
30文章
4797瀏覽量
68707 -
多線程同步
+關(guān)注
關(guān)注
0文章
2瀏覽量
5242
發(fā)布評論請先 登錄
相關(guān)推薦
評論