iOS-多線程

一 進程和線程

進程

  • 進程是一個具有一定獨立功能的程序關(guān)于某次數(shù)據(jù)集合的一次運行活動,它是操作系統(tǒng)分配資源的基本單元.
  • 進程是指在系統(tǒng)中正在運行的一個應用程序,就是一段程序的執(zhí)行過程,可以理解為手機上的一個app.
  • 每個進程之間是獨立的,每個進程均運行在其專用且受保護的內(nèi)存空間內(nèi),擁有獨立運行的全部資源.

線程

  • 程序執(zhí)行流的最小單元,線程是進程中的一個實體.
  • 一個進程要想執(zhí)行任務,必須有一條線程.應用程序啟動時,系統(tǒng)會默認開啟一條線程,也就是主線程.

線程和進程的關(guān)系

  • 線程是進程的執(zhí)行單元,進程所有的任務都在線程中執(zhí)行的.
  • 線程是CPU分配資源和調(diào)度的最小單位.
  • 一個程序可以對應多個進程,一個進程中可以有多個線程,但至少要有一個主線程.
  • 同一個進程內(nèi)的線程共享資源.

二 多進程和多線程

多進程

打開MAC的活動監(jiān)視器,可以看到很多歌進程同事運行.

  • 進程是程序在計算機上的一次執(zhí)行活動.當你運行一個程序,你就啟動了一個進程.顯然,程序 是死的(靜態(tài)的),進程是活的(動態(tài)的).
  • 進程可以分為系統(tǒng)進程和用戶進程.凡是用于完成操作系統(tǒng)的各種功能的進程就是系統(tǒng)進程,他們就是處于運行狀態(tài)下的操作系統(tǒng)本身;所有由用戶啟動的進程都是用戶進程.進程是應用系統(tǒng)進行資源分配的單位.
  • 在同一個時間內(nèi),同一個計算系統(tǒng)中如果允許兩個或兩個以上的進程處于運行狀態(tài),這便是多進程

多線程

同一時間,CPU只能處理一條線程.多線程并發(fā)執(zhí)行,其實是CPU快速的在多個線程之間調(diào)度.如果CPU調(diào)度線程的時間足夠快,就造成了多線程并發(fā)執(zhí)行的假象.如果線程非常多,CPU會在N個多線程之間調(diào)度,消耗大量的CPU資源,每條線程被執(zhí)行的頻次會被降低,造成線程的執(zhí)行率下降.

多線程的優(yōu)點

  • 能適當?shù)奶岣叱绦虻膱?zhí)行效率;
  • 能適當?shù)奶岣哔Y源利用率(CPU,內(nèi)存利用率);

多線程的缺點

  • 開啟線程需要占用一定的內(nèi)存空間(默認情況下,主線程占用1M,子線程占用512KB),如果大量開啟線程,會占用大量的內(nèi)存空間,降低程序性能;
  • 線程越多,CPU在調(diào)度線程上的開銷越大;
  • 程序設計更加復雜:比如線程之間的通信,多線程的數(shù)據(jù)共享;

三 任務/隊列

任務

所執(zhí)行的操作,也就是在線程中執(zhí)行的那段代碼.在GCD中是放在block中的.執(zhí)行任務有兩種方式:同步執(zhí)行(sync)和異步執(zhí)行(async)

同步(sync):同步添加任務到指定的隊列中,在添加的任務執(zhí)行結(jié)束執(zhí)行之前會一直等待,知道隊列里的任務完成之后再繼續(xù)執(zhí)行,即會柱塞線程.只能在當前線程中執(zhí)行任務(不一定是主線程),不具備開辟新線程的能力.

異步(async):線程會立即返回,無需等待就會繼續(xù)執(zhí)行下面的任務,不阻塞當前線程.可以在新的線程中執(zhí)行任務,具備開啟新線程的能力.

隊列

隊列(Dispatch Queue):這里的隊列指執(zhí)行任務的等待隊列,即用來存放任務的隊列.隊列是一種特殊的線性表,采用FIFO(先進先出)的原則,即新任務總是被插入到隊列的末位,而讀取任務總是從隊列的頭部讀取.每讀取一個任務,則從隊列釋放一個任務.

