2018-06-25 學習GCD

一.基本概念

Grand Central Dispatch,可以將應用需要執行的工作拆分為可分散在多個線程和多個CPU上的更小的塊。

二.SlowWorker模擬

1.基本布局

這個布局有毒..自己弄了很久,還是太年輕..

- (void) initialize
{
    _startButton = [[UIButton alloc] init];
    [_startButton setTitleColor: [UIColor blueColor]
                       forState: UIControlStateNormal];
    [_startButton setTitleColor: [UIColor lightGrayColor]
                       forState: UIControlStateSelected];
    [_startButton addTarget: self
                     action: @selector(startClicked:)
           forControlEvents: UIControlEventTouchUpInside];
    [_startButton setTitle: @"Start Working"
                  forState: UIControlStateNormal];
    [self.view addSubview: _startButton];
    
    [_startButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(30);
        make.centerX.equalTo(self.view);
        make.height.mas_equalTo(100);
    }];
    
    _resultsTextView = [[UITextView alloc] init];
    [_resultsTextView setTextColor: [UIColor blackColor]];
    [self.view addSubview: _resultsTextView];
    
    [_resultsTextView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_startButton.mas_bottom).offset(30);
        make.width.mas_equalTo(200);
        make.left.equalTo(self.view).offset(20);
        make.height.mas_equalTo(100);
    }];
}

2.設置響應事件

為了模擬出那個相應很久的效果,在這里用了sleep

- (NSString *) fetchSomethingFromServer
{
    [NSThread sleepForTimeInterval: 1];
    return @"Hi There";
}

- (NSString *) processData: (NSString *) data
{
    [NSThread sleepForTimeInterval: 2];
    return [data uppercaseString];
}

- (NSString *) calculateFirstResult: (NSString *) data
{
    [NSThread sleepForTimeInterval: 3];
    return [NSString stringWithFormat: @"Number of chars: %lu", (unsigned long) [data length]];
}

- (NSString *) calculateSecondResult: (NSString *) data
{
    [NSThread sleepForTimeInterval: 4];
    return [data stringByReplacingOccurrencesOfString: @"E"
                                           withString: @"e"];
}

再為Button設置一下響應事件:

- (void) startClicked: (UIButton *) sender
{
    
    self.resultsTextView.text = @"";
    NSDate *startTime = [NSDate date];
    NSString *fetchedData = [self fetchSomethingFromServer];
    NSString *processedData = [self processData: fetchedData];
    NSString *firstResult = [self calculateFirstResult: processedData];
    NSString *secondResult = [self calculateSecondResult: processedData];

    NSString *resultSummary = [NSString stringWithFormat: @"First: [%@]\nSecond: [%@]", firstResult, secondResult];
    [_resultsTextView setText: resultSummary];

    NSDate *endTime = [NSDate date];
    NSLog(@"Completed in %f seconds", [endTime timeIntervalSinceDate: startTime]);
}

可以發現,UI會被一系列的讀取操作阻塞,導致遲遲不刷新。

二.使用GCD

GCD的一個重要概念是隊列。系統提供了許多預定義的隊列,包括可以保證始終在主線程上執行其工作的隊列,非常適合非線程安全的UIKit。開發人員也可以創建自己的隊列,按照自己的需求創建任意多個。GCD隊列嚴格遵守FIFO原則,添加到GCD隊列的工作單元將始終按照加入隊列的順序啟動。

1.閉包和代碼塊

閉包和代碼塊可以訪問在創建它的范圍內所有可用的變量。其中閉包擁有對范圍內變量的讀寫權限,可以向其傳遞參數。但是,因為閉包是在訪問時獲取變量的值,而不是在閉包被創建時。默認情況下,代碼塊通過這種方式“捕獲”了你訪問的任何變量,將值復制到一個新的同名變量中,保留原始變量不變。可以在變量聲明之前添加存儲修飾符__block,進行外部變量“讀/寫”。

//定義一個能夠被塊訪問并修改的變量
__block int a = 0;

//定義一個塊,這個塊會修改它作用域中的一個變量
void (^sillyBlock)(void) = ^{ a = 47; };

