一、進程、線程及關系
一、什么是進程?
進程是具有一定獨立功能程序關于某次數(shù)據集合的一次運行活動,操作系統(tǒng)分配資源的基本單元也是最小單位。
進程是指在系統(tǒng)中正在運行的一個應用程序,就是一段程序的執(zhí)行過程,我們可以理解為手機上的一個app就是一個進程。
每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內,擁有獨立運行所需的全部資源。
二、什么是線程?
線程是CPU調度的最小單位。
一個進程要想執(zhí)行任務,必須至少有一條線程.應用程序啟動的時候,系統(tǒng)會默認開啟一條線程,也就是主線程
三、進程和線程的關系
進程和線程的關系,可以簡單的形象的做個比喻:進程=火車,線程=車廂
線程須在進程下才能運行的(單純的車廂無法運行)
一個進程可以包含多個線程(一輛火車可以有多個車廂)
不同進程間的數(shù)據很難共享(一輛火車上的乘客很難換到另外一輛火車,比如站點換乘)
同一進程下不同線程間數(shù)據很易共享(A車廂換到B車廂很容易)
進程要比線程消耗更多的計算機資源(采用多列火車相比多個車廂更耗資源)
進程間不會相互影響,一個線程掛掉將導致整個進程掛掉(一列火車不會影響到另外一列火車,但是如果一列火車中一節(jié)車廂著火了,將影響到所有車廂)
進程可以拓展到多機,線程最多適合多核不同火車(可以開在多個軌道上,同一火車的車廂不能在行進的不同的軌道上)
進程使用的內存地址可以上鎖,即一個線程使用某些共享內存時,其他線程必須等它結束,才能使用這一塊內存。(比如火車上的洗手間)-"互斥鎖"
進程使用的內存地址可以限定使用量(比如火車上的餐廳,最多只允許多少人進入,如果滿了需要在門口等,等有人出來了才能進去)-“信號量”
二、任務、隊列及執(zhí)行任務的方式
一、任務
- 任務就是要執(zhí)行的操作,也就是你在線程中執(zhí)行的那段代碼。在 GCD 中是放在 block 中的。
二、隊列
一、隊列有兩種:
-
串行隊列:
- 每次只有一個任務被執(zhí)行。讓任務一個接著一個地執(zhí)行。(只開啟一個線程,一個任務執(zhí)行完畢后,再執(zhí)行下一個任務)
-
并行隊列:
- 可以讓多個任務并發(fā)(同時)執(zhí)行。(可以開啟多個線程,并且同時執(zhí)行任務)
二、執(zhí)行隊列的方式
-
同步執(zhí)行(sync):
- 同步添加任務到指定的隊列中,在添加的任務執(zhí)行結束之前,會一直等待,直到隊列里面的任務完成之后再繼續(xù)執(zhí)行。
- 只能在當前線程中執(zhí)行任務,不具備開啟新線程的能力。
異步執(zhí)行(async):
異步添加任務到指定的隊列中,它不會造成主線程的等待,可以繼續(xù)執(zhí)行任務.
可以在新的線程中執(zhí)行任務,具備開啟新線程的能力。
隊列和任務的關系
任務和隊列的關系可以打一個形象的比喻,比如小時后玩的四驅車,隊列=跑道,串行隊列=一條跑道,并行隊列=多條跑道,四驅車=任務)。
基本規(guī)則就是每條跑道上只能有一輛車在上面跑。串行隊列由于只有一條跑道,所以每次只能跑一輛車(一個任務),等這輛車跑完,別的車(任務)才能跑。并發(fā)隊列由于有多個跑道,所以可以供多輛車(多個任務)一起跑。
執(zhí)行隊列的方式就相當于我們如何把車(任務)放到跑道(隊列)里
-
同步執(zhí)行不開啟新線程:
就相當于我們只有主線程一只手,每次只能把一輛車(任務)放到跑道上跑,等車跑完,把車收了以后,才能把下一輛車(任務)放到跑道上跑
所以不管我們是把車(任務)放到單條跑道(串行隊列)還是把車(任務)放到多條跑道(并發(fā)隊列),每次都只能控制一輛車。并且由于主線程在玩車(執(zhí)行任務),也就干不了別的事,所以同步執(zhí)行會造成主線程等待。
-
異步執(zhí)行可以開啟新線程:
就相當于我們除了有主線程這只手外還邀請了很多一起玩的小伙伴(分線程),我們每個人都能拿起一輛車(任務)放到對應的跑道(隊列)上,然后一起跑。所以在多條跑道(并發(fā)隊列)上多輛車(多個任務)可以一起跑。
如果車(任務)很多,跑道只有一個(串行隊列),那么還是得排隊玩,每次只能跑一輛車(任務)。但是,由于邀請了小伙伴(開啟了線程),主線程這只手就可以讓小伙伴(分線程)先玩車,自己去處理別的事情。所以異步執(zhí)行不會造成主線程等待。
四、多線程引起的死鎖
死鎖就是隊列引起的循環(huán)等待。
產生死鎖的情況:
- 一個比較常見的死鎖例子:主隊列同步
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
}
在主線程中運用主隊列同步,也就是把任務放到了主線程的隊列中。
同步對于任務是立刻執(zhí)行的,那么當把任務放進主隊列時,它就會立馬執(zhí)行,只有執(zhí)行完這個任務,viewDidLoad才會繼續(xù)向下執(zhí)行。
viewDidLoad和任務都是在主隊列上的,由于隊列的先進先出原則,任務又需等待viewDidLoad執(zhí)行完畢后才能繼續(xù)執(zhí)行,viewDidLoad和這個任務就形成了相互循環(huán)等待,就造成了死鎖。
想避免這種死鎖,可以將同步改成異步dispatch_async,或者將dispatch_get_main_queue換成其他串行或并行隊列,都可以解決。
- 二、在同串行隊列下異步任務里調用同步任務
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});
外面的函數(shù)無論是同步還是異步都會造成死鎖。
這是因為里面的任務和外面的任務都在同一個serialQueue隊列內,又是同步,這就和上邊主隊列同步的例子一樣造成了死鎖
解決方法也和上邊一樣,將里面的同步改成異步dispatch_async,或者將serialQueue換成其他串行或并行隊列,都可以解決
二、在不同的串行隊列,異步block調用同步不會引起死鎖
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue2, ^{
NSLog(@"serialQueue2");
});
NSLog(@"serialQueue");
});
serialQueue、serialQueue2是兩個不同隊列,所以不存在隊列引起的循環(huán)等待。
這樣是不會死鎖的,并且serialQueue和serialQueue2是在同一個線程中的。
打印結果:
serialQueue2
serialQueue
五、GCD任務執(zhí)行順序
- 1、串行隊列先異步后同步
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(serialQueue, ^{
NSLog(@"4");
});
NSLog(@"5");
代碼打印結果為:1 3 2 4 5
首先先打印1,
接下來將任務2其添加至串行隊列上,由于任務2是異步,不會阻塞線程,繼續(xù)向下執(zhí)行,打印3
然后是任務4,將任務4添加到串行隊列上,因為任務4和任務2在同一串行隊列,根據隊列先進先出原則,任務4必須等任務2執(zhí)行后才能執(zhí)行。所以先打印2
又因為任務4是同步任務,會阻塞線程,所以再打印4.
最后打印5
- 2、performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
});
這里的test方法是不會去執(zhí)行的,原因在于
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument
afterDelay:(NSTimeInterval)delay;
這個方法要創(chuàng)建提交任務到runloop上的。
gcd底層創(chuàng)建的子線程是默認沒有開啟對應runloop的,所有這個方法就會失效。
解決方法:
將dispatch_get_global_queue改成主隊列,由于主隊列所在的主線程是默認開啟了runloop的,就會去執(zhí)行。
或者將dispatch_async改成同步,因為同步是在當前線程執(zhí)行,那么如果當前線程是主線程,test方法也是會去執(zhí)行的,如果不是就還是不執(zhí)行)。
在就是開啟子線程的run,在perfomselector下開發(fā)runloop就行代碼如下:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
[[NSRunLoop currentRunLoop] run];
});
開啟子線程的runloop就會執(zhí)行了。
六、自旋鎖與互斥鎖
一、自旋鎖
自旋鎖是一種用于保護多線程共享資源的鎖,與一般互斥鎖(mutex)不同之處在于當自旋鎖嘗試獲取鎖時以忙等待(busy waiting)的形式不斷地循環(huán)檢查鎖是否可用。
當上一個線程的任務沒有執(zhí)行完畢的時候(被鎖住),那么下一個線程會一直等待(不會睡眠),當上一個線程的任務執(zhí)行完畢,下一個線程會立即執(zhí)行。
使用場景:對持有鎖較短的程序
在多CPU的環(huán)境中,對持有鎖較短的程序來說,使用自旋鎖代替一般的互斥鎖往往能夠提高程序的性能。
自旋鎖會忙等: 所謂忙等,即在訪問被鎖資源時,調用者線程不會休眠,而是不停循環(huán)在那里,直到被鎖資源釋放鎖。
二、互斥鎖
互斥鎖:當上一個線程的任務沒有執(zhí)行完畢的時候(被鎖?。?,那么下一個線程會進入睡眠狀態(tài)等待任務執(zhí)行完畢。
當上一個線程的任務執(zhí)行完畢,下一個線程會自動喚醒然后執(zhí)行任務。
互斥鎖會休眠: 所謂休眠,即在訪問被鎖資源時,調用者線程會休眠,此時cpu可以調度其他線程工作。直到被鎖資源釋放鎖。此時會喚醒休眠線程。
三、各自優(yōu)缺點:
自旋鎖優(yōu)點:
自旋鎖的優(yōu)點在于,因為自旋鎖不會引起調用者睡眠,所以不會進行線程調度,CPU時間片輪轉等耗時操作。
所有如果能在很短的時間內獲得鎖,自旋鎖的效率遠高于互斥鎖。
自旋鎖缺點:
- 自旋鎖一直占用CPU,他在未獲得鎖的情況下,一直運行--自旋,所以占用著CPU,如果不能在很短的時 間內獲得鎖,這無疑會使CPU效率降低。
- 自旋鎖不能實現(xiàn)遞歸調用。
自旋鎖:
- atomic
- OSSpinLock
- dispatch_semaphore_t 等
互斥鎖如:
- pthread_mutex
- @ synchronized
- NSLock、NSConditionLock
- NSCondition
- NSRecursiveLock等
六、NSThread+runloop實現(xiàn)常駐線程
一、為什么去實現(xiàn)常駐線程,常駐線程使用背景?
由于每次開辟子線程都會消耗cpu,在需要頻繁使用子線程的情況下,頻繁開辟子線程會消耗大量的cpu,而且創(chuàng)建線程都是任務執(zhí)行完成之后也就釋放了,不能再次利用。
那么如何創(chuàng)建一個線程可以讓它可以再次工作呢?也就是創(chuàng)建一個常駐線程。
一、實現(xiàn)常駐線程的步驟
- 首先常駐線程既然是常駐,那么可以用GCD實現(xiàn)一個單例來保存NSThread 代碼如下:
+ (NSThread *)shareThread {
static NSThread *_shareThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
[_shareThread setName:@"threadTest"];
[_shareThread start];
});
return _shareThread;
}
這樣創(chuàng)建的thread就不會銷毀了嗎?
[self performSelector:@selector(test) onThread:[ViewController shareThread] withObject:nil waitUntilDone:NO];
- (void)test {
NSLog(@"test:%@", [NSThread currentThread]);
}
執(zhí)行結果并沒有打印,說明test方法沒有被調用。
當創(chuàng)建的NSThread沒有開啟runloop的話,常駐線是不會有效的。
因為新建的子線程默認沒有開啟runloop
因此需要給這個線程添加了一個runloop,并且加了一個NSMachPort端口監(jiān)聽,防止新建的線程由于沒有活動直接退出。
- 其次給線程開啟runloop。
需要給這個線程添加了一個runloop,并且加了一個NSMachPort端口監(jiān)聽,防止新建的線程由于沒有活動直接退出。。
- (void)threadTest
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
二、常駐線程的使用
performSelector是封裝在NSObject的NSThreadPerformAdditions類別里,只要是NSObject直接調用
[self performSelector: onThread: withObject: waitUntilDone:]
三、常駐線程退出
只有從runloop中移除我們之前添加的端口,這樣runloop沒有任何事件,所以直接退出。方法如下:
[NSRunLoop currentRunLoop]removePort:<#(nonnull NSPort *)#> forMode:<#(nonnull NSRunLoopMode)#>