当前位置: 首页 > 工具软件 > Dispatch > 使用案例 >

GCD中dispatch_semaphore(信号量)的使用方法

柴彬
2023-12-01

Dispatch Semaphore(信号量) 是持有计数的信号,该信号是多线程编程中的计数类型信号。信号类似于高速收费站的栏杆,可以通过时抬起栏杆,不可以通过时放下栏杆。在 Dispatch Semaphore 中使用了计数来实现该功能:计数小于 0 时等待,阻塞当前线程。计数为 0 或大于 0 时,唤醒线程,继续执行线程中的代码。


Dispatch Semaphore 的三个方法

Dispatch Semaphore 提供了三种方法来改变信号量的值:

  • dispatch_semaphore_create
  • dispatch_semaphore_wait
  • dispatch_semaphore_signal

dispatch_semaphore_t dispatch_semaphore_create(long value);

dispatch_semaphore_create 函数可以创建新的用于计数的信号量,参数即为初始化的信号的值,如果传入的值小于 0 的值,将会导致返回值为 NULL。

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

dispatch_semaphore_wait 函数会使传入的信号量(第一个参数)的值减 1,如果结果值小于 0,则此函数在返回(return)前将一直等待直到信号出现;

第二个参数是指定多长时间超时,系统提供了 2 种时间定义方便我们使用:DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER

返回值为 0 代表函数执行成功,为非 0 时代表函数执行超时。

例如:当前信号量的值是 0,然后调用 dispatch_semaphore_wait 函数并将这个信号量作为参数传入,那么信号量的值就变成了 -1,也就是小于 0 了,此时 dispatch_semaphore_wait 函数将无法返回(return),会一直等待,直到出现信号或者到达超时时间来解除阻塞,然后才能返回 long 类型的值。利用这一特点可以人为阻塞线程。

long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

dispatch_semaphore_signal 函数会使传入的信号量的值加 1,如果信号量的前一个值小于 0,则此函数在返回(return)前会唤醒一个等待的线程。所以 dispatch_semaphore_signal 函数一般会和 dispatch_semaphore_wait 函数配合使用,先利用 dispatch_semaphore_wait 函数阻塞线程,再利用 dispatch_semaphore_signal 函数唤醒这个线程继续执行下去。

如果线程被唤醒,该函数将返回非 0 值。否则,返回 0。


学会使用 Dispatch Semaphore 的三个方法

下面,我们通过一个例子来熟悉一下这三个函数的使用方法:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"-- begin -- %@", [NSThread currentThread]);
        
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    __block int number = 0;
        
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"task -- %@", [NSThread currentThread]);
            
        number = 10;
            
        long count =  dispatch_semaphore_signal(semaphore);
        NSLog(@"return value of dispatch_semaphore_signal: %ld", count);
    });
        
    long count = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"return value of dispatch_semaphore_wait: %ld", count);
        
    NSLog(@"-- end -- number is now %d", number);
});

我们来逐句分析上面例子中各行代码的作用:

1、使用 dispatch_async 函数创建一个全局并发队列异步执行任务;
2、打印当前线程(子线程);
3、创建一个信号量,初始值设为 0;
4、创建一个 __block 修饰的局部变量并设置其初始值为 0;
5、使用 dispatch_async 函数创建一个全局并发队列异步执行任务:

线程休眠 2 秒
打印当前线程
修改 number 的值
使用 dispatch_semaphore_signal 函数增加信号量的值
打印 dispatch_semaphore_signal 函数的返回值

6、使用 dispatch_semaphore_wait 函数减少信号量的值;
7、打印 dispatch_semaphore_wait 函数的返回值;
8、打印 number 的值。

如果你已经理解了前面所讲的关于信号量的三个函数的作用,那么我们就可以先来分析一下这段代码会有怎样的表现:

首先,NSLog(@"-- begin -- %@", [NSThread currentThread]); 会打印当前线程的信息,然后 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 创建了一个信号量,初始值为 0,我们创建了一个变量 number;