//對塊進行調用之前,先檢查變量的值
NSLog(@"a == %d", a);    //a == 0

//執行塊
sillyBlock();

//調用塊之后再次檢查變量的值
NSLog(@"a == %d", a);    //a == 47

有了閉包和代碼塊之后,只需一步就可以將它添加到隊列中。如果在定義閉包和代碼塊后立即將它添加到隊列,而不是存儲到變量之后,就多了一個優勢:能在使用代碼的上下文中看到相關代碼。

2.改進SlowWorker

我們讓之前的讀取等方法放在后臺運行,只需將所有代碼包裝在一個閉包/代碼塊中,并將它傳遞給一個名為dispatch_async的GCD函數。

此函數接受兩個參數:一個GCD隊列和一個分配給該隊列的閉包/代碼塊。

- (void) startClicked: (UIButton *) sender
{
    self.resultsTextView.text = @"";
    NSDate *startTime = [NSDate date];
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSString *fetchedData = [self fetchSomethingFromServer];
        NSString *processedData = [self processData: fetchedData];
        NSString *firstResult = [self calculateFirstResult: processedData];
        NSString *secondResult = [self calculateSecondResult: processedData];
        
        NSString *resultSummary = [NSString stringWithFormat: @"First: [%@]\nSecond: [%@]", firstResult, secondResult];
        [_resultsTextView setText: resultSummary];
        
        NSDate *endTime = [NSDate date];
        NSLog(@"Completed in %f seconds", [endTime timeIntervalSinceDate: startTime]);
    });
    
}

3.并發閉包/代碼塊

但是上面的方法依舊是10秒,因為我們所做的只是將此方法的一部分轉移到了一個后臺線程,然后在主線程完成它。最重要的問題在于,calculateFirstResultcalculateSecondResult方法之間沒有依賴關系,不需要順序執行,并發執行可以顯著提高速度。

這時候,就要使用分派組dispatch group了,將在一個組的上下文中通過dispatch_group_async()函數異步分派的所有閉包/代碼塊設置為松散的,以便盡可能快執行。如果可能,將它們分發給多個線程同時執行。也可以使用dispatch_group_notify()指定一個額外的閉包/代碼塊,讓它在組中的所有閉包/代碼塊運行完成時再執行。

- (void) startClicked: (UIButton *) sender
{
    self.resultsTextView.text = @"";
    NSDate *startTime = [NSDate date];
    self.startButton.enabled = NO;
    
    [self.spinner startAnimating];
    //第一個參數DISPATCH_QUEUE_PRIORITY_DEFAULT指定優先級
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSString *fetchedData = [self fetchSomethingFromServer];
        NSString *processedData = [self processData: fetchedData];
        __block NSString *firstResult;
        __block NSString *secondResult;
        
        dispatch_group_t group = dispatch_group_create();
        dispatch_group_async(group, queue, ^{
            firstResult = [self calculateFirstResult: processedData];
        });
        
        dispatch_group_async(group, queue, ^{
            secondResult = [self calculateSecondResult: processedData];
        });
        dispatch_group_notify(group, queue, ^{
            NSString *resultSummary = [NSString stringWithFormat: @"First: [%@]\nSecond: [%@]", firstResult, secondResult];
            //執行需要主線程的代碼
            dispatch_async(dispatch_get_main_queue(), ^{
                [_resultsTextView setText: resultSummary];
                self.startButton.enabled = YES;
                [self.spinner stopAnimating];
            });
            NSDate *endTime = [NSDate date];
            NSLog(@"Completed in %f seconds", [endTime timeIntervalSinceDate: startTime]);
        });
    });
    
}

可以看到,現在7秒就可以完成這個工作了!


更新于7.2,看到一篇很不錯的文章:GCD學習總結,現在補充一下自己遺漏的知識點:

一.GCD任務與隊列

任務隊列可以說是GCD的兩個核心概念了:

1.任務(Task)

