Q:為什么出現(xiàn)多線程伶贰?
A:為了實現(xiàn)同時干多件事的需求(并發(fā))诱贿,同時進(jìn)行著下載和頁面UI刷新娃肿。對于處理器,為每個線程分配執(zhí)行的時間片珠十。在不同的線程之間切換料扰。
Q:多線程和單線程,那個計算效率更高焙蹭?
A:單線程晒杈,計算效率更高。cpu在從A線程切換到B線程孔厉,需要保存A線程的寄存器拯钻,棧信息,同時使用B線程的寄存器撰豺,棧信息粪般。叫做切換上下文。這里就出現(xiàn)了線程切換的開銷了污桦,所以要合理的使用多線程亩歹,開啟很多線程,會使運行效率大大下降。
Q:為什么會出現(xiàn)多線程的問題小作?
A:每個線程都有自己的棧信息亭姥,但是堆數(shù)據(jù)是共享的,線程之間存在共享資源顾稀。那么不同線程訪問共享數(shù)據(jù)的時候达罗,就出現(xiàn)了數(shù)據(jù)錯亂的問題。
Q:解決辦法静秆?
A:使用鎖
一粮揉、線程狀態(tài):
1、新建狀態(tài):線程創(chuàng)建
2诡宗、就緒狀態(tài):向線程對象發(fā)送start信息,線程對象被加入可調(diào)度線程池击儡,等待獲得CPU的使用權(quán)塔沃,獲得執(zhí)行的時間片,開始執(zhí)行
3阳谍、運行狀態(tài):就緒狀態(tài)獲得CPU使用權(quán)蛀柴,執(zhí)行代碼
4、阻塞狀態(tài):
?(1)矫夯、同步阻塞鸽疾,運行過程中要獲得鎖,才能執(zhí)行下面的代碼训貌,但是獲得失敗制肮,就會被加入鎖池中,只有鎖被別的線程釋放递沪,并且被本線程獲得豺鼻,才會從阻塞狀態(tài),變成就緒狀態(tài)
?(2)款慨、其他阻塞儒飒,sleep,或是執(zhí)行I/O 操作檩奠,或是barrier操作桩了,線程會變成阻塞狀態(tài)
對于不同線程之間的調(diào)度:
1、喚醒別的線程埠戳,通過釋放鎖(互斥量)井誉,釋放條件變量,增加信號量整胃,可以將別的線程從阻塞狀態(tài)變成就緒狀態(tài)送悔,別的線程能繼續(xù)執(zhí)行
2、阻塞線程,通過鎖欠啤,條件變量荚藻,信號量等方式,可以使線程進(jìn)入阻塞狀態(tài)洁段。
多線程導(dǎo)致的問題:
多線程应狱,對于共享數(shù)據(jù)而言,不同線程訪問很可能導(dǎo)致數(shù)據(jù)錯亂的問題祠丝。舉例:一個線程通過條件判斷已經(jīng)進(jìn)入后面的代碼執(zhí)行疾呻,但是這個值同時在別的線程被修改了。一個變量的賦值和寫入写半,多線程操作也會導(dǎo)致問題岸蜗。為了避免多線程的問題,出現(xiàn)了鎖的概念叠蝇,這個鎖概念的基礎(chǔ)就是原子操作璃岳,有了原子操作,保證了加鎖的過程中悔捶,不會被多線程干擾铃慷,要么加鎖成功,可以繼續(xù)向下執(zhí)行蜕该,要么加鎖失敗犁柜,線程進(jìn)入阻塞狀態(tài)。
注意:這里的加鎖操作堂淡,是原子操作馋缅。但是之后執(zhí)行的代碼不是原子操作。類似于下面的代碼绢淀,只有if條件判斷是原子操作股囊,臨界代碼執(zhí)行并不是原子操作,還會被線程調(diào)度影響更啄。
二稚疹、原子操作
不會被線程調(diào)度機制打斷的操作,這個操作一旦開始祭务,直到運行結(jié)束内狗,中間不會切換到另外一個線程。原子操作是不可分割的义锥,怎樣實現(xiàn)這中不可分割那柳沙?對于單核處理器而言,指令就是原子操作拌倍,出現(xiàn)了test_and_set赂鲤、test_and_clear這樣的指令來保證臨界資源互斥噪径。(互斥量)
atomic_flag_test_set 保證了線程的安全
偽代碼
對于多處理器而言,原子操作的實現(xiàn)更加復(fù)雜一些数初,單條指令已經(jīng)不是原子操作了找爱,例子:不同的cpu都在遞減某一個共享的值
⒈ CPU A(CPU A上所運行的進(jìn)程,以下同)從內(nèi)存單元把當(dāng)前計數(shù)值⑵裝載進(jìn)它的寄存器中泡孩;
⒉ CPU B從內(nèi)存單元把當(dāng)前計數(shù)值⑵裝載進(jìn)它的寄存器中车摄。
⒊ CPU A在它的寄存器中將計數(shù)值遞減為1;
⒋ CPU B在它的寄存器中將計數(shù)值遞減為1仑鸥;
⒌ CPU A把修改后的計數(shù)值⑴寫回內(nèi)存單元吮播。
⒍ CPU B把修改后的計數(shù)值⑴寫回內(nèi)存單元。
我們看到眼俊,內(nèi)存里的計數(shù)值應(yīng)該是0意狠,然而它卻是1。如果該計數(shù)值是一個共享資源的引用計數(shù)疮胖,每個進(jìn)程都在遞減后把該值與0進(jìn)行比較环戈,從而確定是否需要釋放該共享資源。這時获列,兩個進(jìn)程都去掉了對該共享資源的引用谷市,但沒有一個進(jìn)程能夠釋放它--兩個進(jìn)程都推斷出:計數(shù)值是1蛔垢,共享資源仍然在被使用击孩。
原子性不可能由軟件單獨保證--必須需要硬件的支持,因此是和架構(gòu)相關(guān)的鹏漆。在x86 平臺上巩梢,CPU提供了在指令執(zhí)行期間對總線加鎖的手段。CPU芯片上有一條引線#HLOCK pin艺玲,如果匯編語言的程序中在一條指令前面加上前綴"LOCK"括蝠,經(jīng)過匯編以后的機器代碼就使CPU在執(zhí)行這條指令的時候把#HLOCK pin的電位拉低胰默,持續(xù)到這條指令結(jié)束時放開姥闪,從而把總線鎖住,這樣同一總線上別的CPU就暫時不能通過總線訪問內(nèi)存了掺炭,保證了這條指令在多處理器環(huán)境中的原子性秒梳。
原子操作和線程安全的關(guān)系:
原子操作執(zhí)行過程中法绵,不會出現(xiàn)線程切換。但是線程安全的代碼運行過程中可能出現(xiàn)線程切換酪碘。正式因為用原子操作保證線程安全朋譬,加鎖和解鎖操作是原子操作,但是對于共享資源的獲取和修改兴垦,就是線程安全了徙赢。加鎖代表著字柠,加鎖內(nèi)部的代碼在一個時刻只能被同一個線程訪問,其他線程到了這個位置狡赐,會進(jìn)入阻塞狀態(tài)窑业。只有等那個拿到鎖的線程釋放鎖,這些阻塞線程才會被喚醒阴汇。嘗試加鎖数冬,加鎖成功,繼續(xù)執(zhí)行搀庶。
原子操作是鎖功能實現(xiàn)的基礎(chǔ)拐纱,因為有了鎖,臨界代碼才線程安全哥倔。
三秸架、鎖
多線程安全問題:
看到結(jié)果,會發(fā)現(xiàn)出現(xiàn)了多線程數(shù)據(jù)錯亂的問題:
不同種類的鎖:
1咆蒿、互斥鎖东抹,NSLock,pthread_mutex_t,最簡單的加鎖機制,當(dāng)一個線程對一個代碼區(qū)域加鎖之后沃测,其他線程當(dāng)要進(jìn)入這個區(qū)域的時候會阻塞缭黔,保證這段代碼是線程安全的,這段區(qū)域蒂破,通常有獲取共享資源馏谨,修改資源的操作
(1)、將耗時操作也上鎖了:
沒有出現(xiàn)數(shù)據(jù)錯亂:執(zhí)行時間 ?20s
(2)附迷、只對于那些獲得修改共享數(shù)據(jù)的操作加鎖惧互,無關(guān)的耗時操作不加鎖。沒有數(shù)據(jù)錯亂的問題喇伯,執(zhí)行時間15s
注意:對于無關(guān)的耗時操作不能加鎖喊儡,不然耗時操作不執(zhí)行完就不能釋放鎖。別的線程一直阻塞稻据,等待鎖的釋放艾猜。就會導(dǎo)致性能問題。
Lock還有其他方法:
tryLock 嘗試加鎖捻悯,加鎖成功匆赃,立刻返回ture,不能返回false秋度,不會發(fā)生阻塞炸庞。
tryLock和自旋鎖還是區(qū)別很大的,自旋鎖荚斯,加鎖失敗埠居,不會繼續(xù)執(zhí)行查牌,而是一直獲取,直到成功獲得鎖滥壕。而tryLock是立即返回結(jié)果并且向下執(zhí)行纸颜。
lockBeforeDate ?加鎖失敗,會被阻塞绎橘,獲得鎖或是超過時間胁孙,線程轉(zhuǎn)成就緒狀態(tài)。
使用pthread_mutex 互斥量称鳞,實現(xiàn)鎖的功能涮较。NSLock是基于pthread_mutex實現(xiàn)的。
// 初始化互斥鎖 int pthread_mutex_init (pthread_mutex_t * restrict mutex , \ const pthread_mutexattr_t * restrict attr ) ;
int pthread_mutex_destroy (pthread_mutex_t * mutex ) ;
// 加鎖 int pthread_mutex_lock (pthread_mutex_t *mutex) ;
// 解鎖 int pthread_mutex_unlock (pthread_mutex_t *mutex) ;
// 嘗試加鎖 int pthread_mutex_trylock (pthread_mutex_t *mutex) ;
// 帶超時的嘗試加鎖冈止,防止死鎖的一種方式 int pthread_mutex_timedlock (pthread_mutex_t * restrict mutex , \ const struct timespec * restrict abstime ) ;
查看使用NSLock調(diào)用堆棧:
(1)狂票、可以看到其他線程都處在等待鎖的狀態(tài),只有一個線程真正獲得了鎖
(2)熙暴、發(fā)現(xiàn)NSLock底層調(diào)用的就是pthread_mutex
2闺属、自旋鎖:
當(dāng)線程嘗試加鎖,如果加鎖失敗周霉,不會進(jìn)入阻塞狀態(tài)掂器,而是會一直嘗試加鎖,直到加鎖成功俱箱。使用OSSpinLock
自旋鎖的忙等出現(xiàn)国瓮,是為了節(jié)省線程上下文快速切換帶來的開銷。比如A線程在甲處理器上運行匠楚,A線程獲得了鎖巍膘,B線程在乙處理器上運行厂财,現(xiàn)在B線程要加鎖芋簿,如果是傳統(tǒng)的互斥鎖,那么B線程會進(jìn)入阻塞璃饱,并且乙處理器會快速切換到另一個線程与斤。如果A線程獲得鎖之后執(zhí)行的操作耗時非常少,之后會立即釋放鎖荚恶,互斥鎖的性能就不好撩穿。這里就是忙等開銷大,還是立刻進(jìn)行上下文切換開銷大的問題谒撼。
只有一個線程獲得了鎖食寡,執(zhí)行。其他線程在忙等:
注意:OSSpinLock已經(jīng)在iOS10 之后廢棄了廓潜,廢棄的原因抵皱,是低優(yōu)先級線程獲得鎖善榛,但是低優(yōu)先級線程執(zhí)行的時間片比較短,任務(wù)遲遲不能完成呻畸,釋放鎖的時間也會延后移盆。那么高優(yōu)先級線程需要加鎖,就需要一直忙等伤为。導(dǎo)致優(yōu)先級反轉(zhuǎn)咒循。
建議使用os_unfair_lock 代替。
在iOS中atomic修飾的屬性绞愚,也是使用自旋鎖保證線程安全叙甸,產(chǎn)生的getter和setter方法是線程安全的。(使用atomic位衩,但是自己實現(xiàn)了getter蚁署,setter方法系統(tǒng)的實現(xiàn)就沒有用了)
3、條件變量:(cond蚂四,類似于一個信號光戈,也可以說條件,當(dāng)condsignal或是broadcast的時候遂赠,等待著這個cond的線程都會結(jié)束條件等待)
NSCondition久妆、NSConditonLock、pthread_cond_t 這三者都使用的是條件變量跷睦,NSCondition筷弦、NSConditonLock都是基于pthread_cond_t進(jìn)行封裝的。條件變量要和互斥鎖配合使用抑诸。
pthread_cond_t 基本函數(shù):
// 初始化條件變量 int pthread_cond_init (pthread_cond_t * restrict cond , \ pthread_condattr_t * restrict attr ) ;
// 銷毀條件變量 int pthread_cond_destroy ( pthread_cond_t * cond ) ;
// 等待事件發(fā)生 int pthread_cond_wait (pthread_cond_t * restrict cond , \ pthread_mutex_t * restrict mutex )
// 帶超時的等待烂琴,防止死鎖的一種方式 int pthread_cond_timedwait (pthread_cond_t * restrict cond , \ pthread_mutex_t * restrict mutex , \ const struct timespec * restrict tsptr ) ;
// 向任意一個在等待的進(jìn)程或線程通知鎖可用 int pthread_cond_signal ( pthread_cond_t *cond ) ;
// 通知所有在等待的進(jìn)程或者線程鎖可用 int pthread_cond_broadcast ( pthread_cond_t *cond ) ;
主要函數(shù):pthread_cond_wait 有兩個參數(shù)一個是cond,另外一個是互斥鎖mutex蜕乡,也就是條件變量是和鎖共同使用的奸绷。pthread_cond_wait時,會將mutext unlock层玲,同時進(jìn)入等待号醉,只有等待條件實現(xiàn),并且能夠獲得這個mutex 時辛块,才會繼續(xù)執(zhí)行畔派。
例子:存在兩個生產(chǎn)者,存在兩個消費者润绵,消費物品為饅頭线椰,生產(chǎn)者不停生產(chǎn),生產(chǎn)有隨機的時間消耗尘盼,生產(chǎn)10個憨愉,不再生產(chǎn)呜魄,只要發(fā)現(xiàn)不滿10個,就接續(xù)生產(chǎn)莱衩。消費者一直消費爵嗅,消費會帶來隨機的時間消耗。
NSCondition 使用的仍然是pthread_cond_wait
注意:
(1)笨蚁、為什么要使用條件變量睹晒?能不能用互斥鎖代替條件變量,當(dāng)消費者發(fā)現(xiàn)條件不滿足wait的時候括细,讓出了互斥鎖伪很,并且線程進(jìn)入了阻塞,這里的等待不是等待一個另一個互斥鎖奋单,因為并沒有任何線程獲得了這個額外的鎖锉试。因為生產(chǎn)者不會被這個鎖影響,生產(chǎn)者只負(fù)責(zé)生產(chǎn)览濒,并且通知cond完成呆盖。
(2)、pthread_cond_wait 釋放鎖贷笛,并且對cond進(jìn)行阻塞等待应又,是一個原子操作,才能保證這些操作是線程安全的乏苦。
(3)株扛、NSCondition 實現(xiàn)了NSLocking協(xié)議,除了cond 外汇荐,還自帶一個互斥鎖洞就,這個功能能通過一個NSCondition對象 實現(xiàn).
可以只使用condition的wait和broadcast功能,但是因為沒有使用互斥鎖的功能掀淘,所以會出現(xiàn)數(shù)據(jù)錯亂的問題旬蟋,所以還是老老實實把鎖功能也用上。這樣就不會導(dǎo)致數(shù)據(jù)錯亂的問題繁疤。
(4)咖为、如果不適用condition的wait和broadCast功能秕狰,只使用互斥鎖稠腊。那么結(jié)果就像是自旋鎖一樣,消費者者線程當(dāng)food 不足的時候,一直處于忙等狀態(tài)鸣哀。知道food足夠才開始工作架忌。
4、NSConditionLock 提供了一個條件控制?
5我衬、NSRecursiveLock 遞歸鎖叹放,對于一些遞歸調(diào)用而言饰恕,使用普通的互斥鎖會造成死鎖,所以出現(xiàn)了遞歸鎖井仰。同一個線程可以多次獲得遞歸鎖埋嵌,并不會出現(xiàn)問題,只要保證獲得一次俱恶,釋放一次雹嗦。
NSRecursiveLock 就是一個pthread_mutex 的特殊類型。
注意:
常用的@synchronized 就是一種遞歸鎖合是,@synchronized的好處就是了罪,程序自己產(chǎn)生了鎖對象,而不用我們產(chǎn)生聪全。至于后面緊跟的參數(shù)泊藕,底層維護(hù)著一個鎖表。能夠通過后面緊跟參數(shù)的哈希值难礼,迅速的拿到這個鎖對象娃圆。
6、讀寫鎖:
讀操作蛾茉,和寫操作進(jìn)行了區(qū)分踊餐。一個線程獲得了讀鎖,其他線程仍能獲得讀鎖臀稚,進(jìn)行讀操作吝岭。但是一旦一個線程獲取了寫鎖,那么不再允許以后的線程獲得讀鎖吧寺,或是寫鎖窜管。知道前面獲得讀鎖的線程執(zhí)行完畢。進(jìn)行寫操作稚机,寫入完畢幕帆。釋放讀鎖,或是寫鎖赖条。
iOS沒有直接提供除了pthread外更高程度的讀寫鎖失乾。讀寫鎖一般用于數(shù)據(jù)庫軟件。
// 初始化讀寫鎖 int pthread_rwlock_init ( pthread_rwlock_t * restrict rwlock , \ const pthread_rwlockattr_t * restrict attr ) ;
// 銷毀讀寫鎖 int pthread_rwlock_destroy (pthread_rwlock_t * rwlock ) ;
// 加讀鎖 int pthread_rwlock_rdlock ( pthread_rwlock_t * rwlock ) ;
// 加寫鎖 int pthread_rwlock_wrlock ( pthread_rwlock_t * rwlock ) ;
// 解鎖 int pthread_rwlock_unlock ( pthread_rwlock_t * rwlock ) ;
// 嘗試加讀鎖 int pthread_rwlock_tryrdlock ( pthread_rwlock_t * rwlock ) ;
// 嘗試加寫鎖 int pthread_rwlock_trywrlock ( pthread_rwlock_t * rwlock ) ;
// 帶有超時的讀寫鎖纬乍,避免死鎖的一種方式 int pthread_rwlock_timedrdlock ( pthread_rwlock_t * restrict rwlock ,\ const struct timespec * restrict tsptr ) ; int pthread_rwlock_timedwrlock ( pthread_rwlock_t * restrict rwlock , \ const struct timespec * restrict tsptr ) ;
7碱茁、信號量
讓多個進(jìn)程通過特殊變量展開交互,一個進(jìn)程在某一個關(guān)鍵點上被迫停止執(zhí)行直至接收到對應(yīng)的特殊變量值仿贬,通過這一措施纽竣,任何復(fù)雜的進(jìn)程交互要求均可得到滿足,這種特殊的變量就是信號量。
? 設(shè)s為一個記錄型數(shù)據(jù)結(jié)構(gòu)蜓氨,其中value為整型變量聋袋,系統(tǒng)初始化時為其賦值,PV操作的原語描述如下:
??????P(s):將信號量value值減1,若結(jié)果小于0,則執(zhí)行P操作的進(jìn)程被阻塞穴吹,若結(jié)果大于等于0,則執(zhí)行P操作的進(jìn)程將繼續(xù)執(zhí)行幽勒。
??????V(s):將信號量的值加1,若結(jié)果不大于0,則執(zhí)行V操作的進(jìn)程從信號量s有關(guān)的list所知隊列中釋放一個進(jìn)程,使其轉(zhuǎn)化為就緒態(tài)港令,自己則繼續(xù)執(zhí)行代嗤,若結(jié)果大于0,則執(zhí)行V操作的進(jìn)程繼續(xù)執(zhí)行。
不同操作系統(tǒng)有不同的實現(xiàn)
作用域
信號量: 進(jìn)程間或線程間(linux僅線程間的無名信號量pthread semaphore)
互斥鎖: 線程間
信號量: 只要信號量的value大于0缠借,其他線程就可以sem_wait成功干毅,成功后信號量的value減一。若value值不大于0泼返,則sem_wait使得線程阻塞硝逢,直到sem_post釋放后value值加一,但是sem_wait返回之前還是會將此value值減一
互斥鎖: 只要被鎖住,其他任何線程都不可以訪問被保護(hù)的資源
信號量
dispatch_semaphore_create(信號量值)// 創(chuàng)建信號量
dispatch_semaphore_wait(信號量绅喉,等待時間)// 等待渠鸽,并且降低信號量
dispatch_semaphore_signal(信號量) // 添加信號量
使用信號量完成生產(chǎn)者消費者模型,能保證一定程度上的安全柴罐。但是沒有互斥鎖那樣對于共享資源的保護(hù)徽缚,信號量只能保護(hù),消費者消費的時候一定是有food的革屠,但是沒辦法保證共享資源的數(shù)據(jù)正確性凿试。所以要實現(xiàn)線程安全,還是要使用鎖似芝。事實上那婉,信號量功能主要體現(xiàn)在流程控制上,對于共享資源的互斥保護(hù)党瓮,并不是信號量的功能详炬。當(dāng)一個線程因為某個資源不足被阻塞,或是要等待某個其他線程的流程執(zhí)行完畢寞奸∏好眨可以使用信號量。像是信號燈枪萄,告訴你能不能通行隐岛,要不要進(jìn)入等待。
參考文章:
http://www.reibang.com/p/eca71b7fda2c