2018-06-25 学习GCD

作者: 肠粉白粥_Hoben | 来源:发表于2018-06-25 17:51 被阅读55次

一.基本概念

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。

相关文章

  • 2018-06-25 学习GCD

    一.基本概念 Grand Central Dispatch,可以将应用需要执行的工作拆分为可分散在多个线程和多个C...

  • GCD学习(三)

    GCD学习一 GCD学习二 GCD学习三 常用函数: dispatch_set_target_queue disp...

  • 学习GCD看我就够了

    学习GCD看我就够了 学习GCD看我就够了

  • GCD学习(二)

    GCD学习一 GCD学习二 GCD学习三 一。队列有哪几种呢? 1.1、自定义的队列 :dispatch_queu...

  • 十一、ios线程调用学习

    常用线程方式:GCD 一、GCD学习 Grand Central Dispatch(GCD) 是 Apple 开发...

  • GCD学习(一)

    GCD学习一 GCD学习二 GCD学习三 我不会讲太多理论,这篇是我自己的理解。 举个例子: 汽车进入维修厂的关...

  • watch - 2018-06-25

    2018-06-25 创建

  • 4.2.1 Service 练气

    标注:本文为个人学习使用,仅做自己学习参考使用,请勿转载和转发2018-06-25: 初稿,这个周末什么也没有学习...

  • GCD 线程安全同步-信号量

    GCD 线程安全同步 学习、记录与分享 GCD 与 NSThread比较 GCD会自动利用更多的CPU内核、 会自...

  • iOS GCD的使用

    什么是GCD了解GCD前,需要了解的基础知识GCD的使用使用注意事项 -GCD学习前铺垫-什么是GCDGCD (G...

网友评论

    本文标题:2018-06-25 学习GCD

    本文链接:https://www.haomeiwen.com/subject/vfoqyftx.html