即在線程中執行的那段代碼,在GCD中放在block里面。分為兩種執行方式:同步執行(sync)異步執行(async),這兩者的區別在于:是否等待隊列的任務執行結束、是否具備開啟新線程的能力。

  • 同步執行(sync):同步添加任務到指定的隊列中,在添加的任務執行結束之前會一直等待,直到隊列里面的任務完成之后再繼續執行。即:只能在當前線程中執行任務,不具備開啟新線程的能力。

  • 異步執行(async):異步添加任務到指定的隊列中,不會做任何等待,可以繼續執行任務。即:可以在新的線程執行任務,具備開啟新線程的能力。

在這里要注意: 異步執行(async)雖然具有開啟新線程的能力,但是并不一定會開啟新線程,與任務所指定的隊列類型有關。

2.隊列(Dispatch Queue)

執行任務的等待隊列,即用來存放任務的隊列。隊列是一種特殊的線性表,采用FIFO原則。分為兩種隊列:串行隊列(Serial Dispatch Queue)并發隊列(Concurrent Dispatch Queue)。兩者都符合FIFO的原則,其主要區別在于:執行順序不同、開啟的線程數不同。

  • 串行隊列(Serial Dispatch Queue):每次只有一個任務被執行,讓任務一個接著一個地執行。一個任務執行完畢之后,再執行下一個任務。串行隊列只開啟一個新線程(或者不開啟,在當前線程執行任務)。

  • 并發隊列(Concurrent Dispatch Queue):可以讓多個任務并發(同時)執行,可以開啟多個線程,并且同時執行任務。

在這里要注意:并發隊列(Concurrent Dispatch Queue)的并發功能只有在異步(dispatch_async)函數下才有效。

二.GCD的使用

1.隊列的創建與獲取方法

使用dispatch_queue_create創建隊列,傳入的參數:第一個為隊列的唯一標識符,用于debug,可置空。第二個用來識別隊列的類型(串行/并行),有DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT

// 串行隊列的創建方法
dispatch_queue_tqueue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);

// 并發隊列的創建方法
dispatch_queue_tqueue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);

GCD有一種特殊的串行隊列,叫做主隊列(Main Dispatch Queue),所有放在主隊列中的任務,都會放到主線程中執行。獲取方法如下:

// 主隊列的獲取方法
dispatch_queue_tqueue = dispatch_get_main_queue();

而對于并發隊列,GCD則提供了全局并發隊列(Global Dispatch Queue),獲取方法如下:

// 全局并發隊列的獲取方法
dispatch_queue_tqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

2.任務的創建方法

GCD提供的同步/異步創建方法分別對應dispatch_syncdispatch_async。任務+隊列的方法經過組合可以得到六種組合方案:

1.同步+并發

在當前線程中執行任務,不會開啟新線程,執行完一個任務,再執行下一個任務。

所有任務都是在當前線程(主線程)中執行的,沒有開啟新的線程(同步執行不具備開啟新線程的能力)。

2.異步+并發

可以開啟多個線程,任務交替(同時)執行。

除了當前線程(主線程),系統又開啟了多個線程,并且任務是交替/同時執行的。(異步執行具備開啟新線程的能力。且并發隊列可開啟多個線程,同時執行多個任務)。

3.同步+串行

不會開啟新線程,在當前線程執行任務。任務是串行的,執行完一個任務,再執行下一個任務。

所有任務都是在當前線程(主線程)中執行的,并沒有開啟新的線程(同步執行不具備開啟新線程的能力)。

4.異步+串行

會開啟新線程,但是因為任務是串行的,執行完一個任務,再執行下一個任務

開啟了一條新線程(異步執行具備開啟新線程的能力,串行隊列只開啟一個線程)。

5.同步+主隊列

同步執行 + 主隊列在不同線程中調用結果也是不一樣的。

  • 在主隊列調用:會出現死鎖

在同步執行 + 主隊列可以驚奇的發現:

在主線程中使用同步執行 + 主隊列,追加到主線程的任務1、任務2、任務3都不再執行了,而且syncMain---end也沒有打印,在XCode 9上還會報崩潰。這是為什么呢?