在GCD中有兩種隊列:串行隊列和并發(fā)隊列.兩者都符合FIFO(先進先出)的原則.兩者主要區(qū)別是:執(zhí)行順序不同,以及開啟線程數(shù)不同.

串行隊列(Serial Dispatch Queue):同一時間內(nèi),隊列中只能執(zhí)行一個任務,只有當前的任務執(zhí)行完成之后,才能執(zhí)行下一個任務.(只 開啟一個線程,一個任務執(zhí)行完畢后,再執(zhí)行下一個任務).主隊列是主線程上的一個串行隊列,是 系統(tǒng)自動為我們創(chuàng)建的

并發(fā)隊列(Concurrent Dispatch Queue):同時允許多個任務并發(fā)執(zhí)行.(可以開啟多個線程,并且同時執(zhí)行任務).并發(fā)隊列的并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效.

四 iOS中的多線程

iOS中的多線程主要有三種:NSThread,NSOperationQueue,GCD

NSThread:輕量級別的多線程

是我們手動開辟的子線程,如果使用的是初始化方式需要我們自己啟動,如果使用的構(gòu)造器方式它會自動啟動.只要我們手動開辟線程,都需要我們自己管理該線程,不只是啟動,還有該線程使用完畢后的資源回收.

 NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(testThread:) object:@"我是參數(shù)"];
    //使用初始化方法時需要start來啟動
    [thread start];
    //開辟子線程的名字
    thread.name = @"NSThread線程";
    //線程的權(quán)限,范圍值0-1,權(quán)限越高,先執(zhí)行的概率越高,由于是概率,所以并不能很準確的實現(xiàn)我們想要的執(zhí)行
    thread.threadPriority = 1;
    //取消當前已啟動的線程
    [thread cancel];

performSelector...只要是 NSObject 的子類或者對象都可以通過調(diào)用方法進入子線程和主線程,其實這些 方法所開辟的子線程也是 NSThread 的另一種體現(xiàn)方式,是在NSObject的分類中對NSThread的封裝.在編譯階段并不會去檢查方法是否有效存在,如果不存在只會給出警告.

 //在當前線程延遲1s執(zhí)行
    [self performSelector:@selector(performSelectorTest:) withObject:@"performSelectorTestafterDelay1s" afterDelay:1.f];
    //回到主線程執(zhí)行.waitUntilDone是否將該回調(diào)方法執(zhí)行完在執(zhí)行后面的代碼
    [self performSelectorOnMainThread:@selector(performSelectorTest:) withObject:@"performSelectorOnMainThread" waitUntilDone:YES];
    //開辟子線程
    [self performSelectorInBackground:@selector(performSelectorTest:) withObject:@"performSelectorInBackground"];
    //在指定線程上執(zhí)行
    [self performSelector:@selector(performSelectorTest:) onThread:[NSThread currentThread] withObject:@"performSelectorOnThread" waitUntilDone:YES];

需要注意的是:如果是帶 afterDelay 的延時函數(shù),會在內(nèi)部創(chuàng)建一個 NSTimer,然后添加到當前線程的 Runloop 中.也就是如果當前線程沒有開啟 runloop,該方法會失效.在子線程中,需要啟動 runloop(注意調(diào)用順序)

NSOperationQueue

operation 是蘋果基于GCD封裝的,是面向?qū)ο蟆R话?operation 和 operation queue配合使用,這樣可以很方便的在異步執(zhí)行任務,但是這樣也不是必須的。operation 內(nèi)部有一個 start 方法可調(diào)用,但是無法保證在異步執(zhí)行。我們通常使用的NSInvocationOperation和NSBlockOperation都是NSOperation的子類。當然,我們也可以自己創(chuàng)建NSOperation的子類。

NSInvocationOperation
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest:) object:@"參數(shù)"];
[queue addOperation:invOp];

- (void)operationTest:(NSString *)parameter
{
    NSLog(@"%@ - %@", [NSThread currentThread], parameter);
}

