iOS -- 淺談多線程原理

進程與線程

如果把進程比作是一個電子廠猫胁,那么線程就是一條條的流水作業(yè)線名眉。電子廠與電子廠之間相互獨立,當前電子廠的作業(yè)流水線只能使用自己電子廠資源。

進程

  • 進程是指在系統(tǒng)中正在運行的一個應用程序鸣戴,比如打開的Xcode
  • 每個進程之間是獨立的粘拾,每個進程運行在專有的而且受保護的內(nèi)存空間中窄锅。

線程

  • 線程是進程的基本執(zhí)行單元,一個進程的所有任務都在線程中執(zhí)行缰雇。
  • 進程想要執(zhí)行任務入偷,必須要有線程,進程至少要有一條線程用來執(zhí)行任務械哟。
  • 程序啟動時會默認開啟一條線程疏之,這條線程被稱為主線程或者UI線程。

進程與線程的關系

  1. 線程是進程的執(zhí)行單元戒良,進程的所有任務都在線程中執(zhí)行体捏,同一個進程內(nèi)的線程共享進程資源。
  2. 地址空間:同一進程的線程共享本進程的地址空間糯崎,而進程之間則是獨立的地址空間几缭。
  3. 資源擁有:同一進程內(nèi)的線程共享本進程的資源如內(nèi)存、I/O沃呢、cpu等年栓,但是進程之間的 資源是獨立的。
  4. 一個進程崩潰后薄霜,在保護模式下不會對其他進程產(chǎn)生影響某抓,但是一個線程崩潰整個進 程都死掉。所以多進程要比多線程健壯惰瓜。

作為一個開發(fā)者否副,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:834688868崎坊,不管你是大牛還是小白都歡迎入駐 备禀,分享BAT,阿里面試題、面試經(jīng)驗奈揍,討論技術曲尸, 大家一起交流學習成長!

如果你正在面試男翰,或者正準備跳槽另患,不妨看看我精心總結的面試資料: BAT 大廠最新面試題+答案合集(持續(xù)更新中) 來獲取一份詳細的大廠面試資料 為你的跳槽加薪多一份保障

  1. 進程切換時,消耗的資源大蛾绎,效率高昆箕。所以涉及到頻繁的切換時鸦列,使用線程要好于進程。同樣如果要求同時進行并且又要共享某些變量的并發(fā)操作为严,只能用線程不能用進程敛熬。
  2. 執(zhí)行過程:每個獨立的進程有一個程序運行的入口、順序執(zhí)行序列和程序入口第股。但是 線程不能獨立執(zhí)行应民,必須依存在應用程序中,由應用程序提供多個線程執(zhí)行控制夕吻。
  3. 線程是處理器調(diào)度的基本單位诲锹,但是進程不是。
  4. 線程沒有地址空間,線程包含在進程地址空間中涉馅。

多線程

多線程原理

我們知道一個進程可以開啟多個線程归园,進程的所有任務都在線程中執(zhí)行,而一個線程中的任務是串行的稚矿,如果要在一個線程中執(zhí)行多個任務庸诱,那么只能一個一個地按順序執(zhí)行這些任務,也就是說晤揣,在同一時間內(nèi)桥爽,一個線程只能執(zhí)行一個任務,而在同一時刻昧识,一個CPU只能處理一條線程(只有一個線程在執(zhí)行任務)钠四,但CPU可以在多條線程之間快速的切換,只要切換的足夠快跪楞,就造成了多線程一同執(zhí)行的假象缀去。

多線程的優(yōu)缺點

優(yōu)點

  • 能適當提高程序的執(zhí)行效率
  • 能適當提高資源的利用率(CPU,內(nèi)存)
  • 線程上的任務執(zhí)行完成后甸祭,線程會自動銷毀

缺點

  • 開啟線程需要占用一定的內(nèi)存空間(默認情況下缕碎,主線程占用1 MB,子線程都占用512 KB)
  • 如果開啟大量的線程池户,會占用大量的內(nèi)存空間咏雌,降低程序的性能
  • 線程越多,CPU在調(diào)用線程上的開銷就越大
  • 程序設計更加復雜煞檩,比如線程間的通信处嫌、多線程的數(shù)據(jù)共享

那么提出一個疑問?如果進程開啟的線程非常非常多栅贴,會發(fā)生什么情況?

