Q:為什么出現多線程?
A:為了實現同時干多件事的需求(并發),同時進行著下載和頁面UI刷新。對于處理器,為每個線程分配執行的時間片。在不同的線程之間切換。
Q:多線程和單線程,那個計算效率更高?
A:單線程,計算效率更高。cpu在從A線程切換到B線程,需要保存A線程的寄存器,棧信息,同時使用B線程的寄存器,棧信息。叫做切換上下文。這里就出現了線程切換的開銷了,所以要合理的使用多線程,開啟很多線程,會使運行效率大大下降。
Q:為什么會出現多線程的問題?
A:每個線程都有自己的棧信息,但是堆數據是共享的,線程之間存在共享資源。那么不同線程訪問共享數據的時候,就出現了數據錯亂的問題。
Q:解決辦法?
A:使用鎖
一、線程狀態:
1、新建狀態:線程創建
2、就緒狀態:向線程對象發送start信息,線程對象被加入可調度線程池,等待獲得CPU的使用權,獲得執行的時間片,開始執行
3、運行狀態:就緒狀態獲得CPU使用權,執行代碼
4、阻塞狀態:
?(1)、同步阻塞,運行過程中要獲得鎖,才能執行下面的代碼,但是獲得失敗,就會被加入鎖池中,只有鎖被別的線程釋放,并且被本線程獲得,才會從阻塞狀態,變成就緒狀態
?(2)、其他阻塞,sleep,或是執行I/O 操作,或是barrier操作,線程會變成阻塞狀態
對于不同線程之間的調度:
1、喚醒別的線程,通過釋放鎖(互斥量),釋放條件變量,增加信號量,可以將別的線程從阻塞狀態變成就緒狀態,別的線程能繼續執行
2、阻塞線程,通過鎖,條件變量,信號量等方式,可以使線程進入阻塞狀態。
多線程導致的問題:
多線程,對于共享數據而言,不同線程訪問很可能導致數據錯亂的問題。舉例:一個線程通過條件判斷已經進入后面的代碼執行,但是這個值同時在別的線程被修改了。一個變量的賦值和寫入,多線程操作也會導致問題。為了避免多線程的問題,出現了鎖的概念,這個鎖概念的基礎就是原子操作,有了原子操作,保證了加鎖的過程中,不會被多線程干擾,要么加鎖成功,可以繼續向下執行,要么加鎖失敗,線程進入阻塞狀態。
注意:這里的加鎖操作,是原子操作。但是之后執行的代碼不是原子操作。類似于下面的代碼,只有if條件判斷是原子操作,臨界代碼執行并不是原子操作,還會被線程調度影響。
二、原子操作
不會被線程調度機制打斷的操作,這個操作一旦開始,直到運行結束,中間不會切換到另外一個線程。原子操作是不可分割的,怎樣實現這中不可分割那?對于單核處理器而言,指令就是原子操作,出現了test_and_set、test_and_clear這樣的指令來保證臨界資源互斥。(互斥量)
atomic_flag_test_set 保證了線程的安全
偽代碼
對于多處理器而言,原子操作的實現更加復雜一些,單條指令已經不是原子操作了,例子:不同的cpu都在遞減某一個共享的值
⒈ CPU A(CPU A上所運行的進程,以下同)從內存單元把當前計數值⑵裝載進它的寄存器中;
⒉ CPU B從內存單元把當前計數值⑵裝載進它的寄存器中。
⒊ CPU A在它的寄存器中將計數值遞減為1;
⒋ CPU B在它的寄存器中將計數值遞減為1;
⒌ CPU A把修改后的計數值⑴寫回內存單元。
⒍ CPU B把修改后的計數值⑴寫回內存單元。
我們看到,內存里的計數值應該是0,然而它卻是1。如果該計數值是一個共享資源的引用計數,每個進程都在遞減后把該值與0進行比較,從而確定是否需要釋放該共享資源。這時,兩個進程都去掉了對該共享資源的引用,但沒有一個進程能夠釋放它--兩個進程都推斷出:計數值是1,共享資源仍然在被使用。
原子性不可能由軟件單獨保證--必須需要硬件的支持,因此是和架構相關的。在x86 平臺上,CPU提供了在指令執行期間對總線加鎖的手段。CPU芯片上有一條引線#HLOCK pin,如果匯編語言的程序中在一條指令前面加上前綴"LOCK",經過匯編以后的機器代碼就使CPU在執行這條指令的時候把#HLOCK pin的電位拉低,持續到這條指令結束時放開,從而把總線鎖住,這樣同一總線上別的CPU就暫時不能通過總線訪問內存了,保證了這條指令在多處理器環境中的原子性。
原子操作和線程安全的關系:
原子操作執行過程中,不會出現線程切換。但是線程安全的代碼運行過程中可能出現線程切換。正式因為用原子操作保證線程安全,加鎖和解鎖操作是原子操作,但是對于共享資源的獲取和修改,就是線程安全了。加鎖代表著,加鎖內部的代碼在一個時刻只能被同一個線程訪問,其他線程到了這個位置,會進入阻塞狀態。只有等那個拿到鎖的線程釋放鎖,這些阻塞線程才會被喚醒。嘗試加鎖,加鎖成功,繼續執行。
原子操作是鎖功能實現的基礎,因為有了鎖,臨界代碼才線程安全。
三、鎖
多線程安全問題:
看到結果,會發現出現了多線程數據錯亂的問題:
不同種類的鎖:
1、互斥鎖,NSLock,pthread_mutex_t,最簡單的加鎖機制,當一個線程對一個代碼區域加鎖之后,其他線程當要進入這個區域的時候會阻塞,保證這段代碼是線程安全的,這段區域,通常有獲取共享資源,修改資源的操作
(1)、將耗時操作也上鎖了:
沒有出現數據錯亂:執行時間 ?20s
(2)、只對于那些獲得修改共享數據的操作加鎖,無關的耗時操作不加鎖。沒有數據錯亂的問題,執行時間15s
注意:對于無關的耗時操作不能加鎖,不然耗時操作不執行完就不能釋放鎖。別的線程一直阻塞,等待鎖的釋放。就會導致性能問題。
Lock還有其他方法:
tryLock 嘗試加鎖,加鎖成功,立刻返回ture,不能返回false,不會發生阻塞。
tryLock和自旋鎖還是區別很大的,自旋鎖,加鎖失敗,不會繼續執行,而是一直獲取,直到成功獲得鎖。而tryLock是立即返回結果并且向下執行。
lockBeforeDate ?加鎖失敗,會被阻塞,獲得鎖或是超過時間,線程轉成就緒狀態。
使用pthread_mutex 互斥量,實現鎖的功能。NSLock是基于pthread_mutex實現的。
// 初始化互斥鎖 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調用堆棧:
(1)、可以看到其他線程都處在等待鎖的狀態,只有一個線程真正獲得了鎖
(2)、發現NSLock底層調用的就是pthread_mutex
2、自旋鎖:
當線程嘗試加鎖,如果加鎖失敗,不會進入阻塞狀態,而是會一直嘗試加鎖,直到加鎖成功。使用OSSpinLock
自旋鎖的忙等出現,是為了節省線程上下文快速切換帶來的開銷。比如A線程在甲處理器上運行,A線程獲得了鎖,B線程在乙處理器上運行,現在B線程要加鎖,如果是傳統的互斥鎖,那么B線程會進入阻塞,并且乙處理器會快速切換到另一個線程。如果A線程獲得鎖之后執行的操作耗時非常少,之后會立即釋放鎖,互斥鎖的性能就不好。這里就是忙等開銷大,還是立刻進行上下文切換開銷大的問題。
只有一個線程獲得了鎖,執行。其他線程在忙等:
注意:OSSpinLock已經在iOS10 之后廢棄了,廢棄的原因,是低優先級線程獲得鎖,但是低優先級線程執行的時間片比較短,任務遲遲不能完成,釋放鎖的時間也會延后。那么高優先級線程需要加鎖,就需要一直忙等。導致優先級反轉。
建議使用os_unfair_lock 代替。
在iOS中atomic修飾的屬性,也是使用自旋鎖保證線程安全,產生的getter和setter方法是線程安全的。(使用atomic,但是自己實現了getter,setter方法系統的實現就沒有用了)
3、條件變量:(cond,類似于一個信號,也可以說條件,當condsignal或是broadcast的時候,等待著這個cond的線程都會結束條件等待)
NSCondition、NSConditonLock、pthread_cond_t 這三者都使用的是條件變量,NSCondition、NSConditonLock都是基于pthread_cond_t進行封裝的。條件變量要和互斥鎖配合使用。
pthread_cond_t 基本函數:
// 初始化條件變量 int pthread_cond_init (pthread_cond_t * restrict cond , \ pthread_condattr_t * restrict attr ) ;
// 銷毀條件變量 int pthread_cond_destroy ( pthread_cond_t * cond ) ;
// 等待事件發生 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 ) ;
// 向任意一個在等待的進程或線程通知鎖可用 int pthread_cond_signal ( pthread_cond_t *cond ) ;
// 通知所有在等待的進程或者線程鎖可用 int pthread_cond_broadcast ( pthread_cond_t *cond ) ;
主要函數:pthread_cond_wait 有兩個參數一個是cond,另外一個是互斥鎖mutex,也就是條件變量是和鎖共同使用的。pthread_cond_wait時,會將mutext unlock,同時進入等待,只有等待條件實現,并且能夠獲得這個mutex 時,才會繼續執行。
例子:存在兩個生產者,存在兩個消費者,消費物品為饅頭,生產者不停生產,生產有隨機的時間消耗,生產10個,不再生產,只要發現不滿10個,就接續生產。消費者一直消費,消費會帶來隨機的時間消耗。
NSCondition 使用的仍然是pthread_cond_wait
注意:
(1)、為什么要使用條件變量?能不能用互斥鎖代替條件變量,當消費者發現條件不滿足wait的時候,讓出了互斥鎖,并且線程進入了阻塞,這里的等待不是等待一個另一個互斥鎖,因為并沒有任何線程獲得了這個額外的鎖。因為生產者不會被這個鎖影響,生產者只負責生產,并且通知cond完成。
(2)、pthread_cond_wait 釋放鎖,并且對cond進行阻塞等待,是一個原子操作,才能保證這些操作是線程安全的。
(3)、NSCondition 實現了NSLocking協議,除了cond 外,還自帶一個互斥鎖,這個功能能通過一個NSCondition對象 實現.
可以只使用condition的wait和broadcast功能,但是因為沒有使用互斥鎖的功能,所以會出現數據錯亂的問題,所以還是老老實實把鎖功能也用上。這樣就不會導致數據錯亂的問題。
(4)、如果不適用condition的wait和broadCast功能,只使用互斥鎖。那么結果就像是自旋鎖一樣,消費者者線程當food 不足的時候,一直處于忙等狀態。知道food足夠才開始工作。
4、NSConditionLock 提供了一個條件控制?
5、NSRecursiveLock 遞歸鎖,對于一些遞歸調用而言,使用普通的互斥鎖會造成死鎖,所以出現了遞歸鎖。同一個線程可以多次獲得遞歸鎖,并不會出現問題,只要保證獲得一次,釋放一次。
NSRecursiveLock 就是一個pthread_mutex 的特殊類型。
注意:
常用的@synchronized 就是一種遞歸鎖,@synchronized的好處就是,程序自己產生了鎖對象,而不用我們產生。至于后面緊跟的參數,底層維護著一個鎖表。能夠通過后面緊跟參數的哈希值,迅速的拿到這個鎖對象。
6、讀寫鎖:
讀操作,和寫操作進行了區分。一個線程獲得了讀鎖,其他線程仍能獲得讀鎖,進行讀操作。但是一旦一個線程獲取了寫鎖,那么不再允許以后的線程獲得讀鎖,或是寫鎖。知道前面獲得讀鎖的線程執行完畢。進行寫操作,寫入完畢。釋放讀鎖,或是寫鎖。
iOS沒有直接提供除了pthread外更高程度的讀寫鎖。讀寫鎖一般用于數據庫軟件。
// 初始化讀寫鎖 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、信號量
讓多個進程通過特殊變量展開交互,一個進程在某一個關鍵點上被迫停止執行直至接收到對應的特殊變量值,通過這一措施,任何復雜的進程交互要求均可得到滿足,這種特殊的變量就是信號量。
? 設s為一個記錄型數據結構,其中value為整型變量,系統初始化時為其賦值,PV操作的原語描述如下:
??????P(s):將信號量value值減1,若結果小于0,則執行P操作的進程被阻塞,若結果大于等于0,則執行P操作的進程將繼續執行。
??????V(s):將信號量的值加1,若結果不大于0,則執行V操作的進程從信號量s有關的list所知隊列中釋放一個進程,使其轉化為就緒態,自己則繼續執行,若結果大于0,則執行V操作的進程繼續執行。
不同操作系統有不同的實現
作用域
信號量: 進程間或線程間(linux僅線程間的無名信號量pthread semaphore)
互斥鎖: 線程間
信號量: 只要信號量的value大于0,其他線程就可以sem_wait成功,成功后信號量的value減一。若value值不大于0,則sem_wait使得線程阻塞,直到sem_post釋放后value值加一,但是sem_wait返回之前還是會將此value值減一
互斥鎖: 只要被鎖住,其他任何線程都不可以訪問被保護的資源
信號量
dispatch_semaphore_create(信號量值)// 創建信號量
dispatch_semaphore_wait(信號量,等待時間)// 等待,并且降低信號量
dispatch_semaphore_signal(信號量) // 添加信號量
使用信號量完成生產者消費者模型,能保證一定程度上的安全。但是沒有互斥鎖那樣對于共享資源的保護,信號量只能保護,消費者消費的時候一定是有food的,但是沒辦法保證共享資源的數據正確性。所以要實現線程安全,還是要使用鎖。事實上,信號量功能主要體現在流程控制上,對于共享資源的互斥保護,并不是信號量的功能。當一個線程因為某個資源不足被阻塞,或是要等待某個其他線程的流程執行完畢。可以使用信號量。像是信號燈,告訴你能不能通行,要不要進入等待。
參考文章:
http://www.lxweimin.com/p/eca71b7fda2c