『iOS开发』多线程开发之 GCD

666 阅读12分钟

GCD 多线程

同步任务、异步任务

两者的区别在于:

  • 同步任务需要等待任务执行完之后才返回
  • 异步任务无需等待任务执行完,直接返回

苹果的官方文档也有描述

同步任务

Submits a block object for execution and returns after that block finishes executing.

异步任务

Submits a block for asynchronous execution on a dispatch queue and returns immediately.

串行队列、并发队列

两者的区别在于:

  • 串行队列中的任务顺序执行,上一个任务执行完后,下一个任务才接着执行,遵循FIFO(先进先出)的原则
  • 并发队列中的任务可以并发执行。

两种任务 + 两种队列组合使用

任务/队列串行队列并发队列主队列(串行队列)
同步不会开启子线程,任务串行执行(当前队列中嵌套添加一个同步任务会造成死锁)不会开启子线程,任务串行执行主线程调会造成死锁,其他线程调用不会
异步会开启子线程,任务串行执行会开启子线程,任务并发执行不会开启子线程,任务在主线程串行执行。

同步 + 串行

- (void)syncSerrialTask {
    // 同步任务 + 串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.thatisawesome.demo", DISPATCH_QUEUE_SERIAL);
    
    [self runTaskWithNum:1];
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:2];
    });
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:3];
    });
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:4];
    });
    
    [self runTaskWithNum:5];
}


- (void)runTaskWithNum:(NSUInteger)num {
    NSLog(@"start execute task %lu on thread %@", num, [NSThread currentThread]);
    sleep(2);
    NSLog(@"end   execute task %lu on thread %@", num, [NSThread currentThread]);
}

运行结果:

2022-01-11 14:07:54.137625+0800 Demo[46700:394383] start execute task 1 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:07:56.139009+0800 Demo[46700:394383] end   execute task 1 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:07:56.139288+0800 Demo[46700:394383] start execute task 2 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:07:58.140514+0800 Demo[46700:394383] end   execute task 2 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:07:58.140797+0800 Demo[46700:394383] start execute task 3 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:08:00.141964+0800 Demo[46700:394383] end   execute task 3 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:08:00.142212+0800 Demo[46700:394383] start execute task 4 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:08:02.143410+0800 Demo[46700:394383] end   execute task 4 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:08:02.143661+0800 Demo[46700:394383] start execute task 5 on thread <NSThread: 0x60000192c600>{number = 1, name = main}
2022-01-11 14:08:04.144890+0800 Demo[46700:394383] end   execute task 5 on thread <NSThread: 0x60000192c600>{number = 1, name = main}

从结果可以看出,任务串行执行,没有开启新的线程。

同步 + 并发

- (void)syncConcurrentTask {
    dispatch_queue_t queue = dispatch_queue_create("com.thatisawesome.demo", DISPATCH_QUEUE_CONCURRENT);
    
    [self runTaskWithNum:1];
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:2];
    });
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:3];
    });
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:4];
    });
    
    [self runTaskWithNum:5];
}

运行结果:

2022-01-11 14:11:42.240424+0800 Demo[46833:397762] start execute task 1 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:44.241164+0800 Demo[46833:397762] end   execute task 1 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:44.241326+0800 Demo[46833:397762] start execute task 2 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:46.242443+0800 Demo[46833:397762] end   execute task 2 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:46.242588+0800 Demo[46833:397762] start execute task 3 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:48.243767+0800 Demo[46833:397762] end   execute task 3 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:48.244037+0800 Demo[46833:397762] start execute task 4 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:50.245273+0800 Demo[46833:397762] end   execute task 4 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:50.245550+0800 Demo[46833:397762] start execute task 5 on thread <NSThread: 0x600002af0700>{number = 1, name = main}
2022-01-11 14:11:52.246746+0800 Demo[46833:397762] end   execute task 5 on thread <NSThread: 0x600002af0700>{number = 1, name = main}

任务串行执行,没有开启新的线程。

异步 + 串行