答:CPU會在許多線程之間調(diào)度斟湃,CPU會累死,會消耗大量的CPU資源, 而且每條線程被調(diào)度執(zhí)行的頻次會降低(線程的執(zhí)行效率也就降低)

主線程(UI線程)

一個iOS程序運行后檐薯,默認會開啟一條線程凝赛,稱為主線程UI線程.主線程主要用于顯示刷新UI界面注暗,處理UI事件。(最好不要將耗時任務放在主線程處理墓猎,耗時操作會卡住主線程捆昏,造成一種卡頓現(xiàn)象。)

線程的生命周期

image.png
  • 新建:實例化線程對象
  • 就緒:向線程對象發(fā)送start消息毙沾,線程對象并不會立即執(zhí)行骗卜,線程對象被加入可調(diào)度線程池等待CPU調(diào)度。
  • 運行:CPU 負責調(diào)度可調(diào)度線程池中線程的執(zhí)行左胞。線程執(zhí)行完成之前寇仓,狀態(tài)可能會在就緒和運行之間來回切換。就緒和運行之間的狀態(tài)變化由CPU負責烤宙,程序員不能干預遍烦。
  • 阻塞:當滿足某個預定條件時,可以使用休眠或鎖躺枕,阻塞線程執(zhí)行服猪。當進入休眠時,會重新將線程加入就緒狀態(tài)中拐云。休眠的時間設置參數(shù)為:sleepForTimeInterval(休眠指定時長)罢猪,sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥鎖)慨丐。
  • 死亡:正常死亡坡脐,線程執(zhí)行完畢。非正常死亡房揭,當滿足某個條件后备闲,在線程內(nèi)部中止執(zhí)行(或者在主線程中止線程對象)

關于線程的exitcancel

[NSThread exit]:一旦強行終止線程,后續(xù)的所有代碼都不會執(zhí)行

[thread cancel]:并不會直接取消正在執(zhí)行的線程捅暴,只是給線程對象添加 isCancelled 標記

線程的優(yōu)先級

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));
復制代碼

上述優(yōu)先級從高到低恬砂,但是,線程執(zhí)行的快慢蓬痒,除了看線程的優(yōu)先級泻骤,還需要查看執(zhí)行任務資源的大小(即任務的復雜度)梧奢、以及 CPU調(diào)度情況狱掂。

線程池

image.png

線程安全

當多個線程訪問同一塊資源時,很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題亲轨。就好像售票系統(tǒng)趋惨,如果多人同時在售票,每個人的售票處理的速度不一樣惦蚊,那么就會造成余票的數(shù)量飄忽不定器虾。 那么解決多線程安全問題有兩種方法:互斥鎖和自旋鎖讯嫂。

互斥鎖和自旋鎖

互斥鎖(同步鎖)@synchronized

@synchronized(鎖對象) {
    // 需要鎖定的代碼
}
復制代碼
  • 用于保護臨界區(qū),保證鎖內(nèi)的代碼,同一時間兆沙,只有一條線程能夠執(zhí)行欧芽。
  • 判斷的時候鎖對象要存在,如果代碼中只有一個地方需要加鎖葛圃,大多都使用self作為鎖對象千扔,這樣可以避免單獨再創(chuàng)建一個鎖對象。
  • 加了互斥鎖的代碼库正,當有新的線程訪問時昏鹃,如果發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼,新線程就會進入休眠诀诊。
  • 鎖對象一定要保證所有的線程都能夠訪問洞渤。
  • 互斥鎖的鎖定范圍,應該盡量小属瓣,鎖定范圍越大载迄,效率越差。

自旋鎖

自旋鎖不同于互斥鎖通過線程休眠來達到阻塞抡蛙,自旋鎖是線程在獲取鎖對象之前护昧,一直處于忙等詢問的阻塞狀態(tài)。

加了自旋鎖粗截,當新線程訪問代碼時惋耙,如果發(fā)現(xiàn)有其他線程正在鎖定代碼,新線程會用死循環(huán)的方式熊昌,一直等待鎖定的代碼執(zhí)行完成绽榛。相當于不停嘗試執(zhí)行代碼,比較消耗性能婿屹。其中灭美,屬性修飾符atomic,本身就有一把自旋鎖(atomic又稱為原子鎖)昂利。

atomicnonatomic

