回顧
在上一篇博客中,我們已經(jīng)對進(jìn)程和線程有了一定的了解了列粪,那么本次博客將繼續(xù)講解宵晚!
1. 線程的生命周期
在程序開發(fā)中有個名詞——生命周期
寻定,我們都知道APP
有生命周期襟衰,那么線程的生命周期是什么樣子的呢?
- 線程生命周期
線程生命周期大致包括
5
個階段:
新建
:通過創(chuàng)建線程的函數(shù)方法逊躁,創(chuàng)建一個新的線程谢翎。就緒
:線程創(chuàng)建完成之后,調(diào)用start
方法走趋,線程這個時候處于等待狀態(tài)衅金,等待CPU
時間分配執(zhí)行。運(yùn)行
:當(dāng)就緒的線程被調(diào)度并獲得CPU
資源時吆视,便進(jìn)入運(yùn)行狀態(tài)典挑,run
方法定義了線程的操作和功能。阻塞
:在運(yùn)行狀態(tài)的時候啦吧,可能因為某些原因?qū)е逻\(yùn)行狀態(tài)的線程變成了阻塞
狀態(tài)您觉,比如sleep
、等待同步鎖
授滓,線程就從可調(diào)度線程池
移出琳水,處于了阻塞
狀態(tài),這個時候sleep
到時般堆、獲取同步鎖
在孝,此時會重新添加到可調(diào)度線程池
。喚醒的線程不會立刻執(zhí)行run
方法淮摔,它們要再次等待CPU
分配資源進(jìn)入運(yùn)行狀態(tài)私沮。銷毀
:如果線程正常執(zhí)行完畢后或線程被提前強(qiáng)制性的終止或出現(xiàn)異常導(dǎo)致結(jié)束,那么線程就要被銷毀和橙,釋放資源仔燕。線程生命周期大致流程圖如下:
- 線程狀態(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);
//可以設(shè)置彈框 ---> 這里直接制空
[self.p_thread cancel];
self.p_thread = nil;
}
}
2. 線程池的運(yùn)行策略
線程池運(yùn)行策略
線程的工作執(zhí)行,也是有一定的策略的魔招,線程池的運(yùn)行策略見下圖:
隊列滿了且正在運(yùn)行的線程數(shù)量晰搀,小于最大線程數(shù),則新進(jìn)來的任務(wù)办斑,會直接創(chuàng)建非核心線程來完成工作外恕。
- 線程池剛創(chuàng)建時,里面沒有一個線程。任務(wù)隊列是作為參數(shù)傳進(jìn)來的鳞疲。不過罪郊,就算隊列里面有任務(wù),線程池也不會馬上執(zhí)行它們建丧。
當(dāng)有任務(wù)時排龄,線程池會做如下判斷:
- 如果正在運(yùn)行的線程數(shù)量小于
corePoolSize
(核心線程數(shù)),那么馬上創(chuàng)建核心線程
運(yùn)行這個任務(wù)翎朱。
如果正在運(yùn)行的線程數(shù)量大于或等于corePoolSize
橄维,那么將這個任務(wù)放入隊列。 - 如果這時候隊列滿了拴曲,而且正在運(yùn)行的線程數(shù)量小于
maximumPoolSize
(最大線程數(shù))争舞,那么還是要創(chuàng)建非核心線程立刻運(yùn)行這個任務(wù)。 - 如果隊列滿了澈灼,而且正在運(yùn)行的線程數(shù)量大于或等于
maximumPoolSize
竞川,那么線程池飽和策略
將進(jìn)行處理。
- 當(dāng)一個線程完成任務(wù)時叁熔,它會從隊列中取下一個任務(wù)來執(zhí)行委乌。
- 當(dāng)一個線程無事可做,超過一定的時間(超時)時荣回,線程池會判斷遭贸,如果當(dāng)前運(yùn)行的線程數(shù)大于
corePoolSize
,那么這個線程就被停掉心软。所以線程池的所有任務(wù)完成后壕吹,它最終會收縮到corePoolSize
的大小。
飽和策略
如果線程池中的隊列滿了删铃,并且正在運(yùn)行的線程數(shù)量已經(jīng)大于等于當(dāng)前線程池的最大線程數(shù)耳贬,則進(jìn)行飽和策略的處理。
-
AbortPolicy
直接拋出RejectedExecutionExeception
異常來阻?系統(tǒng)正常運(yùn)? -
CallerRunsPolicy
將任務(wù)回退到調(diào)?者 -
DisOldestPolicy
丟掉等待最久的任務(wù) -
DisCardPolicy
直接丟棄任務(wù)
3. 自旋鎖和互斥鎖
任務(wù)的執(zhí)行速度的影響因素:
CPU
- 任務(wù)的復(fù)雜度
- 任務(wù)的優(yōu)先級
- 線程的狀態(tài)
優(yōu)先級翻轉(zhuǎn):
-
IO
密集型(頻繁的等待線程) -
CPU
密集型(很少等待) -
IO
比CPU
更容易得到優(yōu)先級的提升 - 餓死:一直等不到執(zhí)行猎唁,就丟棄了
- 調(diào)度:優(yōu)先級和
CPU
的調(diào)度還有關(guān)系
優(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 - 在應(yīng)用程序中咒劲,收集錯誤日志,能夠記錄工作的線程诫隅!
// 否則不好判斷具體哪一個線程出的問題缎患!
t.name = @"吃飯線程";
//This value must be in bytes and a multiple of 4KB.
t.stackSize = 1024*1024;
t.threadPriority = 1;
[t start];
threadPriority
threadPriority
替換成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)先級)
下圖是:一個線程的經(jīng)典案例
車票售賣系統(tǒng),是線程工作執(zhí)行的經(jīng)典案例阎肝,如果是多個窗口賣票,會出現(xiàn)資源搶奪的情況肮街,如果 A窗口賣了一張票风题,B窗口不知道,或者同一時間 AB窗口賣同一張票,這樣就會出現(xiàn)問題沛硅,所有鎖的意義重大了眼刃。
自旋鎖
是一種用于保護(hù)多線程共享資源
的鎖,與一般互斥鎖
(mutex
)不同之處在于當(dāng)自旋鎖
嘗試獲取鎖時摇肌,以忙等待(busy waiting
)的形式不斷地循環(huán)檢查鎖是否可用擂红;當(dāng)上一個線程的任務(wù)沒有執(zhí)行完畢的時候(被鎖住
),那么下一個線程會一直等待
(不會睡眠)围小;當(dāng)上一個線程的任務(wù)執(zhí)行完畢昵骤,下一個線程會立即執(zhí)行。
在多CPU
的環(huán)境中肯适,對持有鎖較短的程序來說变秦,使用自旋鎖代替一般的互斥鎖往往能夠提高程序的性能。
自旋鎖
:OSSpinLock框舔、dispatch_semaphore_t
互斥鎖
當(dāng)上一個線程的任務(wù)沒有執(zhí)行完畢的時候(被鎖住
)蹦玫,那么下一個線程會進(jìn)入睡眠狀態(tài)
等待任務(wù)執(zhí)行完畢,當(dāng)上一個線程的任務(wù)執(zhí)行完畢刘绣,下一個線程會自動喚醒然后執(zhí)行任務(wù)樱溉,該任務(wù)也不會立刻執(zhí)行,而是成為可執(zhí)行狀態(tài)(就緒
)纬凤。
互斥鎖
:pthread_mutex福贞、@ synchronized、NSLock移斩、NSConditionLock肚医、NSCondition、NSRecursiveLock
自旋鎖和互斥鎖的特點
自旋鎖
會忙等向瓷,所謂忙等肠套,即在訪問被鎖資源時,調(diào)用者線程不會休眠猖任,而是不停循環(huán)在那里你稚,直到被鎖資源釋放鎖。互斥鎖
會休眠朱躺,所謂休眠刁赖,即在訪問被鎖資源時,調(diào)用者線程會休眠长搀,此時cpu可以調(diào)度其他線程工作宇弛,直到被鎖資源釋放鎖。此時會喚醒休眠線程源请。
自旋鎖優(yōu)缺點
- 優(yōu)點在于枪芒,因為自旋鎖不會引起調(diào)用者睡眠彻况,所以不會進(jìn)行線程調(diào)度,CPU時間片輪轉(zhuǎn)等耗時操作舅踪。所有如果能在很短的時間內(nèi)獲得鎖纽甘,自旋鎖的效率遠(yuǎn)高于互斥鎖。
- 缺點在于抽碌,自旋鎖一直占用CPU悍赢,他在未獲得鎖的情況下,一直運(yùn)行自旋货徙,所以占用著CPU左权,如果不能在很短的時間內(nèi)獲得鎖,這無疑會使CPU效率降低破婆。自旋鎖不能實現(xiàn)遞歸調(diào)用涮总。
原子屬性和非原子屬性
OC
在定義屬性時有nonatomic
和atomic
兩種選擇,默認(rèn)為atomic
屬性
-
atomic
:原子屬性祷舀,為setter
方法加自旋鎖(即為單寫多讀) -
nonatomic
:非原子屬性瀑梗,不會為setter方法加鎖
nonatomic
和atomic
的對比
-
atomic
:線程安全,需要消耗大量的資源裳扯; -
nonatomic
:非線程安全抛丽,適合內(nèi)存小的移動設(shè)備。
平時開發(fā)需要注意
- 如非需搶占資源的屬性(如購票饰豺,充值)亿鲜,所有屬性都聲明為
nonatomic
。 - 盡量避免多線程
搶奪
同一塊資源冤吨。 - 盡量將
加鎖
蒿柳、資源搶奪
的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動客戶端的壓力漩蟆。
atomic
底層實現(xiàn)自旋鎖
我們在探索類的本質(zhì)時垒探,對于類的屬性的setter
方法,系統(tǒng)會有一層objc_setProperty
的封裝(libobjc.dylib源碼)
底層會調(diào)用
reallySetProperty
方法怠李,在該方法的實現(xiàn)中圾叼,針對原子屬性,添加了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
內(nèi)核中提供的一種比較常見的鎖機(jī)制捺癞,自旋鎖是原地等待的方式解決資源沖突的夷蚊,即,一個線程獲取了一個自旋鎖后髓介,另外一個線程期望獲取該自旋鎖惕鼓,獲取不到,只能夠原地打轉(zhuǎn)(忙等待)唐础。由于自旋鎖的這個忙等待的特性呜笑,注定了它使用場景上的限制 —— 自旋鎖不應(yīng)該被長時間的持有(消耗CPU
資源)夫否。
糾正一下
atomic
只是原子屬性的一個標(biāo)識符,所以atomic
并不是自旋鎖叫胁,底層是通過Spinlock
實現(xiàn)自旋鎖。
線程和Runloop的關(guān)系
-
runloop
與線程是一一對應(yīng)的汞幢,一個runloop
對應(yīng)一個核心的線程驼鹅,為什么說是核心的,是因為runloop
是可以嵌套的森篷,但是核心的只能有一個输钩,他們的關(guān)系保存在一個全局
的字典里。 -
runloop
是來管理線程的仲智,當(dāng)線程的runloop
被開啟后买乃,線程會在執(zhí)行完任務(wù)后進(jìn)入休
眠狀態(tài),有了任務(wù)就會被喚醒去執(zhí)行任務(wù)钓辆。 -
runloop
在第一次獲取時被創(chuàng)建剪验,在線程結(jié)束時被銷毀。 - 對于主線程來說前联,
runloop
在程序一啟動就默認(rèn)創(chuàng)建好了功戚。 - 對于子線程來說,
runloop
是懶加載的似嗤,只有當(dāng)我們使用的時候才會創(chuàng)建啸臀,所以在子線程用定時器要注意
:確保子線程的runloop
被創(chuàng)建,不然定時器不會回調(diào)烁落。
4. iOS技術(shù)方案
多線程有
Pthread
乘粒、NSThread
、GCD
伤塌、NSOperation
等方案灯萍。
iOS技術(shù)方案如下圖:
關(guān)于多線程的更多信息,可以去蘋果文檔去看看
Threading Programming Guide
更多內(nèi)容持續(xù)更新
?? 喜歡就點個贊吧????
?? 覺得有收獲的寸谜,可以來一波竟稳,收藏+關(guān)注,評論 + 轉(zhuǎn)發(fā)熊痴,以免你下次找不到我????
??歡迎大家留言交流他爸,批評指正,互相學(xué)習(xí)??果善,提升自我??