代码继续往下走,我们开启了异步线程任务,但不会影响当前线程的执行,继续向下, long count = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 会使信号量减 1,变为了 -1,此时 dispatch_semaphore_wait 函数无法返回, 当前线程执行到这里形成了等待;

而前边开启的子线程任务还是可以执行的:线程休眠了 2 秒,打印当前线程的信息,修改 number 的值为 10,long count = dispatch_semaphore_signal(semaphore); 的执行会使信号量加 1,因为之前信号量已经变为了 -1,此时加1 就会成功唤醒等待中的线程,所以返回值为1(返回值为非 0 代表成功唤醒了等待中的线程),下边打印会是 ”return value of dispatch_semaphore_signal: 1“;

等待的线程被唤醒,代码继续执行,此时 dispatch_semaphore_wait 函数可以返回了,并且返回值是 0(返回值为 0 代表函数执行成功),因为线程唤醒后函数成功执行而不是到了超时时间,接着会打印 ”return value of dispatch_semaphore_wait: 0“,再接着打印 ”-- end – number is now 10“,因为此时 number 的值已经在前边的线程中进行了修改。

下面我们看下运行后的打印结果:

2019-09-27 01:37:58.713507+0800 GCDSummary[1624:44221] -- begin -- <NSThread: 0x6000003d4bc0>{number = 3, name = (null)}
2019-09-27 01:38:00.714420+0800 GCDSummary[1624:44223] task -- <NSThread: 0x6000003ee680>{number = 4, name = (null)}
2019-09-27 01:38:00.714932+0800 GCDSummary[1624:44223] return value of dispatch_semaphore_signal: 1
2019-09-27 01:38:00.714939+0800 GCDSummary[1624:44221] return value of dispatch_semaphore_wait: 0
2019-09-27 01:38:00.715404+0800 GCDSummary[1624:44221] -- end -- number is now 10

可以看到,打印结果和我们预期的一模一样。


Dispatch Semaphore 的用途

Dispatch Semaphore 在实际开发中主要用于:

  • 线程同步;
  • 线程并发量控制;
  • 保证线程安全、线程加锁。

线程同步

什么是线程同步?

所谓线程同步,就是当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。

在遇到一些耗时操作或者网络请求时,我们一般会将其放入子线程中进行处理,但有时我们也需要同步使用这类操作的结果,也就是说有时我们需要将异步执行任务转换为同步执行任务,这就是线程同步。

下面是开源框架 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;
}

细心的同学可能已经发现,尽管这部分代码的内容有所改变,但是和最开始进行分析的那段代码相比,其本质相同。都是先创建一个信号量,然后进行异步操作(不会阻塞当前线程),接着是通过 dispatch_semaphore_wait 函数减少信号量使当前线程等待,等到异步操作完成时又通过 dispatch_semaphore_signal 函数增加信号量唤醒等待中的线程,接着执行 return tasks;。这样一来,就成功把异步操作中获取的结果同步在当前线程中使用。

上面所讲的例子都是将单个异步操作的结果同步使用到当前线程中,那么如果现在有多个异步操作需要执行,并且当所有操作完成后需要根据各个操作的结果来执行其他任务,这种情况下又该怎么办呢?

下面我们就通过一个例子,来模拟一下异步下载 2 张图片并将其合成后显示到屏幕上的过程:

// 异步下载 2 张图片并将其合成为 1 张图片
- (void)downloadImage1: (NSString *)url1 image2:(NSString *)url2 combinedHandler:(void (^)(UIImage *combinedImage))combinedHandler {
    NSLog(@"-- all tasks begin --");
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"start downloading image1");
        [self downloadImage:url1 completionHandler:^(UIImage *image) {
            image1 = image;
            NSLog(@"image1 finished");
            
            dispatch_semaphore_signal(semaphore);
        }];
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"start downloading image2");
        [self downloadImage:url1 completionHandler:^(UIImage *image) {
            image2 = image;
            NSLog(@"image2 finished");
            
            dispatch_semaphore_signal(semaphore);
        }];
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"-- all tasks end -- ");
        
        // 模拟图片合成过程
        [NSThread sleepForTimeInterval:2];
        if (combinedHandler) {
            combinedHandler([UIImage new]);
        }
    });
}
// 使用 block 回调的形式返回下载的图片
- (void)downloadImage:(NSString *)url completionHandler:(void(^)(UIImage * image))completionHandler {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 模拟图片下载耗费的时间
        [NSThread sleepForTimeInterval:2];
        
        if (completionHandler) {
            completionHandler([UIImage new]);
        }
    });
}