這是因為我們在主線程中執行syncMain方法,相當于把syncMain任務放到了主線程的隊列中。而同步執行會等待當前隊列中的任務執行完畢,才會接著執行。那么當我們把任務1追加到主隊列中,任務1就在等待主線程處理完syncMain任務。而syncMain任務需要等待任務1執行完畢,才能接著執行。

那么,現在的情況就是syncMain任務和任務1都在等對方執行完畢。這樣大家互相等待,所以就卡住了,所以我們的任務執行不了,而且syncMain---end也沒有打印。

  • 在其他線程調用:不會出現死鎖

任務是按順序執行的(主隊列是串行隊列,每次只有一個任務被執行,任務一個接一個按順序執行)。

為什么現在就不會卡住了呢?

因為syncMain 任務放到了其他線程里,而任務1、任務2、任務3都在追加到主隊列中,這三個任務都會在主線程中執行。syncMain 任務在其他線程中執行到追加任務1到主隊列中,因為主隊列現在沒有正在執行的任務,所以,會直接執行主隊列的任務1,等任務1執行完畢,再接著執行任務2、任務3。所以這里不會卡住線程。

6.異步+主隊列

只在主線程中執行任務,執行完一個任務,再執行下一個任務。

所有任務都是在當前線程(主線程)中執行的,并沒有開啟新的線程(雖然異步執行具備開啟線程的能力,但因為是主隊列,所以所有任務都在主線程中)。

三.GCD的其他方法

1.GCD柵欄方法:dispatch_barrier_async

如果我們需要異步執行兩組操作,且第一組執行完之后才能開始第二組的操作,則這時候需要用柵欄一樣的一個方法將兩組異步執行的操作給分隔起來。用到了dispatch_barrier_async,這個函數會等待前面追加到并發隊列中的任務全部執行完畢后,再將指定的任務追加到該異步隊列中。

2.GCD延時執行方法:dispatch_after

如果我們需要在指定時間之后執行某個任務,則可以用dispatch_after來實現。但是這個函數并不是在指定時間之后才開始執行處理,而是在指定時間之后將任務追加到主隊列中,嚴格來說這個時間并不是絕對準確的。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
        NSLog(@"after...%@", [NSThread currentThread]);
});

3.GCD一次性代碼:dispatch_once

在創建單例,或者有整個程序運行過程中只執行一次的代碼的時候,就要用到dispatch_once函數,它能保證某段代碼在程序運行過程中只被執行一次,并且即使在多線程的環境下,它也可以保證線程安全。

4.GCD快速迭代方法:dispatch_apply

通常我們會用for循環遍歷,但是GCD給我們提供了快速迭代的函數dispatch_apply,它會按照指定的次數將特定的任務追加到特定的隊列中,并等待全部隊列執行結束。

如果在串行隊列中使用dispatch_apply,就會和for循環一樣按順序同步執行,體現不出快速迭代的意義。

如果利用并發隊列進行異步執行,如:遍歷0~5這六個數字,for循環的做法是每次取出一個元素逐個遍歷。dispatch_apply則可以在對個線程同時(異步)遍歷多個數字。

無論是在串行隊列還是在異步隊列,dispatch_apply都會等待全部任務執行完畢,這點就像是同步操作,也像是隊列組當中的dispatch_group_wait方法。

dispatch_apply(6, queue, ^(size_t index) {
        [NSThread sleepForTimeInterval: 2];
        NSLog(@"%zd...%@", index, [NSThread currentThread]);
    });

可以看到,首先執行的是耗時一秒的fetch,然后是耗時兩秒的apply(共開了四個線程),然后是耗時兩秒的process,最后是耗時兩秒的剩余兩個線程的apply。

參考這篇帖子,知道了這個函數的用處:
在某些場景下使用dispatch_apply會對性能有很大的提升,比如你的代碼需要以每個像素為基準來處理計算image圖片。同時dispatch_apply能夠避免一些線程爆炸的情況發生(創建很多線程)

//危險,可能導致線程爆炸以及死鎖
for (int i = 0; i < 999; i++){
   dispatch_async(q, ^{...});
}
dispatch_barrier_sync(q, ^{});

