iOS 多线程之 GCD

前言:
以下内容是对GCD的一些总结,若有写的不合适的地方,欢迎批评指正。若有遗漏的地方欢迎下方留言,我会做及时更新。

一:关于GCD的一些概念

1.GCD概念

Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

2.GCD 任务和队列

1>任务:

1)概念
就是执行操作的意思,换句话说就是你在线程中执行的那段代码在 GCD 中是放在 block 中的*。
2)执行任务有两种方式:
同步执行(sync)和异步执行(async)
同步执行(sync)
同步添加任务到指定的队列中,在添加的任 务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行只能在当前线程中执行任务,不具备开启新线程的能力
异步执行(async)
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。

2>队列

1)概念
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
2)GCD 中两种队列
串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue)
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意:并发队列 的并发功能只有在异步(dispatch_async)函数下才有效

3. GCD 的使用

3.1 队列的创建方法、获取方法

3.1.1自己创建的队列

可以使用dispatch_queue_create来创建队列,需要传入两个参数,
第一个参数表示队列的唯一标识符,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;
第二个参数用来识别是串行队列还是并发队列。
DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列

    /*
      第一个参数表示队列的唯一标识符,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;
      第二个参数用来识别是串行队列还是并发队列。
      **********DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。
    */
    //创建串行队
    dispatch_queue_t queueSerial = dispatch_queue_create("gcd.test.queue", DISPATCH_QUEUE_SERIAL);
    //创建并发队列
    dispatch_queue_t queueConcurret = dispatch_queue_create("gcd.test.queue", DISPATCH_QUEUE_CONCURRENT);    

3.1.2主队列

主队列的获取方法:

// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

说明:
主队列由系统自动创建,并与应用程序的主线程相关联。主队列上的任务一定是在主线程上执行(不过主线程并不是只执行主队列的任务,为了避免线程切换对性能的消耗,主线程还有可能会执行其他队列的任务)。
主队列是一个串行队列,所以即便是在异步函数中也不会去开启新的线程。它只有在主线程空闲的时候才能调度里面的任务。
开发中一般是子线程执行完耗时操作后,我们获取到主队列并将刷新UI的任务添加到主队列中去执行。

案例分析:

-(void)mainQueueTest{
    NSLog(@"全局队列同步");
    for (int i = 0 ; i < 5; i++) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d------%@",i,[NSThread currentThread]);
        });
    }
}
上面程序运行会堵塞线程;
因为主队列是串行队列,如果通过同步函数向主队列中添加任务,那么并不会开启子线程来执行这个任务,而是在主线程中执行。
我们将方法mainQueueTest1称作task1,将同步函数添加的任务称作task2。task1和task2都在主队列中,主队列先安排task1到主线程执行,等task1执行了完了才能安排task2到主线程执行,而task1又必须等task2执行完了才能继续往下执行,而task2又必须等task1执行完了才能被主队列安排执行,这样就造成了相互等待而卡死(死锁)。
-(void)mainQueueTest{
    NSLog(@"主队列同步");
    for (int i = 0 ; i < 5; i++) {
        dispatch_async(dispatch_get_main_queue(), ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d------%@",i,[NSThread currentThread]);
        });
    }
}
运行结果:
2021-03-22 23:36:04.077073+0800 ios_thread[4498:62538] 全局队列同步
2021-03-22 23:36:06.129103+0800 ios_thread[4498:62538] 0------{number = 1, name = main}
2021-03-22 23:36:08.130407+0800 ios_thread[4498:62538] 1------{number = 1, name = main}
2021-03-22 23:36:10.132023+0800 ios_thread[4498:62538] 2------{number = 1, name = main}
2021-03-22 23:36:12.133623+0800 ios_thread[4498:62538] 3------{number = 1, name = main}
2021-03-22 23:36:14.135197+0800 ios_thread[4498:62538] 4------{number = 1, name = main}
说明:
在主队列中通过异步函数添加的任务都是在主线程执行
(因为主队列中的任务都在主线程执行,所以并不会开启新的线程),
并且是串行执行。

3.1.3全局队列

获取全局队列的函数是dispatch_get_global_queue(long identifier, unsigned long flags)。第一个参数表示队列的优先级(优先级等级如下表所示);第二个参数是保留参数,传0即可。全局队列是一个并发队列。
注意:这里的优先级和NSOperation中的优先级不同,这里的优先级是指队列的优先级,NSOperation中的优先级是指任务的优先级。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
-(void)globalQueueTest{
    NSLog(@"全局队列同步");
    for (int i = 0 ; i < 5; i++) {
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d------%@",i,[NSThread currentThread]);
        });
    }
}
打印结果:
2021-03-22 23:54:07.952830+0800 ios_thread[4676:73422] 全局队列同步
2021-03-22 23:54:09.954219+0800 ios_thread[4676:73422] 0------{number = 1, name = main}
2021-03-22 23:54:11.955710+0800 ios_thread[4676:73422] 1------{number = 1, name = main}
2021-03-22 23:54:13.957181+0800 ios_thread[4676:73422] 2------{number = 1, name = main}
2021-03-22 23:54:15.958770+0800 ios_thread[4676:73422] 3------{number = 1, name = main}
2021-03-22 23:54:17.960338+0800 ios_thread[4676:73422] 4------{number = 1, name = main}