调用方法:

[self downloadImage1:@"image url 1" image2:@"image url 2" combinedHandler:^(UIImage *combinedImage) {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"在主线程中更新 UI");
    });
}];

运行后产看打印信息:

2019-09-28 21:21:12.662678+0800 GCDSummary[12552:492624] -- all tasks begin --
2019-09-28 21:21:12.662918+0800 GCDSummary[12552:492625] start downloading image2
2019-09-28 21:21:12.662924+0800 GCDSummary[12552:492623] start downloading image1
2019-09-28 21:21:14.667730+0800 GCDSummary[12552:492622] image2 finished
2019-09-28 21:21:14.667764+0800 GCDSummary[12552:492624] image1 finished
2019-09-28 21:21:14.668180+0800 GCDSummary[12552:492625] -- all tasks end --
2019-09-28 21:21:16.673244+0800 GCDSummary[12552:492583] 在主线程中更新 UI

可以看到,两个下载任务都结束后成功执行了 dispatch_group_notify 方法,在 dispatch_group_notify 方法中合成了下载的两张图片并通过 block 形式进行了回调。

下面我们分析一下代码的设计思想和执行过程:

由于要在两个下载任务都完成后再进行图片的合成操作,所以我们需要使用 dispatch_group_asyncdispatch_group_notify 两个函数,使用 dispatch_group_async 处理每一个下载任务,使用 dispatch_group_notify 处理所有任务完成后的操作。

由于 dispatch_group_async 函数中要处理的下载任务也是异步执行的,所以在图片下载完成之前,要使当前线程等待,直到图片下载后再唤醒当前线程,完成这个 dispatch_group_async 方法。

这样一来,2 张图片真正下载后才意味着 2 个 dispatch_group_async 函数执行完毕,然后触发 dispatch_group_notify 函数进行下一步操作。

1、dispatch_semaphore_create(0); 创建了一个初始值为 0 的信号量;

2、两个 dispatch_group_async 函数中先进行了图片下载请求(异步执行),然后调用了 dispatch_semaphore_wait 函数,两个 wait 函数调用的先后顺序没有影响;

3、第一个 dispatch_semaphore_wait 函数执行时,将信号量的值减 1,变为 -1,该函数等待,也就是当前线程等待(阻塞);

4、第二个 dispatch_semaphore_wait 执行时,将信号量的值减 1,变为 -2,还是负数,该函数也等待,也就是当前线程等待;

5、当其中一个异步下载图片的操作结束后,会执行 dispatch_semaphore_signal 函数,信号量的值加 1,因为前一个信号量值(-2)小于 0,所以唤醒当前线程,然后这个 dispatch_group_async 函数执行完毕;

6、同理,另一个异步下载图片的操作结束后,执行 dispatch_semaphore_signal 函数,信号量的值加 1,因为前一个信号量值(-1)小于 0,所以唤醒当前线程,然后这个 dispatch_group_async 函数执行完毕;

7、两个dispatch_group_async 函数执行完毕后,触发 dispatch_group_notify 函数,然后在子线程中合成图片并通过 block 回调的形式将图片传递出去。


线程并发量控制

有时我们需要同时执行很多个任务,例如下载 100 首歌曲,很明显我们不能同时开启 100 个子线程去执行下载任务,这时我们就可以利用信号量的特点来控制并发线程的数量。例如在下例中,我们控制线程并发量为 4:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);
        
    for (int i = 0; i < 100; i++) {
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
        dispatch_async(queue, ^{
            NSLog(@"task%d begin -- %@", i, [NSThread currentThread]);
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"task%d end", i);
                
            dispatch_semaphore_signal(semaphore);
        });
    }
});

运行后查看打印信息:

// 部分打印信息
2019-09-29 01:18:01.499670+0800 GCDSummary[13933:553670] task2 begin -- <NSThread: 0x6000011f7880>{number = 4, name = (null)}
2019-09-29 01:18:01.499660+0800 GCDSummary[13933:553672] task0 begin -- <NSThread: 0x6000011ce600>{number = 3, name = (null)}
2019-09-29 01:18:01.499701+0800 GCDSummary[13933:553673] task1 begin -- <NSThread: 0x6000011fd440>{number = 5, name = (null)}
2019-09-29 01:18:01.499712+0800 GCDSummary[13933:553683] task3 begin -- <NSThread: 0x6000011f7940>{number = 6, name = (null)}
2019-09-29 01:18:03.501472+0800 GCDSummary[13933:553672] task0 end
2019-09-29 01:18:03.501479+0800 GCDSummary[13933:553670] task2 end
2019-09-29 01:18:03.501524+0800 GCDSummary[13933:553683] task3 end
2019-09-29 01:18:03.501479+0800 GCDSummary[13933:553673] task1 end
2019-09-29 01:18:03.502126+0800 GCDSummary[13933:553672] task5 begin -- <NSThread: 0x6000011ce600>{number = 3, name = (null)}
2019-09-29 01:18:03.502126+0800 GCDSummary[13933:553673] task4 begin -- <NSThread: 0x6000011fd440>{number = 5, name = (null)}
2019-09-29 01:18:03.502129+0800 GCDSummary[13933:553683] task6 begin -- <NSThread: 0x6000011f7940>{number = 6, name = (null)}
2019-09-29 01:18:03.502311+0800 GCDSummary[13933:553684] task7 begin -- <NSThread: 0x6000011cad00>{number = 7, name = (null)}
......

可以看到,同一时间只会开启 4 个子线程执行任务,自己可以试着根据之前讲过的知识思考一下其中的原理。

保证线程安全、线程加锁

什么是线程安全?

假设现在有一段代码,会有多个线程来访问它,如果并发访问和单一线程访问的结果一样,我们就可以说这段代码是线程安全的。

一般情况下,对于一个常量或只读属性来说,它们是线程安全的;如果对于一个变量只有读操作,而没有写操作,一般认为这个变量也是线程安全的;如果同时会有多个线程修改一个变量的值,那么我们就要考虑线程同步的问题,保证线程安全。

下面我们通过模拟吃包子的行为来分析如何设计一段线程安全的代码。

假设总共有 20 个包子,有 3 个人在同时吃,每个人吃完之后自动去拿下一个,直到所有包子被吃完。

首先来看一下不考虑线程安全的示例:

// 初始化包子数量
self.surplusCount = 20; // 最开始剩余的包子的数量为 20 个
    
    // 创建 3 个线程,同时执行吃的操作
dispatch_queue_t queue1 = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue3", DISPATCH_QUEUE_SERIAL);
    
// 3 个人开始吃包子
dispatch_async(queue1, ^{
     [self eat];
});
dispatch_async(queue2, ^{
     [self eat];
});
dispatch_async(queue3, ^{
     [self eat];
});

// 模拟吃包子
- (void)eat {
    while (YES) {
        if (self.surplusCount > 0) {
            self.surplusCount--;
            NSLog(@"start eating, surplusCount is %ld", (long)self.surplusCount);
            [NSThread sleepForTimeInterval:0.1]; // 模拟吃包子(耗时操作)
        } else {
            NSLog(@"所有包子已吃完");
            break;
        }
    }
}

运行后查看打印信息:

// 部分打印信息
2019-09-29 22:58:12.865161+0800 GCDSummary[20068:769741] start eating, surplusCount is 19
2019-09-29 22:58:12.865205+0800 GCDSummary[20068:769743] start eating, surplusCount is 18
2019-09-29 22:58:12.865232+0800 GCDSummary[20068:769742] start eating, surplusCount is 17
2019-09-29 22:58:12.966000+0800 GCDSummary[20068:769741] start eating, surplusCount is 16
2019-09-29 22:58:12.966000+0800 GCDSummary[20068:769743] start eating, surplusCount is 16
2019-09-29 22:58:12.966000+0800 GCDSummary[20068:769742] start eating, surplusCount is 15