打印結(jié)果可以看到我們在異步線程執(zhí)行了這段方法。如果我們直接使用 operation 的 start 方法而不是加入 queue ,那打印結(jié)果就只會在當前線程打印。

NSBlockOperation
NSOperationQueue *queue = [NSOperationQueue new];
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
}];
[queue addOperation:blockOp];

除了上述使用 NSOperationQueue 添加 NSBlockOperation 實例外, NSBlockOperation 實例還有 addExecutionBlock: 方法,可以方便的添加多個 block

NSOperationQueue *queue = [NSOperationQueue new];

NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1- %@", [NSThread currentThread]);
}];
[blockOp addExecutionBlock:^{
    NSLog(@"2- %@", [NSThread currentThread]);
}];
[blockOp addExecutionBlock:^{
    NSLog(@"3 - %@", [NSThread currentThread]);
}];

//    [blockOp start];
[queue addOperation:blockOp];
自定義NSOperation
@synthesize finished = _finished;
@synthesize executing = _executing;

- (BOOL)isAsynchronous
{
    return YES;
}

- (void)start
{
    if (self.isCancelled) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    
    [self willChangeValueForKey:@"isExecuting"];
    
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    _executing = YES;
    
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)main
{
    for (NSInteger i = 0; i < 9999; i++) {
        if (self.isCancelled) {
            break;
        }
        
        NSLog(@"thread - %@, i is %@", [NSThread currentThread], @(i));
    }
    
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    
    _executing = NO;
    _finished  = YES;
    
    [self didChangeValueForKey:@"isFinished"];
    [self didChangeValueForKey:@"isExecuting"];
}

首先,NSOperation 類中含有executing、finished屬性,并且是只讀的。如果想修改這兩個屬性的值,我們可以使用 @synthesize 關(guān)鍵字手動合成兩個實例變量 _executing 和 _finished 。并且通過 willChangeValueForKey: 和 didChangeValueForKey: 通過 KVO 通知它們的值修改了。

那怎么實現(xiàn)并發(fā)了:
1.重寫 - (BOOL)isAsynchronous 方法來告訴別人方法是否是并發(fā)。
2.一般我們會重寫 start 方法來進行一些初始化操作。比如,如果判斷線程被取消或者添加一些條件判斷。
3.重寫 main 方法做復雜邏輯操作。

GCD

GCD---隊列

GCD共有三種隊列類型:
main queue:通過dispatch_get_main_queue()獲得,這是一個主線程相關(guān)的串行隊列.
global queue:全局隊列是并發(fā)隊列,由整個進程共享.存在高中低三種優(yōu)先級的全局隊列.調(diào)用dispatch_get_global_queue()并傳入優(yōu)先級來訪問.
自定義隊列:通過dispatch_queue_create()創(chuàng)建隊列.

GCD任務執(zhí)行順序
    //創(chuàng)建一個串行隊列
    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"1");
    
    //異步任務插入串行隊列
    dispatch_async(serialQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2");
    });
    
    NSLog(@"3");
    
    //同步任務插入串行隊列
    dispatch_sync(serialQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4");
    });
    
    NSLog(@"5");
    
    //創(chuàng)建一個并發(fā)隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    //異步任務插入并發(fā)隊列
    dispatch_async(concurrentQueue, ^{
       [NSThread sleepForTimeInterval:2];
        NSLog(@"6");
    });
    
    NSLog(@"7");
    
    //同步任務插入并發(fā)隊列
    dispatch_sync(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"8");
    });
    
    NSLog(@"9");
    
    //獲取主隊列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    //異步任務插入主隊列
    dispatch_async(mainQueue, ^{
       [NSThread sleepForTimeInterval:2];
        NSLog(@"10");
    });
    
    NSLog(@"11");
    
    //同步任務插入主隊列
    dispatch_sync(mainQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"12");
    });
    
    NSLog(@"13");