3.1.4队列总结

image.png

3.2 任务的创建方法

GCD 同步执行任务:创建方法dispatch_sync
GCD异步执行任务:创建方法dispatch_async

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
});

我们根据两种任务(同步和异步)和3种执行方式(串行队列、并发队列、主队列)组合成6中不同的方式

1.同步执行 + 并发队列
2.异步执行 + 并发队列
3.同步执行 + 串行队列
4.异步执行 + 串行队列
5.同步执行 + 主队列
6.异步执行 + 主队列

接下分析一下这6中不同的组合方式

3.2.1同步执行+并发队列

#pragma mark 同步执行+并发队列
/**
 * 同步执行 + 并发队列
 * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    
    NSLog(@"end");
}
打印结果:
2021-03-23 22:45:04.439960+0800 ios_thread[1136:32399] currentThread---{number = 1, name = main}
2021-03-23 22:45:04.440112+0800 ios_thread[1136:32399] begin
2021-03-23 22:45:06.441521+0800 ios_thread[1136:32399] 1---{number = 1, name = main}
2021-03-23 22:45:08.442404+0800 ios_thread[1136:32399] 1---{number = 1, name = main}
2021-03-23 22:45:10.443892+0800 ios_thread[1136:32399] 2---{number = 1, name = main}
2021-03-23 22:45:12.445396+0800 ios_thread[1136:32399] 2---{number = 1, name = main}
2021-03-23 22:45:14.446111+0800 ios_thread[1136:32399] 3---{number = 1, name = main}
2021-03-23 22:45:16.448193+0800 ios_thread[1136:32399] 3---{number = 1, name = main}
2021-03-23 22:45:16.448513+0800 ios_thread[1136:32399] end
总结:不啰嗦了打印结果一目了然

3.2.2 异步执行 + 并发队列

#pragma mark 同步执行+并发队列
/**
 * 同步执行 + 并发队列
 * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    
    NSLog(@"end");
}
#pragma mark 异步执行并发队列
/**
 * 异步执行 + 并发队列
 * 特点:可以开启多个线程,任务交替(同时)执行。
 */
- (void)asyncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"begin");

    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 3; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 1; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"asyncConcurrent---end");
}
打印结果:
2021-03-23 22:53:23.149962+0800 ios_thread[1210:37698] currentThread---{number = 1, name = main}
2021-03-23 22:53:23.150112+0800 ios_thread[1210:37698] begin
2021-03-23 22:53:23.150273+0800 ios_thread[1210:37698] asyncConcurrent---end
2021-03-23 22:53:25.151177+0800 ios_thread[1210:37781] 1---{number = 5, name = (null)}
2021-03-23 22:53:25.151179+0800 ios_thread[1210:37783] 3---{number = 4, name = (null)}
2021-03-23 22:53:25.151269+0800 ios_thread[1210:37779] 2---{number = 6, name = (null)}
2021-03-23 22:53:27.156291+0800 ios_thread[1210:37781] 1---{number = 5, name = (null)}
2021-03-23 22:53:27.156291+0800 ios_thread[1210:37783] 3---{number = 4, name = (null)}
2021-03-23 22:53:29.161743+0800 ios_thread[1210:37781] 1---{number = 5, name = (null)}

从打印的日志可以清晰的看到,除了当前主线程,系统有开启了3个线程,并且任务是交替同时执行的。说明异步执行具备开启线程的能力,并发队列可开启多个线程同时执行多个任务。

3.2.3 同步执行 + 串行队列

#pragma mark 同步执行 + 串行队列
/**
 * 同步执行 + 串行队列
 * 特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
 */
- (void)syncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncSerial---begin");

    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"syncSerial---end");
}
打印日志:
2021-03-23 23:06:24.647668+0800 ios_thread[1257:44419] currentThread---{number = 1, name = main}
2021-03-23 23:06:24.647819+0800 ios_thread[1257:44419] syncSerial---begin
2021-03-23 23:06:26.649245+0800 ios_thread[1257:44419] 1---{number = 1, name = main}
2021-03-23 23:06:28.650779+0800 ios_thread[1257:44419] 1---{number = 1, name = main}
2021-03-23 23:06:30.652343+0800 ios_thread[1257:44419] 2---{number = 1, name = main}
2021-03-23 23:06:32.653168+0800 ios_thread[1257:44419] 2---{number = 1, name = main}
2021-03-23 23:06:34.653735+0800 ios_thread[1257:44419] 3---{number = 1, name = main}
2021-03-23 23:06:36.654308+0800 ios_thread[1257:44419] 3---{number = 1, name = main}
2021-03-23 23:06:36.654652+0800 ios_thread[1257:44419] syncSerial---end

总结:
1.所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行不具备开启新线程的能力)
2.任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)

3.2.4 异步执行 + 串行队列

#pragma mark 异步执行 + 串行队列
/**
 * 异步执行 + 串行队列
 * 特点:会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务。
 */
- (void)asyncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncSerial---begin");

    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"asyncSerial---end");
}
打印日志:
2021-03-23 23:16:44.027616+0800 ios_thread[1299:50254] currentThread---{number = 1, name = main}
2021-03-23 23:16:44.027780+0800 ios_thread[1299:50254] asyncSerial---begin
2021-03-23 23:16:44.027958+0800 ios_thread[1299:50254] asyncSerial---end
2021-03-23 23:16:46.033318+0800 ios_thread[1299:50352] 1---{number = 6, name = (null)}
2021-03-23 23:16:48.037867+0800 ios_thread[1299:50352] 1---{number = 6, name = (null)}
2021-03-23 23:16:50.038528+0800 ios_thread[1299:50352] 2---{number = 6, name = (null)}
2021-03-23 23:16:52.044250+0800 ios_thread[1299:50352] 2---{number = 6, name = (null)}
2021-03-23 23:16:54.049903+0800 ios_thread[1299:50352] 3---{number = 6, name = (null)}
2021-03-23 23:16:56.054594+0800 ios_thread[1299:50352] 3---{number = 6, name = (null)}

总结:
1.异步执行具备开启新线程的能力,串行队列只开启一个线程
(主线程:
新线程:)

2.任务是按顺序执行的

3.2.5 同步执行 + 主队列

同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。

#pragma mark 同步执行 + 主队列
/**
 * 同步执行 + 主队列
 * 特点(主线程调用):互等卡主不执行。
 * 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncMain {

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });

    NSLog(@"syncMain---end");
}
打印日志:
2021-03-23 23:26:43.957379+0800 ios_thread[1352:56626] currentThread---{number = 1, name = main}
2021-03-23 23:26:43.957592+0800 ios_thread[1352:56626] syncMain---begin
(lldb) 

总结:
1.直接崩掉,具体原因在上文的主队列案例中已经做了详细解释

3.2.6 在其他线程中调用同步执行 + 主队列

// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行
 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
- (void)syncMain {

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });

    NSLog(@"syncMain---end");
}
输出结果:
2021-03-23 23:46:33.008678+0800 ios_thread[1411:66125] currentThread---{number = 8, name = (null)}
2021-03-23 23:46:33.008840+0800 ios_thread[1411:66125] syncMain---begin
2021-03-23 23:46:35.036965+0800 ios_thread[1411:65970] 1---{number = 1, name = main}
2021-03-23 23:46:37.037517+0800 ios_thread[1411:65970] 1---{number = 1, name = main}
2021-03-23 23:46:39.039026+0800 ios_thread[1411:65970] 2---{number = 1, name = main}
2021-03-23 23:46:41.040455+0800 ios_thread[1411:65970] 2---{number = 1, name = main}
2021-03-23 23:46:41.040750+0800 ios_thread[1411:66125] syncMain---end

总结:
1.所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)
2.任务是按顺序执行的(主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)
分析:完美解决3.2.5中的问题
因为syncMain 任务放到了其他线程里,而任务1、任务2、都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2。这样就解决了卡顿死锁的问题了

3.2.7 异步执行 + 主队列

#pragma mark 异步执行 + 主队列
/**
 * 异步执行 + 主队列
 * 特点:只在主线程中执行任务,执行完一个任务,再执行下一个任务
 */
- (void)asyncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"asyncMain---end");
}
输入结果:
2021-03-23 23:57:44.857805+0800 ios_thread[1450:72181] currentThread---{number = 1, name = main}
2021-03-23 23:57:44.857960+0800 ios_thread[1450:72181] asyncMain---begin
2021-03-23 23:57:44.858116+0800 ios_thread[1450:72181] asyncMain---end
2021-03-23 23:57:46.887331+0800 ios_thread[1450:72181] 1---{number = 1, name = main}
2021-03-23 23:57:48.887700+0800 ios_thread[1450:72181] 1---{number = 1, name = main}
2021-03-23 23:57:50.889198+0800 ios_thread[1450:72181] 2---{number = 1, name = main}
2021-03-23 23:57:52.890802+0800 ios_thread[1450:72181] 2---{number = 1, name = main}

总结:
1.所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)
*2.任务是按顺序执行的(因为主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

4.GCD 线程间的通信

4.1 iOS开发中的高频操作

在 iOS 开发过程中。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,在主线程里边进行 UI 刷新,那么就用到了线程之间的通讯
案例

- (void)communication {
    // 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        // 异步追加任务
        for (int i = 0; i < 3; ++i) {
            // 模拟耗时操作 例如网络请求
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"1---%@",[NSThread currentThread]);
        }

        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务 例如刷新UI
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:2];
            // 打印当前线程
            NSLog(@"2---%@",[NSThread currentThread]);
        });
    });
}
输出结果:
2021-03-24 00:08:54.439904+0800 ios_thread[1499:79037] 1---{number = 3, name = (null)}
2021-03-24 00:08:56.445422+0800 ios_thread[1499:79037] 1---{number = 3, name = (null)}
2021-03-24 00:08:58.446310+0800 ios_thread[1499:79037] 1---{number = 3, name = (null)}
2021-03-24 00:09:00.448014+0800 ios_thread[1499:78899] 2---{number = 1, name = main}

总结:
输入结果显示:其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作

4.2 GCD 栅栏方法:dispatch_barrier_async

4.2.1.概念解释:

dispatch_barrier)栅栏函数是GCD提供的用于阻塞分割任务的一组函数。其主要作用就是在队列中设置栅栏,来人为干预队列中任务的执行顺序。也可以理解为用来设置任务之间的依赖关系。栅栏函数分同步栅栏函数和异步栅栏函数

4.2.2栅栏函数分类

同步栅栏函数和异步栅栏函数

同步栅栏函数和异步栅栏函数的异同点
** 相同点:** 它们都将多个任务分割成了3个部分,第1部分是栅栏函数之前的任务,是最先执行的;第2部分是栅栏函数添加的任务,这个任务要等栅栏函数之前的任务都执行完了才会执行;第3个部分是栅栏函数之后的任务,这个部分要等栅栏函数里面的任务执行完了才会执行
不同点: 同步栅栏函数不会开启新线程,其添加的任务在当前线程执行,会阻塞当前线程;异步栅栏函数会开启新线程来执行其添加的任务,不会阻塞当前线程

4.2.3需求案例分析

假设有这样一个需求:
一个大文件被分成part1和part2两部分存在服务器上,现在要将part1和part2都下载下来后然后合并并写入磁盘。这里其实有4个task,下载part1是task1,下载part2是task2,合并part1和part2是task3,将合并后的文件写入磁盘是task4。这4个任务执行顺序是task1和task2并发异步执行,这两个任务都执行完了后再执行task3,task3执行完了再执行task4
我们通过两种方案来实现这个需求:

4.2.3.1同步栅栏实现方式:

- (void)barrierSync {
    NSLog(@"当前线程1");
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"开始下载part1 -> %@",[NSThread currentThread]);
        //这里模拟下载操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"完成part1下载 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程2");
    dispatch_async(queue, ^{
        NSLog(@"开始下载part2 -> %@",[NSThread currentThread]);
        //这里模拟下载操作
        [NSThread sleepForTimeInterval:4];
        NSLog(@"完成part2下载 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程3");
    dispatch_barrier_sync(queue, ^{
        NSLog(@"合并part1和part2 -> %@",[NSThread currentThread]);
        //模拟合并操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"合并part1和part2合并完成 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程4");
输出结果:
2021-03-24 16:51:18.997002+0800 ios_thread[36727:332568] 当前线程1
2021-03-24 16:51:18.998320+0800 ios_thread[36727:332568] 当前线程2
2021-03-24 16:51:18.998501+0800 ios_thread[36727:332941] 开始下载part1 -> {number = 5, name = (null)}
2021-03-24 16:51:18.999153+0800 ios_thread[36727:332568] 当前线程3
2021-03-24 16:51:18.999273+0800 ios_thread[36727:333275] 开始下载part2 -> {number = 6, name = (null)}
2021-03-24 16:51:21.002392+0800 ios_thread[36727:332941] 完成part1下载 -> {number = 5, name = (null)}
2021-03-24 16:51:23.002451+0800 ios_thread[36727:333275] 完成part2下载 -> {number = 6, name = (null)}
2021-03-24 16:51:23.002885+0800 ios_thread[36727:332568] 合并part1和part2 -> {number = 1, name = main}
2021-03-24 16:51:25.003632+0800 ios_thread[36727:332568] 合并part1和part2合并完成 -> {number = 1, name = main}
2021-03-24 16:51:25.003940+0800 ios_thread[36727:332568] 当前线程4

总结:
通过运行过程(看模拟器或真机的运行状态)和运行结果可以看出,需求的功能是实现了,但是有个问题,同步栅栏函数在分割任务的同时也阻塞了当前线程,这里当前线程是主线程,这就意味着在task1、task2这2个任务都完成之前,UI界面是出于卡死状态的,这种用户体验显然是非常糟糕的。所以在开发中很理所应当的就把它pass掉了

4.2.3.2异步栅栏实现

- (void)barrierAsync {
    NSLog(@"当前线程1");
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"开始下载part1 -> %@",[NSThread currentThread]);
        //这里模拟下载操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"完成part1下载 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程2");
    dispatch_async(queue, ^{
        NSLog(@"开始下载part2 -> %@",[NSThread currentThread]);
        //这里模拟下载操作
        [NSThread sleepForTimeInterval:4];
        NSLog(@"完成part2下载 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程3");
    dispatch_barrier_async(queue, ^{
        NSLog(@"合并part1和part2 -> %@",[NSThread currentThread]);
        //模拟合并操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"合并part1和part2合并完成 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程4");
}
输出结果:
2021-03-24 16:58:59.454335+0800 ios_thread[36787:337646] 当前线程1
2021-03-24 16:58:59.454524+0800 ios_thread[36787:337646] 当前线程2
2021-03-24 16:58:59.454576+0800 ios_thread[36787:337720] 开始下载part1 -> {number = 5, name = (null)}
2021-03-24 16:58:59.454679+0800 ios_thread[36787:337646] 当前线程3
2021-03-24 16:58:59.454742+0800 ios_thread[36787:337719] 开始下载part2 -> {number = 3, name = (null)}
2021-03-24 16:58:59.454816+0800 ios_thread[36787:337646] 当前线程4
2021-03-24 16:59:01.455557+0800 ios_thread[36787:337720] 完成part1下载 -> {number = 5, name = (null)}
2021-03-24 16:59:03.455153+0800 ios_thread[36787:337719] 完成part2下载 -> {number = 3, name = (null)}
2021-03-24 16:59:03.455575+0800 ios_thread[36787:337719] 合并part1和part2 -> {number = 3, name = (null)}
2021-03-24 16:59:05.460777+0800 ios_thread[36787:337719] 合并part1和part2合并完成 -> {number = 3, name = (null)}

总结说明:
从上面运行结果可以看出,异步栅栏函数不会阻塞当前线程,也就是说UI界面并不会被卡死
特别注意:
1.全局队列对栅栏函数是不生效的,必须是自己创建的并发队列。
2.所有任务(包括栅栏函数添加的任务)都必须在同一个派发队列中,否则栅栏函数不生效。使用第三方网络框架(比如AFNetworking)进行网络请求时使用栅栏函数无效的正是因为这个原因导致。

4.3 GCD 延时执行方法:dispatch_after

案例需求: 在指定时间(例如3秒)之后执行某个任务。
可以用 GCD 的dispatch_after函数来实现。

- (void)dispatch_after {
    // 打印当前线程
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    NSLog(@"asyncMain---begin");

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2.0秒后异步追加任务代码到主队列,并开始执行
        // 打印当前线程
        NSLog(@"after---%@",[NSThread currentThread]);
    });
}
输出结果:2021-03-24 17:20:32.263404+0800 ios_thread[36954:350438] currentThread---{number = 1, name = main}
2021-03-24 17:20:32.263559+0800 ios_thread[36954:350438] asyncMain---begin
2021-03-24 17:20:34.327446+0800 ios_thread[36954:350438] after---{number = 1, name = main}

总结:
需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。

4.4GCD 一次性代码(只执行一次):dispatch_once

案例分析:
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 函数。使用dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行1次的代码(这里面默认是线程安全的)
    });
}

4.5 GCD 快速迭代方法:dispatch_apply

dispatch_apply类似for循环,会在指定的队列中多次执行任务。
dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
我们可以利用异步队列同时遍历。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个数字。

- (void)apply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"apply---begin");
    dispatch_apply(5, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}
输入结果:
2021-03-24 17:32:42.924078+0800 ios_thread[37098:358812] apply---begin
2021-03-24 17:32:42.924317+0800 ios_thread[37098:358812] 0---{number = 1, name = main}
2021-03-24 17:32:42.924320+0800 ios_thread[37098:358915] 2---{number = 3, name = (null)}
2021-03-24 17:32:42.924326+0800 ios_thread[37098:358913] 1---{number = 5, name = (null)}
2021-03-24 17:32:42.924369+0800 ios_thread[37098:358916] 3---{number = 4, name = (null)}
2021-03-24 17:32:42.924475+0800 ios_thread[37098:358812] 4---{number = 1, name = main}
2021-03-24 17:32:42.924563+0800 ios_thread[37098:358812] apply---end

总结:
从dispatch_apply相关代码执行结果中可以看出: 0~5 打印顺序不定,最后打印了 apply---end。
因为是在并发队列中异步队执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是apply---end一定在最后执行。这是因为dispatch_apply函数会等待全部任务执行完毕。

4.6 GCD 的队列组:dispatch_group

现需求如下:
某个界面需要请求banner信息和产品列表信息,等这两个接口的数据都返回后再回到主线程刷新UI。
我就直接上代码了

-(void)grop{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.thread.task", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"当前线程1");
    dispatch_group_async(group, queue, ^{
        NSLog(@"开始请求-轮播图-数据 -> %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"获取到-轮播图-数据 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程2");
    dispatch_group_async(group, queue, ^{
        NSLog(@"开始请求-列表-数据 -> %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:3];
        NSLog(@"获取到-列表-数据 -> %@",[NSThread currentThread]);
    });
    NSLog(@"当前线程3");
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"回到主线程刷新列表");
        [NSThread sleepForTimeInterval:1];
    });
    NSLog(@"当前线程4");
}
输出结果:
2021-03-24 22:58:40.009873+0800 ios_thread[1117:29526] 当前线程1
2021-03-24 22:58:40.010103+0800 ios_thread[1117:29526] 当前线程2
2021-03-24 22:58:40.010218+0800 ios_thread[1117:29607] 开始请求-轮播图-数据 -> {number = 4, name = (null)}
2021-03-24 22:58:40.010248+0800 ios_thread[1117:29526] 当前线程3
2021-03-24 22:58:40.010303+0800 ios_thread[1117:29611] 开始请求-列表-数据 -> {number = 6, name = (null)}
2021-03-24 22:58:40.010351+0800 ios_thread[1117:29526] 当前线程4
2021-03-24 22:58:42.015402+0800 ios_thread[1117:29607] 获取到-轮播图-数据 -> {number = 4, name = (null)}
2021-03-24 22:58:43.013025+0800 ios_thread[1117:29611] 获取到-列表-数据 -> {number = 6, name = (null)}
2021-03-24 22:58:43.013323+0800 ios_thread[1117:29526] 回到主线程刷新列表

说明:dispatch_group_notify监听任务组并不会阻塞当前线程。

备注:(以下不做介绍,了解就行)
1.dispatch_group_wait(暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行)
2.dispatch_group_enter* 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1*
3.dispatch_group_leave* 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1*

4.7 GCD 信号量:dispatch_semaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。Dispatch Semaphore 提供了三个函数。

1.dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
2.dispatch_semaphore_signal:发送一个信号,让信号总量加1
3.* dispatch_semaphore_wait*可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行

注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量

Dispatch Semaphore 在实际开发中主要用于:
1.保持线程同步,将异步执行任务转换为同步执行任务
2.保证线程安全,为线程加锁

4.7.1 Dispatch Semaphore 线程同步

需求分析:
我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。

#pragma mark 线程同步
/**
 * Dispatch Semaphore 实现线程同步,将异步执行任务转换为同步执行任务。
 */
- (void)semaphoreSync {

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");

    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, ^{
        // 追加任务1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程

        number = 100;

        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}
输出结果:
2021-03-24 23:28:57.053051+0800 ios_thread[1277:45210] currentThread---{number = 1, name = main}
2021-03-24 23:28:57.053201+0800 ios_thread[1277:45210] semaphore---begin
2021-03-24 23:28:59.057845+0800 ios_thread[1277:45365] 1---{number = 4, name = (null)}
2021-03-24 23:28:59.058181+0800 ios_thread[1277:45210] semaphore---end,number = 100

代码总结:
从 Dispatch Semaphore 实现线程同步的代码可以看到:
semaphore---end 是在执行完 number = 100; 之后才打印的。而且输出结果 number 为 100。这是因为异步执行不会做任何等待,可以继续执行任务。异步执行将任务1追加到队列之后,不做等待,接着执行dispatch_semaphore_wait方法。此时 semaphore == 0,当前线程进入等待状态。然后,异步任务1开始执行。任务1执行到dispatch_semaphore_signal之后,总信号量,此时 semaphore == 1,dispatch_semaphore_wait方法使总信号量减1,正在被阻塞的线程(主线程)恢复继续执行。最后打印semaphore---end,number = 100。这样就实现了线程同步,将异步执行任务转换为同步执行任务。

4.7.2Dispatch Semaphore 线程安全和线程同步(为线程加锁)

线程安全
1.如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
2.若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全
线程同步
可理解为线程 A 和 线程 B 一起配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作---------------举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)

案例场景:
总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。
下面我们通过非线程安全和线程安全两段代码加以分析比较
1.非线程安全(不使用 semaphore)

/**
 * 非线程安全:不使用 semaphore
 * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");

    self.ticketSurplusCount = 30;

    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);

    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });

    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售卖火车票(非线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {

        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }

    }
}
输出结果(部分):
2021-03-24 23:55:23.408177+0800 ios_thread[1439:59058] currentThread---{number = 1, name = main}
2021-03-24 23:55:23.408325+0800 ios_thread[1439:59058] semaphore---begin
2021-03-24 23:55:23.408621+0800 ios_thread[1439:59130] 剩余票数:28 窗口:{number = 4, name = (null)}
2021-03-24 23:55:23.408622+0800 ios_thread[1439:59133] 剩余票数:29 窗口:{number = 6, name = (null)}
2021-03-24 23:55:23.613155+0800 ios_thread[1439:59133] 剩余票数:27 窗口:{number = 6, name = (null)}
2021-03-24 23:55:23.613358+0800 ios_thread[1439:59130] 剩余票数:26 窗口:{number = 4, name = (null)}
2021-03-24 23:55:23.818865+0800 ios_thread[1439:59133] 剩余票数:25 窗口:{number = 6, name = (null)}
2021-03-24 23:55:23.819457+0800 ios_thread[1439:59130] 剩余票数:24 窗口:{number = 4, name = (null)}
2021-03-24 23:55:24.023485+0800 ios_thread[1439:59133] 剩余票数:23 窗口:{number = 6, name = (null)}
2021-03-24 23:55:24.026350+0800 ios_thread[1439:59130] 剩余票数:22 窗口:{number = 4, name = (null)}
.............
021-03-24 23:55:26.260378+0800 ios_thread[1439:59133] 剩余票数:3 窗口:{number = 6, name = (null)}
2021-03-24 23:55:26.260394+0800 ios_thread[1439:59130] 剩余票数:2 窗口:{number = 4, name = (null)}
2021-03-24 23:55:26.465636+0800 ios_thread[1439:59130] 剩余票数:1 窗口:{number = 4, name = (null)}
2021-03-24 23:55:26.465636+0800 ios_thread[1439:59133] 剩余票数:1 窗口:{number = 6, name = (null)}
2021-03-24 23:55:26.667177+0800 ios_thread[1439:59130] 所有火车票均已售完
2021-03-24 23:55:26.667221+0800 ios_thread[1439:59133] 剩余票数:0 窗口:{number = 6, name = (null)}
2021-03-24 23:55:26.870117+0800 ios_thread[1439:59133] 所有火车票均已售完

总结:
可以看到在不考虑线程安全,不使用 semaphore 的情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。

2. 线程安全(使用 semaphore 加锁)

#pragma mark 线程安全:使用 semaphore
/**
 * 线程安全:使用 semaphore 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");

    self.semaphoreLock = dispatch_semaphore_create(1);

    self.ticketSurplusCount = 50;

    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.thread.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.thread.testQueue2", DISPATCH_QUEUE_SERIAL);

    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });

    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(self.semaphoreLock, DISPATCH_TIME_FOREVER);

        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");

            // 相当于解锁
            dispatch_semaphore_signal(self.semaphoreLock);
            break;
        }

        // 相当于解锁
        dispatch_semaphore_signal(self.semaphoreLock);
    }
}
输出结果:
2021-03-25 00:01:39.161931+0800 ios_thread[1487:63043] currentThread---{number = 1, name = main}
2021-03-25 00:01:39.162083+0800 ios_thread[1487:63043] semaphore---begin
2021-03-25 00:01:39.162360+0800 ios_thread[1487:63102] 剩余票数:49 窗口:{number = 3, name = (null)}
2021-03-25 00:01:39.367677+0800 ios_thread[1487:63100] 剩余票数:48 窗口:{number = 5, name = (null)}
2021-03-25 00:01:39.570486+0800 ios_thread[1487:63102] 剩余票数:47 窗口:{number = 3, name = (null)}
2021-03-25 00:01:39.774759+0800 ios_thread[1487:63100] 剩余票数:46 窗口:{number = 5, name = (null)}
2021-03-25 00:01:39.978508+0800 ios_thread[1487:63102] 剩余票数:45 窗口:{number = 3, name = (null)}
2021-03-25 00:01:40.181938+0800 ios_thread[1487:63100] 剩余票数:44 窗口:{number = 5, name = (null)}
2021-03-25 00:01:40.384955+0800 ios_thread[1487:63102] 剩余票数:43 窗口:{number = 3, name = (null)}
2021-03-25 00:01:40.590373+0800 ios_thread[1487:63100] 剩余票数:42 窗口:{number = 5, name = (null)}
2021-03-25 00:01:40.795111+0800 ios_thread[1487:63102] 剩余票数:41 窗口:{number = 3, name = (null)}
2021-03-25 00:01:40.997988+0800 ios_thread[1487:63100] 剩余票数:40 窗口:{number = 5, name = (null)}
2021-03-25 00:01:41.201602+0800 ios_thread[1487:63102] 剩余票数:39 窗口:{number = 3, name = (null)}
2021-03-25 00:01:41.403678+0800 ios_thread[1487:63100] 剩余票数:38 窗口:{number = 5, name = (null)}
2021-03-25 00:01:41.606723+0800 ios_thread[1487:63102] 剩余票数:37 窗口:{number = 3, name = (null)}
2021-03-25 00:01:41.811657+0800 ios_thread[1487:63100] 剩余票数:36 窗口:{number = 5, name = (null)}
2021-03-25 00:01:42.012513+0800 ios_thread[1487:63102] 剩余票数:35 窗口:{number = 3, name = (null)}
2021-03-25 00:01:42.213897+0800 ios_thread[1487:63100] 剩余票数:34 窗口:{number = 5, name = (null)}
2021-03-25 00:01:42.417450+0800 ios_thread[1487:63102] 剩余票数:33 窗口:{number = 3, name = (null)}
2021-03-25 00:01:42.617871+0800 ios_thread[1487:63100] 剩余票数:32 窗口:{number = 5, name = (null)}
2021-03-25 00:01:42.823405+0800 ios_thread[1487:63102] 剩余票数:31 窗口:{number = 3, name = (null)}
2021-03-25 00:01:43.028284+0800 ios_thread[1487:63100] 剩余票数:30 窗口:{number = 5, name = (null)}
2021-03-25 00:01:43.233293+0800 ios_thread[1487:63102] 剩余票数:29 窗口:{number = 3, name = (null)}
2021-03-25 00:01:43.438096+0800 ios_thread[1487:63100] 剩余票数:28 窗口:{number = 5, name = (null)}
2021-03-25 00:01:43.639584+0800 ios_thread[1487:63102] 剩余票数:27 窗口:{number = 3, name = (null)}
2021-03-25 00:01:43.845104+0800 ios_thread[1487:63100] 剩余票数:26 窗口:{number = 5, name = (null)}
2021-03-25 00:01:44.047000+0800 ios_thread[1487:63102] 剩余票数:25 窗口:{number = 3, name = (null)}
2021-03-25 00:01:44.252575+0800 ios_thread[1487:63100] 剩余票数:24 窗口:{number = 5, name = (null)}
2021-03-25 00:01:44.455481+0800 ios_thread[1487:63102] 剩余票数:23 窗口:{number = 3, name = (null)}
2021-03-25 00:01:44.661001+0800 ios_thread[1487:63100] 剩余票数:22 窗口:{number = 5, name = (null)}
2021-03-25 00:01:44.865401+0800 ios_thread[1487:63102] 剩余票数:21 窗口:{number = 3, name = (null)}
2021-03-25 00:01:45.067629+0800 ios_thread[1487:63100] 剩余票数:20 窗口:{number = 5, name = (null)}
2021-03-25 00:01:45.273211+0800 ios_thread[1487:63102] 剩余票数:19 窗口:{number = 3, name = (null)}
2021-03-25 00:01:45.478097+0800 ios_thread[1487:63100] 剩余票数:18 窗口:{number = 5, name = (null)}
2021-03-25 00:01:45.682950+0800 ios_thread[1487:63102] 剩余票数:17 窗口:{number = 3, name = (null)}
2021-03-25 00:01:45.885351+0800 ios_thread[1487:63100] 剩余票数:16 窗口:{number = 5, name = (null)}
2021-03-25 00:01:46.088975+0800 ios_thread[1487:63102] 剩余票数:15 窗口:{number = 3, name = (null)}
2021-03-25 00:01:46.294408+0800 ios_thread[1487:63100] 剩余票数:14 窗口:{number = 5, name = (null)}
2021-03-25 00:01:46.497893+0800 ios_thread[1487:63102] 剩余票数:13 窗口:{number = 3, name = (null)}
2021-03-25 00:01:46.701095+0800 ios_thread[1487:63100] 剩余票数:12 窗口:{number = 5, name = (null)}
2021-03-25 00:01:46.901881+0800 ios_thread[1487:63102] 剩余票数:11 窗口:{number = 3, name = (null)}
2021-03-25 00:01:47.106582+0800 ios_thread[1487:63100] 剩余票数:10 窗口:{number = 5, name = (null)}
2021-03-25 00:01:47.311287+0800 ios_thread[1487:63102] 剩余票数:9 窗口:{number = 3, name = (null)}
2021-03-25 00:01:47.513746+0800 ios_thread[1487:63100] 剩余票数:8 窗口:{number = 5, name = (null)}
2021-03-25 00:01:47.714846+0800 ios_thread[1487:63102] 剩余票数:7 窗口:{number = 3, name = (null)}
2021-03-25 00:01:47.918548+0800 ios_thread[1487:63100] 剩余票数:6 窗口:{number = 5, name = (null)}
2021-03-25 00:01:48.121033+0800 ios_thread[1487:63102] 剩余票数:5 窗口:{number = 3, name = (null)}
2021-03-25 00:01:48.322008+0800 ios_thread[1487:63100] 剩余票数:4 窗口:{number = 5, name = (null)}
2021-03-25 00:01:48.527011+0800 ios_thread[1487:63102] 剩余票数:3 窗口:{number = 3, name = (null)}
2021-03-25 00:01:48.728485+0800 ios_thread[1487:63100] 剩余票数:2 窗口:{number = 5, name = (null)}
2021-03-25 00:01:48.933886+0800 ios_thread[1487:63102] 剩余票数:1 窗口:{number = 3, name = (null)}
2021-03-25 00:01:49.139563+0800 ios_thread[1487:63100] 剩余票数:0 窗口:{number = 5, name = (null)}
2021-03-25 00:01:49.340256+0800 ios_thread[1487:63102] 所有火车票均已售完
2021-03-25 00:01:49.340603+0800 ios_thread[1487:63100] 所有火车票均已售完

总结分析
可以看出,在考虑了线程安全的情况下,使用 dispatch_semaphore 机制之后,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。

4.8定时器的使用

话不多说了直接看代码吧

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.codeButton];
    [self startCountDownAction];
   
}

- (void)startCountDownAction{
    NSLog(@"倒计时功能");
    //开始倒计时
    __block NSInteger time = 59; //倒计时时间
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); //每秒执行
    
    dispatch_source_set_event_handler(_timer, ^{
        
        if(time <= 0){ //倒计时结束,关闭
            
            dispatch_source_cancel(_timer);
            dispatch_async(dispatch_get_main_queue(), ^{
                
                //设置按钮的样式
                [self.codeButton setTitle:@"重新发送" forState:UIControlStateNormal];
                [self.codeButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
                self.codeButton.userInteractionEnabled = YES;
            });
            
        }else{
            
            int seconds = time % 60;
            dispatch_async(dispatch_get_main_queue(), ^{
                
                //设置按钮显示读秒效果
                [self.codeButton setTitle:[NSString stringWithFormat:@"%.2ds后重新发送", seconds] forState:UIControlStateNormal];
                [self.codeButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
                self.codeButton.userInteractionEnabled = NO;
            });
            time--;
        }
    });
    dispatch_resume(_timer);
    
}

-(UIButton *)codeButton{
    if(!_codeButton){
        _codeButton = [UIButton buttonWithbuttonType:UIButtonTypeCustom andText:@"获取验证码" andTextAlignment:UIControlContentHorizontalAlignmentCenter andTextColor:[UIColor whiteColor] andTextFont:13 andBackgroungColor:[UIColor grayColor] cornerRadiusSize:2 borderWidth:1 borderColor:[UIColor blackColor] abscissa:SCREEN_WIDTH/2-70 ordinate:SCREEN_HEIGHT/2-25 width:140 height:50];
        [_codeButton addTarget:self action:@selector(clickCountDownAction:) forControlEvents:UIControlEventTouchUpOutside];
    }
    return _codeButton;
}
-(void)clickCountDownAction:(UIButton *)sender{
    [self startCountDownAction];
}

最后总结
这是关于GCD内容的一些总结,希望对需要了解GCD的小伙伴一些帮助,一些内容还没添加,后续会慢慢更新此内容

你可能感兴趣的:(iOS 多线程之 GCD)