可以看到,如果不考虑线程安全,会出现一个包子被吃 2 次甚至多次的情况,这显然不符合我们的要求。

下面我们再来看一下线程安全的示例:

// 初始化包子数量
self.surplusCount = 20; // 最开始剩余的包子的数量为 20 个

// 初始化信号量
self.semaphore = dispatch_semaphore_create(1);

// 创建 3 个线程,同时执行吃的操作
dispatch_queue_t queue1 = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue3", DISPATCH_QUEUE_SERIAL);

// 3 个人开始吃包子
dispatch_async(queue1, ^{
    [self eatWithSemaphore:1];
});
dispatch_async(queue2, ^{
    [self eatWithSemaphore:2];
});
dispatch_async(queue3, ^{
    [self eatWithSemaphore:3];
});

// 利用信号量吃包子
- (void)eatWithSemaphore:(int) peopleTag {
    while (YES) {
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        if (self.surplusCount > 0) {
            self.surplusCount--;
            NSLog(@"people %d start eating, surplusCount is %ld", peopleTag, (long)self.surplusCount);
            [NSThread sleepForTimeInterval:0.1]; // 模拟吃包子(耗时操作)
        } else {
            NSLog(@"所有包子已吃完");
            dispatch_semaphore_signal(self.semaphore);
            break;
        }
        
        dispatch_semaphore_signal(self.semaphore);
    }
}

运行后查看打印信息:

2019-09-29 23:07:13.340536+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 19
2019-09-29 23:07:13.441861+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 18
2019-09-29 23:07:13.543666+0800 GCDSummary[20177:773865] people 2 start eating, surplusCount is 17
2019-09-29 23:07:13.649182+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 16
2019-09-29 23:07:13.750205+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 15
2019-09-29 23:07:13.852518+0800 GCDSummary[20177:773865] people 2 start eating, surplusCount is 14
2019-09-29 23:07:13.957716+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 13
2019-09-29 23:07:14.063026+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 12
2019-09-29 23:07:14.164350+0800 GCDSummary[20177:773865] people 2 start eating, surplusCount is 11
2019-09-29 23:07:14.266547+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 10
2019-09-29 23:07:14.369607+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 9
2019-09-29 23:07:14.471100+0800 GCDSummary[20177:773865] people 2 start eating, surplusCount is 8
2019-09-29 23:07:14.573865+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 7
2019-09-29 23:07:14.676414+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 6
2019-09-29 23:07:14.780632+0800 GCDSummary[20177:773865] people 2 start eating, surplusCount is 5
2019-09-29 23:07:14.884601+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 4
2019-09-29 23:07:14.987707+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 3
2019-09-29 23:07:15.090996+0800 GCDSummary[20177:773865] people 2 start eating, surplusCount is 2
2019-09-29 23:07:15.194781+0800 GCDSummary[20177:773866] people 1 start eating, surplusCount is 1
2019-09-29 23:07:15.297627+0800 GCDSummary[20177:773868] people 3 start eating, surplusCount is 0
2019-09-29 23:07:15.401425+0800 GCDSummary[20177:773865] 所有包子已吃完
2019-09-29 23:07:15.401726+0800 GCDSummary[20177:773866] 所有包子已吃完
2019-09-29 23:07:15.401957+0800 GCDSummary[20177:773868] 所有包子已吃完

可以看到,最后 2 个”包子“被第一个和第三个人吃掉了,最后三个人一起说”所有包子已吃完“。

如果你比较细心的话,会发现上边的打印信息显示:三个人吃包子实际上是一个人吃完后另一个人才去拿包子,直到所有包子被吃完,这其实也不完全符合我们的要求。我们期望的结果是:三个人吃包子,有快有慢,吃完的人可以立马去拿包子,相互之间不用等待,直到所有包子被吃完。那怎样才能实现这种效果呢?

我们不妨来分析一下:三个人拿包子不分先后顺序,谁吃完谁拿,每个人所吃的包子的数量也可能不相同,唯一应该限制的是包子的数量不能出现问题。这样一想的话,问题的解决办法是不是很明朗了呢?其实就是对包子数量的写操作进行保护。