- (void)asyncSerrialTask {
    dispatch_queue_t queue = dispatch_queue_create("com.thatisawesome.demo", DISPATCH_QUEUE_SERIAL);
    
    [self runTaskWithNum:1];
    
    dispatch_async(queue, ^{
        [self runTaskWithNum:2];
    });
    
    dispatch_async(queue, ^{
        [self runTaskWithNum:3];
    });
    
    dispatch_async(queue, ^{
        [self runTaskWithNum:4];
    });
    
    [self runTaskWithNum:5];
}

运行结果:

2022-01-11 14:15:01.846326+0800 Demo[46949:400738] start execute task 1 on thread <NSThread: 0x60000204c180>{number = 1, name = main}
2022-01-11 14:15:03.847623+0800 Demo[46949:400738] end   execute task 1 on thread <NSThread: 0x60000204c180>{number = 1, name = main}
2022-01-11 14:15:03.847914+0800 Demo[46949:400738] start execute task 5 on thread <NSThread: 0x60000204c180>{number = 1, name = main}
2022-01-11 14:15:03.847950+0800 Demo[46949:400839] start execute task 2 on thread <NSThread: 0x600002031f80>{number = 3, name = (null)}
2022-01-11 14:15:05.849427+0800 Demo[46949:400738] end   execute task 5 on thread <NSThread: 0x60000204c180>{number = 1, name = main}
2022-01-11 14:15:05.851383+0800 Demo[46949:400839] end   execute task 2 on thread <NSThread: 0x600002031f80>{number = 3, name = (null)}
2022-01-11 14:15:05.851655+0800 Demo[46949:400839] start execute task 3 on thread <NSThread: 0x600002031f80>{number = 3, name = (null)}
2022-01-11 14:15:07.856812+0800 Demo[46949:400839] end   execute task 3 on thread <NSThread: 0x600002031f80>{number = 3, name = (null)}
2022-01-11 14:15:07.856949+0800 Demo[46949:400839] start execute task 4 on thread <NSThread: 0x600002031f80>{number = 3, name = (null)}
2022-01-11 14:15:09.862166+0800 Demo[46949:400839] end   execute task 4 on thread <NSThread: 0x600002031f80>{number = 3, name = (null)}

1,5 号任务在主线程串行执行,2,3,4 号任务在子线程执行,且是串行执行。从结果可以看出创建了新的线程。

异步 + 并发

- (void)asyncConcurrentTask {
    dispatch_queue_t queue = dispatch_queue_create("com.thatisawesome.demo", DISPATCH_QUEUE_CONCURRENT);
    
    [self runTaskWithNum:1];
    
    dispatch_async(queue, ^{
        [self runTaskWithNum:2];
    });
    
    dispatch_async(queue, ^{
        [self runTaskWithNum:3];
    });
    
    dispatch_async(queue, ^{
        [self runTaskWithNum:4];
    });
    
    [self runTaskWithNum:5];
}

运行结果:

2022-01-11 14:20:19.425785+0800 Demo[47168:405302] start execute task 1 on thread <NSThread: 0x6000020384c0>{number = 1, name = main}
2022-01-11 14:20:21.426931+0800 Demo[47168:405302] end   execute task 1 on thread <NSThread: 0x6000020384c0>{number = 1, name = main}
2022-01-11 14:20:21.427064+0800 Demo[47168:405302] start execute task 5 on thread <NSThread: 0x6000020384c0>{number = 1, name = main}
2022-01-11 14:20:21.427119+0800 Demo[47168:405392] start execute task 2 on thread <NSThread: 0x60000205b440>{number = 5, name = (null)}
2022-01-11 14:20:21.427143+0800 Demo[47168:405394] start execute task 4 on thread <NSThread: 0x600002069540>{number = 3, name = (null)}
2022-01-11 14:20:21.427142+0800 Demo[47168:405395] start execute task 3 on thread <NSThread: 0x60000207e380>{number = 4, name = (null)}
2022-01-11 14:20:23.428279+0800 Demo[47168:405302] end   execute task 5 on thread <NSThread: 0x6000020384c0>{number = 1, name = main}
2022-01-11 14:20:23.431917+0800 Demo[47168:405394] end   execute task 4 on thread <NSThread: 0x600002069540>{number = 3, name = (null)}
2022-01-11 14:20:23.432210+0800 Demo[47168:405392] end   execute task 2 on thread <NSThread: 0x60000205b440>{number = 5, name = (null)}
2022-01-11 14:20:23.432252+0800 Demo[47168:405395] end   execute task 3 on thread <NSThread: 0x60000207e380>{number = 4, name = (null)}