// 較優選擇, GCD 會管理并發
dispatch_apply(999, q, ^(size_t i){...});

應用場景:
如果我們從服務器獲取一個數組的數據,那么我們可以使用該方法從而快速的批量字典轉模型。
代碼示例如下:

NSArray *dictArray = nil;//存放從服務器返回的字典數組
    
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
       
    dispatch_apply(dictArray.count, queue,  ^(size_t index){
            //字典轉模型
            
   });
    dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"主線程更新");
    });
 });

5.GCD的隊列組:dispatch_group

在前文已經提及到,如果需要異步執行兩個耗時任務,當兩個耗時任務都執行完畢再回到主線程執行任務的時候,就可以用到dispatch_group了。

首先調用dispatch_group_async把任務放到隊列中,然后把隊列放到隊列組中。

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    firstResult = [self calculateFirstResult: processedData];
});

dispatch_group_async(group, queue, ^{
    secondResult = [self calculateSecondResult: processedData];
});

然后調用dispatch_group_notify監聽group中任務的完成狀態,當所有任務都執行完成之后,追加任務到group中,并執行任務。

dispatch_group_notify(group, queue, ^{
    NSString *resultSummary = [NSString stringWithFormat: @"First: [%@]\nSecond: [%@]", firstResult, secondResult];
    //執行需要主線程的代碼
    dispatch_async(dispatch_get_main_queue(), ^{
        [_resultsTextView setText: resultSummary];
        self.startButton.enabled = YES;
        [self.spinner stopAnimating];
    });
    NSDate *endTime = [NSDate date];
    NSLog(@"Completed in %f seconds", [endTime timeIntervalSinceDate: startTime]);
});

或者調用dispatch_group_wait來阻塞當前線程,等待指定的group中的任務執行完成后,才會往下繼續執行。如下面的例子所示,不加dispatch_group_wait和加了dispatch_group_wait的執行順序是有區別的。

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    firstResult = [self calculateFirstResult: processedData];
});

dispatch_group_async(group, queue, ^{
    secondResult = [self calculateSecondResult: processedData];
});
//dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
    NSLog(@"I am handsome!");
});
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    firstResult = [self calculateFirstResult: processedData];
});

dispatch_group_async(group, queue, ^{
    secondResult = [self calculateSecondResult: processedData];
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
    NSLog(@"I am handsome!");
});

6.GCD信號量

GCD的信號量是指dispatch_semaphore,即持有計數的信號。類似于高速路收費站的欄桿,可以通過時打開欄桿,不可以通過時關閉欄桿。而在信號量中,計數為0時等待,計數\geqslant1時,計數減一且不等待。

  • dispatch_semaphore_create:創建一個Semaphore信號量并初始化總量。

  • dispatch_semaphore_signal:發送一個信號,讓信號總量加一。

  • dispatch_semaphore_wait:可以使總信號量減一,當信號總量為0時則被阻塞。

在實際應用中,信號量可以用于:

  • 保證線程同步,將異步執行任務轉換為同步執行任務。

  • 保證線程安全,為線程加鎖。

6.1 線程同步

在開發中,如果遇到這樣一種需求:異步執行耗時任務,并使用異步執行的結果進行一些額外的操作。換句話說,就是將異步任務轉換為同步執行任務。如:AFNetworking中,AFURLSessionManager.m里面的tasksForKeyPath:方法。通過引入信號量的方式,等待異步執行任務結果,獲取到tasks,再返回該tasks

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

下面來參照博客寫一個例子,可以看到Semaphore能夠確保number賦值為100之后再打印。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval: 2];
    NSLog(@"Sleeping...%@", [NSThread currentThread]);
    number = 100;
    dispatch_semaphore_signal(semaphore);
});
//加了是100,不加是0
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"Number is: %d", number);

6.2 線程安全和線程同步(為線程加鎖)

  • 線程安全:如果代碼所在的進程中有多個線程在同時運行,而這些線程可能同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

