回顧
在上一篇博客中,我們已經對進程和線程有了一定的了解了侮叮,那么本次博客將繼續(xù)講解!
1. 線程的
在程序開發(fā)中有個名詞——生命周期悼瘾,我們都知道 有生命周期囊榜,那么線程的生命周期是什么樣子的呢审胸?
- 線程生命周期
線程生命周期大致包括 5個階段:
:通過創(chuàng)建線程的函數方法,創(chuàng)建一個新的線程卸勺。
:線程創(chuàng)建完成之后砂沛,調用 start方法,線程這個時候處于等待狀態(tài)曙求,等待CPU時間分配執(zhí)行碍庵。
:當就緒的線程被調度并獲得CPU資源時,便進入運行狀態(tài)悟狱,run方法定義了線程的操作和功能静浴。
:在運行狀態(tài)的時候,可能因為某些原因導致運行狀態(tài)的線程變成了阻塞狀態(tài)芽淡,比如sleep马绝、等待同步鎖,線程就從可調度線程池移出挣菲,處于了阻塞狀態(tài)富稻,這個時候sleep到時、獲取同步鎖白胀,此時會重新添加到可調度線程池椭赋。喚醒的線程不會立刻執(zhí)行run方法,它們要再次等待CPU分配資源進入運行狀態(tài)或杠。
:如果線程正常執(zhí)行完畢后或線程被提前強制性的終止或出現異常導致結束哪怔,那么線程就要被銷毀,釋放資源向抢。
線程生命周期大致流程圖如下:
- 線程狀態(tài)演練方法
@interface ViewController ()
@property (nonatomic, strong) NSThread *p_thread;
@end
/**
線程狀態(tài)演練方法
*/
- (void)testThreadStatus{
NSLog(@"%d %d %d", self.p_thread.isExecuting, self.p_thread.isFinished, self.p_thread.isCancelled);
// 生命周期
if ( self.p_thread == nil || self.p_thread.isCancelled || self.p_thread.isFinished ) {
self.p_thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
self.p_thread.name = @"跑步線程";
[self.p_thread start];
}else{
NSLog(@"%@ 正在執(zhí)行",self.p_thread.name);
//可以設置彈框 ---> 這里直接制空
[self.p_thread cancel];
self.p_thread = nil;
}
}
2. 線程池的運行策略
線程池運行策略
線程的工作執(zhí)行认境,也是有一定的策略的,線程池的運行策略見下圖:
隊列滿了且正在運行的線程數量挟鸠,小于最大線程數叉信,則新進來的任務,會直接創(chuàng)建非核心線程來完成工作
- 線程池剛創(chuàng)建時艘希,里面沒有一個線程硼身。任務隊列是作為參數傳進來的。不過覆享,就算隊列里面有任務佳遂,線程池也不會馬上執(zhí)行它們。
當有任務時撒顿,線程池會做如下判斷:
如果正在運行的線程數量小于corePoolSize(核心線程數)丑罪,那么馬上創(chuàng)建核心線程運行這個任務。
如果正在運行的線程數量大于或等于corePoolSize,那么將這個任務放入隊列巍糯。
如果這時候隊列滿了啸驯,而且正在運行的線程數量小于maximumPoolSize(最大線程數)客扎,那么還是要創(chuàng)建非核心線程立刻運行這個任務祟峦。
如果隊列滿了,而且正在運行的線程數量大于或等于maximumPoolSize徙鱼,那么線程池飽和策略將進行處理宅楞。
當一個線程完成任務時,它會從隊列中取下一個任務來執(zhí)行袱吆。
當一個線程無事可做厌衙,超過一定的時間(超時)時,線程池會判斷绞绒,如果當前運行的線程數大于corePoolSize婶希,那么這個線程就被停掉。所以線程池的所有任務完成后蓬衡,它最終會收縮到corePoolSize的大小喻杈。
飽和策略
如果線程池中的隊列滿了,并且正在運行的線程數量已經大于等于當前線程池的最大線程數狰晚,則進行飽和策略的處理筒饰。
- AbortPolicy直接拋出RejectedExecutionExeception異常來阻?系統正常運?
- CallerRunsPolicy將任務回退到調?者
- DisOldestPolicy丟掉等待最久的任務
- DisCardPolicy直接丟棄任務
3. 自旋鎖和互斥鎖
任務的執(zhí)行速度的影響因素:
- CPU
- 任務的復雜度
- 任務的優(yōu)先級
- 線程的狀態(tài)
優(yōu)先級翻轉:
- IO 密集型(頻繁的等待線程)
- CPU密集型(很少等待)
- IO 比CPU更容易得到優(yōu)先級的提升
- 餓死:一直等不到執(zhí)行,就丟棄了
- 調度:優(yōu)先級和CPU的調度還有關系
優(yōu)先級因素:
- 用戶指定優(yōu)先級 --> threadPriority
// 主線程 512K
NSLog(@"%@ %zd K %d", [NSThread currentThread], [NSThread currentThread].stackSize / 1024, [NSThread currentThread].isMainThread);
NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(eat) object:nil];
// 1. name - 在應用程序中壁晒,收集錯誤日志瓷们,能夠記錄工作的線程!
// 否則不好判斷具體哪一個線程出的問題秒咐!
t.name = @"吃飯線程";
//This value must be in bytes and a multiple of 4KB.
t.stackSize = 1024*1024;
t.threadPriority = 1;
[t start];
threadPrioritythreadPriority 替換成qualityOfService(NSQualityOfService)
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
- 等待的頻繁度
-
長時間不執(zhí)行(也會提升優(yōu)先級)
下圖是:一個線程的經典案例
車票售賣系統谬晕,是線程工作執(zhí)行的經典案例,如果是多個窗口賣票携取,會出現資源搶奪的情況攒钳,如果 A窗口賣了一張票,B窗口不知道歹茶,或者同一時間 AB窗口賣同一張票夕玩,這樣就會出現問題,所有鎖的意義重大了惊豺。
自旋鎖
是一種用于保護多線程的鎖燎孟,與一般不同之處在于當自旋鎖嘗試獲取鎖時,以忙等待的形式不斷地循環(huán)檢查鎖是否可用尸昧;當上一個線程的任務沒有執(zhí)行完畢的時候()揩页,那么下一個線程會一直(不會睡眠);當上一個線程的任務執(zhí)行完畢烹俗,下一個線程會立即執(zhí)行爆侣。
在多的環(huán)境中萍程,對持有鎖較短的程序來說,使用自旋鎖代替一般的互斥鎖往往能夠提高程序的性能兔仰。
:OSSpinLock茫负、dispatch_semaphore_t
互斥鎖
當上一個線程的任務沒有執(zhí)行完畢的時候(),那么下一個線程會進入等待任務執(zhí)行完畢乎赴,當上一個線程的任務執(zhí)行完畢忍法,下一個線程會自動喚醒然后執(zhí)行任務,該任務也不會立刻執(zhí)行榕吼,而是成為可執(zhí)行狀態(tài)()饿序。
:pthread_mutex、@ synchronized羹蚣、NSLock原探、NSConditionLock、NSCondition顽素、NSRecursiveLock
自旋鎖和互斥鎖的特點
會忙等咽弦,所謂忙等,即在訪問被鎖資源時戈抄,調用者線程不會休眠离唬,而是不停循環(huán)在那里,直到被鎖資源釋放鎖划鸽。
會休眠输莺,所謂休眠,即在訪問被鎖資源時裸诽,調用者線程會休眠嫂用,此時cpu可以調度其他線程工作,直到被鎖資源釋放鎖丈冬。此時會喚醒休眠線程嘱函。
- 優(yōu)點在于,因為自旋鎖不會引起調用者睡眠埂蕊,所以不會進行線程調度往弓,CPU時間片輪轉等耗時操作。所有如果能在很短的時間內獲得鎖蓄氧,自旋鎖的效率遠高于互斥鎖函似。
- 缺點在于,自旋鎖一直占用CPU喉童,他在未獲得鎖的情況下撇寞,一直運行自旋,所以占用著CPU,如果不能在很短的時間內獲得鎖蔑担,這無疑會使CPU效率降低牌废。自旋鎖不能實現遞歸調用。
原子屬性和非原子屬性
在定義屬性時有和兩種選擇啤握,默認為屬性
- :原子屬性鸟缕,為setter方法加自旋鎖(即為單寫多讀)
- :非原子屬性,不會為setter方法加鎖
和的對比
- :線程安全恨统,需要消耗大量的資源叁扫;
- :非線程安全三妈,適合內存小的移動設備畜埋。
平時開發(fā)需要注意
- 如非需搶占資源的屬性(如購票,充值)畴蒲,所有屬性都聲明為悠鞍。
- 盡量避免多線程同一塊資源。
- 盡量將模燥、的業(yè)務邏輯交給服務器端處理咖祭,減小移動客戶端的壓力。
底層實現自旋鎖
我們在探索類的本質時蔫骂,對于類的屬性的setter
方法么翰,系統會有一層objc_setProperty
的封裝(libobjc.dylib源碼)
底層會調用reallySetProperty方法,在該方法的實現中辽旋,針對原子屬性浩嫌,添加了spinlock鎖
- objc_setProperty_atomic_copy
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
- reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
Spinlock
是Linux
內核中提供的一種比較常見的鎖機制,自旋鎖是原地等待的方式解決資源沖突的补胚,即码耐,一個線程獲取了一個自旋鎖后,另外一個線程期望獲取該自旋鎖溶其,獲取不到骚腥,只能夠原地打轉(忙等待)。由于自旋鎖的這個忙等待的特性瓶逃,注定了它使用場景上的限制 —— 自旋鎖不應該被長時間的持有(消耗CPU
資源)束铭。
糾正一下
atomic只是原子屬性的一個標識符,所以atomic并不是自旋鎖厢绝,底層是通過Spinlock實現自旋鎖契沫。
線程和Runloop的關系
-
runloop
與線程是一一對應的,一個runloop
對應一個核心的線程代芜,為什么說是核心的埠褪,是因為runloop
是可以嵌套的,但是核心的只能有一個,他們的關系保存在一個全局
的字典里钞速。 -
runloop
是來管理線程的贷掖,當線程的runloop
被開啟后,線程會在執(zhí)行完任務后進入休
眠狀態(tài)渴语,有了任務就會被喚醒去執(zhí)行任務苹威。 -
runloop
在第一次獲取時被創(chuàng)建,在線程結束時被銷毀驾凶。 - 對于主線程來說牙甫,
runloop
在程序一啟動就默認創(chuàng)建好了。 - 對于子線程來說调违,
runloop
是懶加載的窟哺,只有當我們使用的時候才會創(chuàng)建,所以在子線程用定時器要注意
:確保子線程的runloop
被創(chuàng)建技肩,不然定時器不會回調且轨。
4. iOS技術方案
iOS技術方案如下圖:多線程有Pthread、NSThread虚婿、GCD旋奢、NSOperation 等方案。
關于多線程的更多信息然痊,可以去蘋果文檔去看看
Threading Programming Guide
更多內容持續(xù)更新
?? 喜歡就點個贊吧????
?? 覺得有收獲的至朗,可以來一波,收藏+關注剧浸,評論 + 轉發(fā)锹引,以免你下次找不到我????
?? 歡迎大家留言交流,批評指正辛蚊,互相學習??粤蝎,提升自我??
?? 作者郵件:zhangxmsy@163.com 有問題聯系。