1,5 号任务在主线程串行执行,2,3,4 号任务在子线程执行,且是并发执行。从结果可以看出创建了新的线程。

当前串行队列嵌套加入同步任务导致死锁

- (void)syncSerrialTask {
    // 同步任务 + 串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.thatisawesome.demo", DISPATCH_QUEUE_SERIAL);
    
    [self runTaskWithNum:1];
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:2];
    });
    
    dispatch_sync(queue, ^{
        [self runTaskWithNum:3];
        dispatch_sync(queue, ^{
            [self runTaskWithNum:4];
        });
    });
    
    [self runTaskWithNum:5];
}

运行结果:

gcd_serrial_crash

造成死锁,运行时崩溃。

造成死锁的原因分析

dispatch_sync(queue, ^{
        [self runTaskWithNum:3];
        dispatch_sync(queue, ^{
            [self runTaskWithNum:4];
        });
    });

代码中,任务 3 和 4 被当做一个整体同步加入到了串行队列中,之后又将任务 4 又被同步加入到串行队列,最后导致的就是: 任务 4 如果想要被执行,则之前加入到队列中的任务 3、4 作为一个整体的任务必须先执行完,但任务 3、 4 作为一个整体本身又包含一个任务 4,最后导致的结果就是互相等待,造成死锁。

主线程中调用同步+主队列也会触发死锁

原因是因为主线程的任务放在主队列中,主队列本身也是个串行队列,所以跟上文中的例子一样,向当前队列中嵌套加入了同步任务,造成了死锁。

线程同步

假设现在有 N 个苹果,两只猴子,一只猴子每次吃 2 个苹果,一只猴子每次吃 3 个苹果,两只猴子同时吃,知道剩余的苹果不够任意一只猴子吃时结束,如何用多线程来模拟这个过程。这是笔者在面试阿里时遇到的一道笔试题。

不考虑线程安全问题

@interface MonkeyEatApple ()

@property (nonatomic, assign) NSInteger appleCount;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;

@end

@implementation MonkeyEatApple

- (instancetype)initWithAppleCount:(NSInteger)appleCount {
    if (self = [super init]) {
        _appleCount = appleCount;
    }
    return self;
}

- (void)startEat {
    
    dispatch_queue_t queue1 = dispatch_queue_create("club.thatisawesome.queue1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("club.thatisawesome.queue2", DISPATCH_QUEUE_SERIAL);
    
    self.semaphore = dispatch_semaphore_create(1);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf eatAppleWithCount:2];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf eatAppleWithCount:3];
    });
}

- (void)eatAppleWithCount:(NSInteger)appleCount {
    while (YES) {
        NSString *monkeyStr = @"A";
        if (appleCount == 2) {
            monkeyStr = @"B";
        }
        if (self.appleCount >= 2) {
            [NSThread sleepForTimeInterval:0.5];
            self.appleCount -= appleCount;
            NSLog(@"%@ 猴子吃了 %ld 个苹果, 还剩 %ld 个苹果", monkeyStr, appleCount, self.appleCount);
        } else {
NSLog(@"%@ 想吃,但没得吃了", monkeyStr);
            break;
        }
    }
}

@end

输出结果