若每個線程中對全局變量、靜態變量只有讀操作,而沒有寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作(更改變量),一般需要考慮線程同步,否則就可能影響線程安全。

  • 線程同步:可理解為線程A和線程B一塊配合,A執行到一定程度時要依靠線程B的某個結構,于是停下來,示意B運行;B運行后再將結果給A;A再繼續操作。

下面模擬一個場景:總共有50張火車票,有兩個售賣火車票的窗口,一個是北京,一個是上海,它們同時售賣火車票,售完即止。

- (void) initTicketStatusNotSave
{
    NSLog(@"Current Thread---%@", [NSThread currentThread]);
    NSLog(@"semaphore--begin");
    self.ticketCount = 50;
    
    dispatch_queue_t beijingQueue = dispatch_queue_create("yellow.slowWorker.beijingQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t shanghaiQueue = dispatch_queue_create("yellow.slowWorker.shanghaiQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(beijingQueue, ^{
        [self saleTicketNotSafe];
    });
    
    dispatch_async(shanghaiQueue, ^{
        [self saleTicketNotSafe];
    });
}

- (void) saleTicketNotSafe
{
    while (1) {
        if (self.ticketCount > 0) {
            //還有票
            self.ticketCount--;
            NSLog(@"%@", [NSString stringWithFormat: @"剩余票數:%d,窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval: 0.2];
        }
        else {
            NSLog(@"所有火車票已售完");
            break;
        }
    }
}

非線程安全狀態下(不加鎖),會得到一個錯亂的票數。

我們再來加鎖看看會發生什么情況:

- (void) initTicketStatusSave
{
    NSLog(@"Current Thread---%@", [NSThread currentThread]);
    NSLog(@"semaphore--begin");
    self.ticketCount = 50;
    
    dispatch_queue_t beijingQueue = dispatch_queue_create("yellow.slowWorker.beijingQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t shanghaiQueue = dispatch_queue_create("yellow.slowWorker.shanghaiQueue", DISPATCH_QUEUE_SERIAL);
    _semaphore = dispatch_semaphore_create(1);
    dispatch_async(beijingQueue, ^{
        [self saleTicketSafe];
    });
    
    dispatch_async(shanghaiQueue, ^{
        [self saleTicketSafe];
    });
}

- (void) saleTicketSafe
{
    while (1) {
        //加鎖
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        
        if (self.ticketCount > 0) {
            //還有票
            self.ticketCount--;
            NSLog(@"%@", [NSString stringWithFormat: @"剩余票數:%d,窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval: 0.2];
        }
        else {
            NSLog(@"所有火車票已售完");
            
            //解鎖
            dispatch_semaphore_signal(_semaphore);
            break;
        }
        
        //解鎖
        dispatch_semaphore_signal(_semaphore);
    }
}

可以看到,加鎖之后,火車票出售的順序得到了保證!因此,多個線程同步的問題也得以解決了!

又想到一個問題,如果我把兩個售票點都改成同步,不也可以避免出售的混亂嗎?來試試:

- (void) initTicketStatusNotSave
{
    NSLog(@"Current Thread---%@", [NSThread currentThread]);
    NSLog(@"semaphore--begin");
    self.ticketCount = 50;
    
    dispatch_queue_t beijingQueue = dispatch_queue_create("yellow.slowWorker.beijingQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t shanghaiQueue = dispatch_queue_create("yellow.slowWorker.shanghaiQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(beijingQueue, ^{
        NSLog(@"beijing!");
        [self saleTicketNotSafe];
    });
    
    dispatch_sync(shanghaiQueue, ^{
        NSLog(@"shanghai!");
        [self saleTicketNotSafe];
    });
}

- (void) saleTicketNotSafe
{
    while (1) {
        if (self.ticketCount > 0) {
            //還有票
            self.ticketCount--;
            NSLog(@"%@", [NSString stringWithFormat: @"剩余票數:%d,窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval: 0.2];
        }
        else {
            NSLog(@"所有火車票已售完");
            break;
        }
    }
}


哈哈哈,上海根本就用不上嘛..兩個線程同時工作的問題可不能用同步解決哦!


更新于7.12的一篇文章,重新回顧了GCD。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容