atomic 原子屬性,是默認屬性届腐,是線程安全的,保證同一時間只有一個線程能夠寫入蜂奸,但是同一個時間多個線程都可以取值犁苏。使用其需要消耗大量的資源。

nonatomic 非原子屬性,是非線程安全的扩所,同一時間可以有很多線程讀和寫围详。相比atomic效率更高。

iOS開發(fā)的過程中碌奉,建議將所有屬性都聲明為nonatomic短曾,開發(fā)過程中盡量避免多線程搶奪同一資源,將資源的業(yè)務邏輯交由服務端完成赐劣。

線程之間的通信

在蘋果的文檔Threading Programming Guide文檔的Table 1-3 Communication mechanisms部分嫉拐,有提到關于線程之間通信的方式。

截屏2021-06-18 下午2.16.46.png

簡單用代碼介紹一下常用的通信方式:

  1. 直接消息: 通過performSelector的一系列方法
//異步下載圖像
[self performSelectorInBackground:@selector(downloadImageWithURL:) withObject:url];

- (void)downloadImageWithURL:(NSURL *)url {
    // 1\. 獲取二進制數(shù)據(jù)
    NSData *data = [NSData dataWithContentsOfURL:url];

    // 2\. 將二進制數(shù)據(jù)轉換成 image
    UIImage *image = [UIImage imageWithData:data];

    // 3\. 在主線程更新 UI
    // waitUntilDone: 是否等待 updateImage: 執(zhí)行完成
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:YES];
    NSLog(@"完成");
}
復制代碼
  1. 端口通信
ZhModel.h

@interface ZhModel : NSObject
- (void)modelLaunchThreadWithPort:(NSPort *)port;
@end

ZhModel.m

#import "ZhModel.h"
@interface ZhModel()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end

@implementation ZhModel
- (void)modelLaunchThreadWithPort:(NSPort *)port{

    NSLog(@"VC 響應了Model里面");
    @autoreleasepool {
        //1\. 保存主線程傳入的port
        self.vcPort = port;
        //2\. 設置子線程名字
        [[NSThread currentThread] setName:@"ZhModelThread"];
        //3\. 開啟runloop
        [[NSRunLoop currentRunLoop] run];
        //4\. 創(chuàng)建自己port
        self.myPort = [NSMachPort port];
        //5\. 設置port的代理回調(diào)對象
        self.myPort.delegate = self;
        //6\. 完成向主線程port發(fā)送消息
        [self sendPortMessage];
    }
}
//   完成向主線程發(fā)送port消息
- (void)sendPortMessage {

    NSData *data1 = [@"ZhModel" dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
    // 發(fā)送消息到VC的主線程
    // 第一個參數(shù):發(fā)送時間魁兼。
    // msgid 消息標識婉徘。
    // components,發(fā)送消息附帶參數(shù)咐汞。
    // reserved:為頭部預留的字節(jié)數(shù)
    [self.vcPort sendBeforeDate:[NSDate date]
                          msgid:10086
                     components:array
                           from:self.myPort
                       reserved:0];

}

#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
    NSLog(@"model:handlePortMessage  == %@",[NSThread currentThread]);
    NSLog(@"從VC 傳過來一些信息:");
    NSLog(@"components == %@",[message valueForKey:@"components"]);
    NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
    NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
    NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
}
@end
復制代碼
PortViewController.m

#import "PortViewController.h"
#import <objc/runtime.h>
#import "ZhModel.h"

@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) ZhModel *zhmodel;

@end

@implementation PortViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //1\. 創(chuàng)建主線程的port
    // 子線程通過此端口發(fā)送消息給主線程
    self.myPort = [NSMachPort port];
    //2\. 設置port的代理回調(diào)對象
    self.myPort.delegate = self;
    //3\. 把port加入runloop盖呼,接收port消息
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];

    self.zhmodel = [[ZhModel alloc] init];
    [NSThread detachNewThreadSelector:@selector(modelLaunchThreadWithPort:)
                             toTarget:self.zhmodel
                           withObject:self.myPort];

}

#pragma mark - NSMachPortDelegate

- (void)handlePortMessage:(NSPortMessage *)message{

    NSLog(@"VC == %@",[NSThread currentThread]);
    NSLog(@"從person 傳過來一些信息:");
    NSArray *messageArr = [message valueForKey:@"components"];
    NSString *dataStr   = [[NSString alloc] initWithData:messageArr.firstObject  encoding:NSUTF8StringEncoding];
    NSLog(@"傳過來一些信息 :%@",dataStr);
    NSPort  *destinPort = [message valueForKey:@"remotePort"];
    if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
        NSLog(@"傳過來的數(shù)據(jù)有誤");
        return;
    }

    NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];

    // 非常重要,如果你想在Person的port接受信息,必須加入到當前主線程的runloop
    [[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
    NSLog(@"Thread == %@",[NSThread currentThread]);
    BOOL success = [destinPort sendBeforeDate:[NSDate date]
                                        msgid:10010
                                   components:array
                                         from:self.myPort
                                     reserved:0];
    NSLog(@"%d",success);
}
@end
復制代碼
截屏2021-06-18 下午3.01.00.png

多線程的實現(xiàn)方式

多線程的四種實現(xiàn)方式分別是:pthreadNSThread化撕,GCD几晤, NSOperation

image.png

下面通過代碼來看一下這四種實現(xiàn)方式:

  1. pthread
/**
     pthread_create 創(chuàng)建線程
     參數(shù):
     1\. pthread_t:要創(chuàng)建線程的結構體指針植阴,通常開發(fā)的時候蟹瘾,如果遇到 C 語言的結構體,類型后綴 `_t / Ref` 結尾
     同時不需要 `*`
     2\. 線程的屬性掠手,nil(空對象 - OC 使用的) / NULL(空地址憾朴,0 C 使用的)
     3\. 線程要執(zhí)行的`函數(shù)地址`
     void *: 返回類型,表示指向任意對象的指針喷鸽,和 OC 中的 id 類似
     (*): 函數(shù)名
     (void *): 參數(shù)類型众雷,void *
     4\. 傳遞給第三個參數(shù)(函數(shù))的`參數(shù)`

     返回值:int
     0          創(chuàng)建線程成功!成功只有一種可能
     非 0       創(chuàng)建線程失敗的錯誤碼做祝,失敗有多種可能砾省!
 */

pthread_t threadId = NULL;
char *cString = "HelloWorld";
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
    NSLog(@"成功");
} else {
    NSLog(@"失敗");
}

void *pthreadTest(void *para){
    // __bridge 將 C 語言的類型橋接到 OC 的類型
    NSString *name = (__bridge NSString *)(para);
    NSLog(@"===>%@ %@", [NSThread currentThread], name);
    return NULL;
}   
復制代碼

2.NSThread

[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
復制代碼
  1. GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self threadTest];
});
復制代碼
  1. NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
        [self threadTest];
}];

- (void)threadTest{
    NSLog(@"begin");
    NSLog(@"over");
}

未完待續(xù)......

作者:Henry_Jeannie
鏈接:https://juejin.cn/post/6975035560607875080
來源:掘金

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市混槐,隨后出現(xiàn)的幾起案子纯蛾,更是在濱河造成了極大的恐慌,老刑警劉巖纵隔,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翻诉,死亡現(xiàn)場離奇詭異,居然都是意外死亡捌刮,警方通過查閱死者的電腦和手機碰煌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绅作,“玉大人芦圾,你說我怎么就攤上這事堵未〔ǚ幔” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長淮腾。 經(jīng)常有香客問我,道長捞稿,這世上最難降的妖魔是什么罩息? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮茫经,結果婚禮上巷波,老公的妹妹穿的比我還像新娘。我一直安慰自己卸伞,他們只是感情好抹镊,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荤傲,像睡著了一般垮耳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上遂黍,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天氨菇,我揣著相機與錄音,去河邊找鬼妓湘。 笑死查蓉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的榜贴。 我是一名探鬼主播豌研,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼唬党!你這毒婦竟也來了鹃共?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤驶拱,失蹤者是張志新(化名)和其女友劉穎霜浴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蓝纲,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡阴孟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了税迷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片永丝。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖箭养,靈堂內(nèi)的尸體忽然破棺而出慕嚷,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布喝检,位于F島的核電站嗅辣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏挠说。R本人自食惡果不足惜澡谭,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纺涤。 院中可真熱鬧,春花似錦抠忘、人聲如沸撩炊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拧咳。三九已至,卻和暖如春囚灼,著一層夾襖步出監(jiān)牢的瞬間骆膝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工灶体, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阅签,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓蝎抽,卻偏偏與公主長得像政钟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子樟结,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容