打印順序是1,3,2,4,5,7,8,9,11 (6隨機出現(xiàn)在7之后或者沒有)
原因是:首先打印 1,接下來將任務2添加至串行隊列上,由于任務2是異步,不會阻塞線程,繼續(xù)向下執(zhí)行,打印3,然后是任務4,將任務4添加至串行隊列上,因為任務4和任務2在同一個串行隊列上,任務4必須等任務2執(zhí)行后才能執(zhí)行,又因為任務4是同步任務,會阻塞線程,只有執(zhí)行完4后才能繼續(xù)向下執(zhí)行打印5.之后創(chuàng)建一個并發(fā)隊列并將異步任務6添加到并發(fā)隊列,因為6是異步任務不會阻塞線程,繼續(xù)向下執(zhí)行,打印7.接下來將同步任務8添加到并發(fā)隊列,因為是并發(fā)隊列8不需要等6完成,又因為8是同步任務,會阻塞線程,所以任務8執(zhí)行完后才能執(zhí)行任務9.向下繼續(xù)執(zhí)行,獲取主隊列,前面提到過主隊列是一個主線程相關(guān)的串行隊列,所以把異步任務10添加到主隊列不會阻塞主線程,接下來會打印11.當把同步任務12添加到主線程時,會發(fā)生死鎖(關(guān)于死鎖,下面篇幅有講解),所以12,13不會被執(zhí)行,異步任務10也因為死鎖的原因斌沒有執(zhí)行完.任務6會因為計算時間不穩(wěn)定,隨機出現(xiàn)在7之后,或者沒有被打印.

總結(jié) 串行隊列 并發(fā)隊列 主隊列
同步任務(sync) 不開辟新線程,阻塞當前線程 不開辟新線程,阻塞當前線程 死鎖
異步任務(async) 開辟新線程,不阻塞當前線程,異步任務串行執(zhí)行 開辟新線程,不阻塞當前線程,異步任務并發(fā)執(zhí)行 主線程運行,不阻塞當前線程,異步任務串行執(zhí)行

CGD中的死鎖

-(void)mainQueueLock{
//獲取主隊列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    //異步任務插入主隊列
    dispatch_async(mainQueue, ^{
       [NSThread sleepForTimeInterval:2];
        NSLog(@"10");
    });
    
    NSLog(@"11");
    
    //同步任務插入主隊列
    dispatch_sync(mainQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"12");
    });
    
    NSLog(@"13");
}

上面例子中,會在同步任務12添加到主隊列時發(fā)生死鎖.我們知道主隊列是一個主線程相關(guān)的串行隊列,當程序執(zhí)行到mainQueueLock方法時,已經(jīng)在主線程中有一個同步任務mainQueueLock,而在執(zhí)行mainQueueLock時又向主隊列中添加了同步任務12.mainQueueLock是同步任務,會阻塞線程,只有mainQueueLock執(zhí)行完成后才能執(zhí)行任務12,而mainQueueLock要執(zhí)行完又必須得執(zhí)行完任務12,這樣兩個任務就會因為相互等待而導致死鎖.
同樣,下面的代碼也會造成死鎖.

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    dispatch_async(serialQueue, ^{
        dispatch_sync(serialQueue, ^{
                   NSLog(@"0");
        });
    });

外面的函數(shù)無論是同步還是異步都會造成死鎖
這是因為里面的任務和外面的任務都太同一個串行隊列里,又是同步,這和上面的主隊列同步是一樣的.
解決方案,試講里面的同步改為異步或者將serialQueue換成其他串行隊列或者并行隊列.

GCD中的柵欄函數(shù)

當任務需要異步進行,但這些任務需要分成兩組,第一組完成才能進行第二組操作.這時候就要用到GCD的柵欄方法dispatch_barrier_async和dispatch_barrier_sync.

    //創(chuàng)建串行隊列
   dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_SERIAL);
    NSLog(@"當前線程%@",[NSThread currentThread]);
    //串行隊列添加異步操作
    dispatch_async(quene, ^{
        NSLog(@"1號我在哪里啊-----%@",[NSThread currentThread]);
    });
    //設置同步柵欄
    dispatch_barrier_async(quene, ^{
        NSLog(@"異步柵欄操作線程------%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"異步柵欄休息5秒");
    });
    //串行隊列添加同步操作
    dispatch_sync(quene, ^{
        
        NSLog(@"2號我在哪里啊-----%@",[NSThread currentThread]);
    });
    //設置異步柵欄
    dispatch_barrier_sync(quene, ^{
           NSLog(@"同步柵欄操作線程------%@",[NSThread currentThread]);
           sleep(5);
           NSLog(@"同步柵欄休息五秒");
       });
    
    NSLog(@"結(jié)束");

打印結(jié)果是

14:16:25 當前線程<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:25 1號我在哪里啊-----<NSThread: 0x600002e94bc0>{number = 4, name = (null)}
14:16:25 異步柵欄操作線程------<NSThread: 0x600002e8ffc0>{number = 3, name = (null)}
14:16:30 異步柵欄休息5秒
14:16:30 2號我在哪里啊-----<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:30 同步柵欄操作線程------<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:35 同步柵欄休息五秒
14:16:35 結(jié)束

我們再試下并發(fā)隊列

    //創(chuàng)建并發(fā)隊列
   dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"當前線程%@",[NSThread currentThread]);
    //串行隊列添加異步操作
    dispatch_async(quene, ^{
        NSLog(@"1號我在哪里啊-----%@",[NSThread currentThread]);
    });
    //設置同步柵欄
    dispatch_barrier_async(quene, ^{
        NSLog(@"異步柵欄操作線程------%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"異步柵欄休息5秒");
    });
    //串行隊列添加同步操作
    dispatch_sync(quene, ^{
        
        NSLog(@"2號我在哪里啊-----%@",[NSThread currentThread]);
    });
    //設置異步柵欄
    dispatch_barrier_sync(quene, ^{
           NSLog(@"同步柵欄操作線程------%@",[NSThread currentThread]);
           sleep(5);
           NSLog(@"同步柵欄休息五秒");
       });
    
    NSLog(@"結(jié)束");

結(jié)果

14:28:33 當前線程<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:33 1號我在哪里啊-----<NSThread: 0x600002fa26c0>{number = 6, name = (null)}
14:28:33 異步柵欄操作線程------<NSThread: 0x600002fa26c0>{number = 6, name = (null)}
14:28:38 異步柵欄休息5秒
14:28:38 2號我在哪里啊-----<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:38 同步柵欄操作線程------<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:43 同步柵欄休息五秒
14:28:43 結(jié)束

我們對比兩次的數(shù)據(jù)可以看到.
1.異步柵欄會開辟新的線程;
2.同步柵欄在當前線程上;
3.無論同步柵欄還是異步柵欄,都是需要前面一組完成后,在執(zhí)行柵欄方法,之后再執(zhí)行下一組操作.
從上面看,兩種柵欄除了線程不一樣其他基本相同.事實上,在基本需求上兩者確實差不多.但柵欄方法內(nèi)部執(zhí)行異步方法時就有所區(qū)別.

    dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_SERIAL);
    NSLog(@"當前線程%@",[NSThread currentThread]);
    
    dispatch_async(quene, ^{
        NSLog(@"1號我在哪里啊-----%@",[NSThread currentThread]);
    });
    
    dispatch_barrier_sync(quene, ^{
        NSLog(@"同步柵欄操作線程1------%@",[NSThread currentThread]);
        dispatch_async(quene, ^{
            NSLog(@"同步柵欄操作線程2------%@",[NSThread currentThread]);
            sleep(5);
            NSLog(@"同步柵欄休息5秒");
        });
    });
    
    NSLog(@"同步柵欄-異步操作--結(jié)束");
  
    dispatch_barrier_async(quene, ^{
        NSLog(@"異步柵欄操作線程1------%@",[NSThread currentThread]);
        dispatch_async(quene, ^{
            NSLog(@"異步柵欄操作線程2------%@",[NSThread currentThread]);
            sleep(5);
            NSLog(@"異步柵欄休息5秒");
        });
    });

    NSLog(@"異步柵欄-異步操作--結(jié)束");
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容