2022-01-12 23:04:04.913983+0800 Demo[93179:739398] B 猴子吃了 2 个苹果, 还剩 13 个苹果
2022-01-12 23:04:04.913984+0800 Demo[93179:739397] A 猴子吃了 3 个苹果, 还剩 12 个苹果
2022-01-12 23:04:05.418122+0800 Demo[93179:739398] B 猴子吃了 2 个苹果, 还剩 10 个苹果
2022-01-12 23:04:05.419167+0800 Demo[93179:739397] A 猴子吃了 3 个苹果, 还剩 7 个苹果
2022-01-12 23:04:05.923410+0800 Demo[93179:739397] A 猴子吃了 3 个苹果, 还剩 4 个苹果
2022-01-12 23:04:05.923410+0800 Demo[93179:739398] B 猴子吃了 2 个苹果, 还剩 5 个苹果
2022-01-12 23:04:06.428653+0800 Demo[93179:739398] B 猴子吃了 2 个苹果, 还剩 3 个苹果
2022-01-12 23:04:06.428653+0800 Demo[93179:739397] A 猴子吃了 3 个苹果, 还剩 2 个苹果
2022-01-12 23:04:06.933991+0800 Demo[93179:739398] B 猴子吃了 2 个苹果, 还剩 0 个苹果
2022-01-12 23:04:06.933991+0800 Demo[93179:739397] A 猴子吃了 3 个苹果, 还剩 -1 个苹果
2022-01-12 23:04:06.934244+0800 Demo[93179:739398] A 想吃,但没得吃了
2022-01-12 23:04:06.934282+0800 Demo[93179:739397] B 想吃,但没得吃了

从输出可以看出来,在不考虑线程同步的情况下,数据会出现错乱,一共 15 个苹果,B 猴子吃了 2 个之后还剩余 13 个, A 猴子又吃了 3 个,本应该剩余 10 个,但输出显示还剩余 12 个。

这里笔者把苹果总数设置成 6,观察输出结果

2022-01-13 00:03:07.904433+0800 Demo[95516:779892] A 猴子吃了 3 个苹果, 还剩 3 个苹果
2022-01-13 00:03:07.904433+0800 Demo[95516:779890] B 猴子吃了 2 个苹果, 还剩 4 个苹果
2022-01-13 00:03:08.409648+0800 Demo[95516:779890] B 猴子吃了 2 个苹果, 还剩 1 个苹果
2022-01-13 00:03:08.409650+0800 Demo[95516:779892] A 猴子吃了 3 个苹果, 还剩 0 个苹果
2022-01-13 00:03:08.409862+0800 Demo[95516:779892] A 想吃,但没得吃了
2022-01-13 00:03:08.409860+0800 Demo[95516:779890] B 想吃,但没得吃了
thread_sync_not_safe

上图即是对输出结果的一种详细说明,显然在线程不做同步的情况下,数据发生错乱。

使用信号量做线程同步

- (void)eatAppleWithCount:(NSInteger)appleCount {
    while (YES) {
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        NSString *monkeyStr = @"A";
        if (appleCount == 2) {
            monkeyStr = @"B";
        }
        if (self.appleCount >= 2) {
            [NSThread sleepForTimeInterval:0.5];
            self.appleCount -= appleCount;
            NSLog(@"%@ 猴子吃了 %ld 个苹果, 还剩 %ld 个苹果", monkeyStr, appleCount, self.appleCount);
        } else {
            dispatch_semaphore_signal(self.semaphore);
            NSLog(@"%@ 想吃,但没得吃了", monkeyStr);
            break;
        }
        dispatch_semaphore_signal(self.semaphore);
    }
}

输出结果

2022-01-13 00:39:28.969801+0800 Demo[96939:804913] B 猴子吃了 2 个苹果, 还剩 4 个苹果
2022-01-13 00:39:29.475415+0800 Demo[96939:804914] A 猴子吃了 3 个苹果, 还剩 1 个苹果
2022-01-13 00:39:29.475550+0800 Demo[96939:804913] B 想吃,但没得吃了
2022-01-13 00:39:29.475571+0800 Demo[96939:804914] A 想吃,但没得吃了
thread_sync_safe

文章首发于个人博客 点击跳转