下面我们来看一下升级吃包子功能后的代码:

// 初始化包子数量
self.surplusCount = 20; // 最开始剩余的包子的数量为 20 个
    
// 初始化信号量
self.semaphore = dispatch_semaphore_create(1);
    
// 创建并发线程,三个人吃包子同时执行,互不影响
dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary.eatqueue", DISPATCH_QUEUE_CONCURRENT);
    
// 3 个人开始吃包子
dispatch_async(queue, ^{
     [self peopleEat:1];
});
dispatch_async(queue, ^{
     [self peopleEat:2];
});
dispatch_async(queue, ^{
     [self peopleEat:3];
});

// 三个人吃包子耗时不一样
- (void)peopleEat:(int)people {
    while (YES) {
        if (self.surplusCount > 0) {
            // 拿包子
            [self getTheSteamedStuffedBun];
            NSLog(@"people %d start eating, surplusCount is %ld", people, (long)self.surplusCount);
            [NSThread sleepForTimeInterval:people/10.0]; // 吃包子的时间因人而异
        } else {
            NSLog(@"所有包子已吃完");
            break;
        }
    }
}
// 拿包子
- (void)getTheSteamedStuffedBun {
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    self.surplusCount--;
    dispatch_semaphore_signal(self.semaphore);
}

运行后查看打印信息:

2019-09-29 23:34:13.694082+0800 GCDSummary[20429:783801] people 3 start eating, surplusCount is 17
2019-09-29 23:34:13.694082+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 19
2019-09-29 23:34:13.694082+0800 GCDSummary[20429:783802] people 2 start eating, surplusCount is 18
2019-09-29 23:34:13.795868+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 16
2019-09-29 23:34:13.898310+0800 GCDSummary[20429:783802] people 2 start eating, surplusCount is 15
2019-09-29 23:34:13.899530+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 14
2019-09-29 23:34:13.996842+0800 GCDSummary[20429:783801] people 3 start eating, surplusCount is 13
2019-09-29 23:34:14.001296+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 12
2019-09-29 23:34:14.102689+0800 GCDSummary[20429:783802] people 2 start eating, surplusCount is 11
2019-09-29 23:34:14.106102+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 10
2019-09-29 23:34:14.208805+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 9
2019-09-29 23:34:14.299013+0800 GCDSummary[20429:783801] people 3 start eating, surplusCount is 8
2019-09-29 23:34:14.307412+0800 GCDSummary[20429:783802] people 2 start eating, surplusCount is 7
2019-09-29 23:34:14.310932+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 6
2019-09-29 23:34:14.414006+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 5
2019-09-29 23:34:14.507901+0800 GCDSummary[20429:783802] people 2 start eating, surplusCount is 4
2019-09-29 23:34:14.517497+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 3
2019-09-29 23:34:14.600811+0800 GCDSummary[20429:783801] people 3 start eating, surplusCount is 2
2019-09-29 23:34:14.621818+0800 GCDSummary[20429:783803] people 1 start eating, surplusCount is 1
2019-09-29 23:34:14.710577+0800 GCDSummary[20429:783802] people 2 start eating, surplusCount is 0
2019-09-29 23:34:14.725603+0800 GCDSummary[20429:783803] 所有包子已吃完
2019-09-29 23:34:14.901507+0800 GCDSummary[20429:783801] 所有包子已吃完
2019-09-29 23:34:14.915197+0800 GCDSummary[20429:783802] 所有包子已吃完

可以看到,第一个人吃了 10 个包子(吃一个包子耗时最短),第二个人吃了 6 个包子,第三个人吃了 4 个包子,所以说平时团建遇到想吃的东西一定要快点吃,吃完再夹下一个?。

关于信号量的原理,个人建议只需了解即可,我们应该学习的是它的思路而不是死记其代码定义,有空的童鞋不妨看看下面这篇文章:深入理解GCD之dispatch_semaphore


参考文章:
iOS 多线程:『GCD』详尽总结

 类似资料: