GCD

原文:https://www.jianshu.com/p/2d57c72016c6

概念

Grand Central Dispatch(GCD)是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。

优点

  • GCD 可用于多核的并行运算
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

任务和队列

任务

任务就是执行操作的意思,换句话说就是线程内执行的代码,在 GCD 中放在 block 里。
执行任务的方式有两种:同步执行(sync)和异步执行(async),两者的主要区别是:是否等待队列里的任务结束以及是否具备开启新线程的能力

  • 同步执行(sync)
    同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async)
    异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力。

注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(下面会讲)。

队列

这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
GCD 中的任务有两种:串行队列并发队列,两者都符合先入先出原则,区别在于:执行顺序不同和开启线程数不同。

  • 串行队列(Serial Dispatch Queue)
    只开启一个线程,每次只执行一个任务,执行完这个任务继续下一个任务。
  • 并发队列(Concurrent Dispatch Queue)
    可以在多个线程中,让多个任务并发(同时)执行。注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效

两个重点

队列与线程的关系:

主队列(main_queue)就是在主线程中的默认队列。并发队列(global_queue)就是新开辟线程中的队列。
一个线程中可以有多个串行的队列,但是无法拥有多个并发的队列,多个并发的队列会放在多个线程中。例如可以自定义队列加到主线程中同步执行,中断主队列任务执行完自定义队列中任务后继续执行主队列任务。

异步与并发的区别:

异步,是指执行顺序上,区别于同步的由上至下,异步会延后执行异步内的任务。而并发是指时间上的,不同于串行的等待,并发会同时执行并发队列中的任务。

GCD的使用步骤

GCD的使用有两步,第一步创建一个队列(串行队列或者并行队列),第二步将任务加入到等待队列中。

创建队列

通常使用dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)来创建队列。第一个参数表示队列的唯一标识符,通常用逆序全程域名;第二个参数用来识别是串行队列还是并发队列:DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。

//创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.dispatch.cp", DISPATCH_QUEUE_SERIAL);
//创建并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.dispatch.cp", DISPATCH_QUEUE_CONCURRENT);
获取队列

对于串行队列,GCD 提供了一种默认的特殊的串行队列:主队列(Main Dispatch Queue)
所有放在主队列中的任务,都会放到主线程中执行。可使用 dispatch_get_main_queue() 获得主队列。

对于并发队列,GCD 供了一种默认的特殊的并发队列:全局并发队列(Global Dispatch Queue)
可以使用 dispatch_get_global_queue 来获取。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可。

//获取默认串行队列(主队列)
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//获取默认并发队列 (全局并发队列)
dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
创建任务

GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async

//创建同步任务
dispatch_sync(<#dispatch_queue_t  _Nonnull queue#>, ^{
    //这里放任务的执行方法
})
    
//创建异步任务
dispatch_async(<#dispatch_queue_t  _Nonnull queue#>, ^{
    //这里放任务的执行方法
})

虽然使用 GCD 只需两步,但是既然我们有两种队列(串行队列/并发队列),两种任务执行方式(同步执行/异步执行),那么我们就有了四种不同的组合方式。实际上,刚才还说了两种特殊队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是主队列因为有点特殊,所以我们就又多了两种组合方式。这样就有六种不同的组合方式了。

  • 同步执行 + 并发队列
    无效。同步无法开启新线程,不能实现并发。
  • 异步执行 + 并发队列
    多线程的常用模式,异步并发,多线程同时进行多个任务。
  • 同步执行 + 串行队列
    普通串行,没有用到多线程。
  • 异步执行 + 串行队列
    多线程的常用模式,异步串行,新建一个线程串行执行多个任务。
  • 同步执行 + 主队列
    无效。新建任务需等待主队列任务结束,主队列后续任务需等待新建任务,互相等待,代码卡死崩溃。
  • 异步执行 + 主队列
    类似异步串行,只是没有新开辟线程,在主线程实现异步。

GCD六种组合下的情况

1.同步执行 + 并发队列

代码:

- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]); //打印当前线程
    NSLog(@"syncConcurrent---begin");
    //创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.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(@"syncConcurrent---end");
    
}

log:

2018-04-08 15:07:13.433670+0800 应用加载时间的优化[5701:894068] currentThread---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:13.434007+0800 应用加载时间的优化[5701:894068] syncConcurrent---begin
2018-04-08 15:07:15.436162+0800 应用加载时间的优化[5701:894068] 任务1---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:17.437191+0800 应用加载时间的优化[5701:894068] 任务1---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:19.438426+0800 应用加载时间的优化[5701:894068] 任务2---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:21.439689+0800 应用加载时间的优化[5701:894068] 任务2---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:23.441261+0800 应用加载时间的优化[5701:894068] 任务3---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:25.442858+0800 应用加载时间的优化[5701:894068] 任务3---<NSThread: 0x600000076180>{number = 1, name = main}
2018-04-08 15:07:25.443222+0800 应用加载时间的优化[5701:894068] syncConcurrent---end

可以看到线程数一直为1,当前线程一直为主线程
总结:并发队列本身不能创建线程,而同步任务同样没有创建线程。因此一直为主线程同步效果,没有实现并发。

2.异步执行 + 并发队列

代码:

- (void)asyncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"asyncConcurrent---begin");
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    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(@"asyncConcurrent---end");
}

log:

2018-04-08 22:01:41.497508+0800 应用加载时间的优化[6217:973574] currentThread---<NSThread: 0x604000078780>{number = 1, name = main}
2018-04-08 22:01:41.497657+0800 应用加载时间的优化[6217:973574] asyncConcurrent---begin
2018-04-08 22:01:41.498239+0800 应用加载时间的优化[6217:973574] asyncConcurrent---end
2018-04-08 22:01:44.622924+0800 应用加载时间的优化[6217:973669] 3---<NSThread: 0x600000277200>{number = 4, name = (null)}
2018-04-08 22:01:44.622929+0800 应用加载时间的优化[6217:973666] 2---<NSThread: 0x604000463440>{number = 3, name = (null)}
2018-04-08 22:01:46.624147+0800 应用加载时间的优化[6217:973667] 1---<NSThread: 0x604000461f40>{number = 5, name = (null)}
2018-04-08 22:01:46.624234+0800 应用加载时间的优化[6217:973666] 2---<NSThread: 0x604000463440>{number = 3, name = (null)}
2018-04-08 22:01:46.624266+0800 应用加载时间的优化[6217:973669] 3---<NSThread: 0x600000277200>{number = 4, name = (null)}
2018-04-08 22:01:48.626508+0800 应用加载时间的优化[6217:973667] 1---<NSThread: 0x604000461f40>{number = 5, name = (null)}

总结:可以看到开辟了四个线程(不知道为什么不是三个),任务交替/同时进行。异步拥有开启新线程的能力,并发执行任务。第一时间在主线程中执行了syncConcurrent---beginsyncConcurrent---end ,然后在开启的三个线程中同时执行自定义的打印任务,然后在延迟三秒钟后再同时打印第二次。

3.同步执行 + 串行队列

代码:

- (void)syncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"syncSerial---begin");
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.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");
}

log:

2018-04-08 22:27:00.564340+0800 应用加载时间的优化[6278:994824] currentThread---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:00.564452+0800 应用加载时间的优化[6278:994824] syncSerial---begin
2018-04-08 22:27:02.565885+0800 应用加载时间的优化[6278:994824] 1---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:04.566469+0800 应用加载时间的优化[6278:994824] 1---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:06.568109+0800 应用加载时间的优化[6278:994824] 2---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:08.569092+0800 应用加载时间的优化[6278:994824] 2---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:10.569759+0800 应用加载时间的优化[6278:994824] 3---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:12.571093+0800 应用加载时间的优化[6278:994824] 3---<NSThread: 0x604000078d40>{number = 1, name = main}
2018-04-08 22:27:12.571398+0800 应用加载时间的优化[6278:994824] syncSerial---end

总结:所有任务都在主线程中,没有开辟新的线程。任务由上至下,间隔两秒,依次执行。

4.异步执行 + 串行队列

代码:

- (void)asyncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"asyncSerial---begin");
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.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");
}

log:

2018-04-08 22:33:07.135609+0800 应用加载时间的优化[6314:1001768] currentThread---<NSThread: 0x604000073b80>{number = 1, name = main}
2018-04-08 22:33:07.136200+0800 应用加载时间的优化[6314:1001768] asyncSerial---begin
2018-04-08 22:33:07.136992+0800 应用加载时间的优化[6314:1001768] asyncSerial---end
2018-04-08 22:33:09.137247+0800 应用加载时间的优化[6314:1001860] 1---<NSThread: 0x60400027b7c0>{number = 3, name = (null)}
2018-04-08 22:33:11.142105+0800 应用加载时间的优化[6314:1001860] 1---<NSThread: 0x60400027b7c0>{number = 3, name = (null)}
2018-04-08 22:33:13.143027+0800 应用加载时间的优化[6314:1001860] 2---<NSThread: 0x60400027b7c0>{number = 3, name = (null)}
2018-04-08 22:33:15.145256+0800 应用加载时间的优化[6314:1001860] 2---<NSThread: 0x60400027b7c0>{number = 3, name = (null)}
2018-04-08 22:33:17.147141+0800 应用加载时间的优化[6314:1001860] 3---<NSThread: 0x60400027b7c0>{number = 3, name = (null)}
2018-04-08 22:33:19.151007+0800 应用加载时间的优化[6314:1001860] 3---<NSThread: 0x60400027b7c0>{number = 3, name = (null)}

总结:在主线程中第一时间syncConcurrent---beginsyncConcurrent---end,然后在开启的新线程中串行执行自定义任务,依次间隔两秒。

5.同步执行 + 主队列

代码:

- (void)syncMain
{
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"syncMain---begin");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(dispatch_get_main_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(@"syncMain---end");
}

运行时会直接崩溃。
崩溃原因:这是因为我们在主线程中执行 syncMain 方法,相当于把 syncMain 任务放到了主线程的主队列中。而同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把任务1追加到主队列中,任务1就在等待主线程处理完 syncMain 任务。而 syncMain 任务需要等待任务1执行完毕,才能接着执行。
那么,现在的情况就是syncMain任务和任务1都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了。
而之前的同步串行和同步并发都可以执行不会崩溃的原因在于,他们虽然都在主线程中,但是并不在主队列(main_queue)中,而是在自定义的队列中。

6.异步执行 + 主队列

代码:

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

log:

2018-04-08 23:14:51.902008+0800 应用加载时间的优化[6448:1043174] currentThread---<NSThread: 0x600000076a00>{number = 1, name = main}
2018-04-08 23:14:51.902183+0800 应用加载时间的优化[6448:1043174] asyncMain---begin
2018-04-08 23:14:51.902511+0800 应用加载时间的优化[6448:1043174] asyncMain---end
2018-04-08 23:14:53.906967+0800 应用加载时间的优化[6448:1043174] 1---<NSThread: 0x600000076a00>{number = 1, name = main}
2018-04-08 23:14:55.907579+0800 应用加载时间的优化[6448:1043174] 1---<NSThread: 0x600000076a00>{number = 1, name = main}
2018-04-08 23:14:57.909015+0800 应用加载时间的优化[6448:1043174] 2---<NSThread: 0x600000076a00>{number = 1, name = main}
2018-04-08 23:14:59.909606+0800 应用加载时间的优化[6448:1043174] 2---<NSThread: 0x600000076a00>{number = 1, name = main}
2018-04-08 23:15:01.910556+0800 应用加载时间的优化[6448:1043174] 3---<NSThread: 0x600000076a00>{number = 1, name = main}
2018-04-08 23:15:03.912148+0800 应用加载时间的优化[6448:1043174] 3---<NSThread: 0x600000076a00>{number = 1, name = main}

总结:类似于异步执行串行队列,都是在第一时间执行完syncConcurrent---beginsyncConcurrent---end后按顺序间隔两秒执行任务。不同之处在于没有开辟新的线程,而是在主线程中异步执行。
所以参照上面所说的异步与并发的关系,实际上异步使自定义的任务延后执行,整个线程仍是在主队列中串行执行自定义任务。而其效果,与异步串行队列效果一致,都是异步串行,根本区别在于前者将任务放在主线程的主队列中,而后者将任务在子线程的自定义队列中,也就是前者只有一个线程,后者多开辟了一个线程。

线程间通信

在iOS开发过程中,我们一般在主线程里边进行UI刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
使用dispatch_async(dispatch_get_main_queue(), ^{ });回到主队列。
代码:

- (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 <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);// 打印当前线程
        }
        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);// 打印当前线程
        });
    });
}

log:

2018-04-08 23:37:32.260716+0800 应用加载时间的优化[6512:1064267] 1---<NSThread: 0x6040004613c0>{number = 3, name = (null)}
2018-04-08 23:37:34.263732+0800 应用加载时间的优化[6512:1064267] 1---<NSThread: 0x6040004613c0>{number = 3, name = (null)}
2018-04-08 23:37:36.265455+0800 应用加载时间的优化[6512:1064179] 2---<NSThread: 0x600000078780>{number = 1, name = main}

总结:可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。

GCD的其他常用方法

栅栏方法:dispatch_barrier_async

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到 dispatch_barrier_async 方法在两个操作组间形成栅栏。
代码:

- (void)barrier {
    NSLog(@"barrier---begin");
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    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_barrier_async(queue, ^{
        // 追加任务 barrier
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务3
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务4
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"4---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    NSLog(@"barrier---end");
}

log:

2018-04-10 18:52:30.142820+0800 应用加载时间的优化[7378:1317650] barrier---begin
2018-04-10 18:52:30.143018+0800 应用加载时间的优化[7378:1317650] barrier---end
2018-04-10 18:52:32.147032+0800 应用加载时间的优化[7378:1317775] 1---<NSThread: 0x604000075880>{number = 4, name = (null)}
2018-04-10 18:52:32.147034+0800 应用加载时间的优化[7378:1317777] 2---<NSThread: 0x600000274500>{number = 3, name = (null)}
2018-04-10 18:52:34.148931+0800 应用加载时间的优化[7378:1317777] 2---<NSThread: 0x600000274500>{number = 3, name = (null)}
2018-04-10 18:52:34.148931+0800 应用加载时间的优化[7378:1317775] 1---<NSThread: 0x604000075880>{number = 4, name = (null)}
2018-04-10 18:52:36.149834+0800 应用加载时间的优化[7378:1317775] barrier---<NSThread: 0x604000075880>{number = 4, name = (null)}
2018-04-10 18:52:38.151610+0800 应用加载时间的优化[7378:1317775] barrier---<NSThread: 0x604000075880>{number = 4, name = (null)}
2018-04-10 18:52:40.153601+0800 应用加载时间的优化[7378:1317775] 3---<NSThread: 0x604000075880>{number = 4, name = (null)}
2018-04-10 18:52:40.153601+0800 应用加载时间的优化[7378:1317777] 4---<NSThread: 0x600000274500>{number = 3, name = (null)}
2018-04-10 18:52:42.155339+0800 应用加载时间的优化[7378:1317775] 3---<NSThread: 0x604000075880>{number = 4, name = (null)}
2018-04-10 18:52:42.155339+0800 应用加载时间的优化[7378:1317777] 4---<NSThread: 0x600000274500>{number = 3, name = (null)}

总结:由于是异步,所有任务都异步执行。加入了栅栏,所有异步任务先执行栅栏前,再执行栅栏,最后执行栅栏后。

栅栏方法的应用

使用栅栏方法实现多读单写。
比如在内存中维护一份数据,有很多地方可能会同时操作这块数据,如何保证数据安全。可以用栅栏方法来实现。主要需要满足:

  • 读写互斥
  • 写写互斥
  • 读读并发

@interface Person()

@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
@property (nonatomic, strong) NSMutableDictionary *dic;

@end

@implementation Person

- (instancetype)init {
    self = [super init];
    if (self) {
        //单独自定义一个并发队列。
        _concurrentQueue = dispatch_queue_create("com.Person.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
        _dic = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)kc_setSafeObject:(id)object forKey:(NSString *)key {
    //防止传进来的key改变
    key = [key copy];
    //异步写数据。加入栅栏方法保证之前的执行完,且内部执行完成之前,不会执行其他操作(写写互斥)。
    dispatch_barrier_async(_concurrentQueue, ^{
        [_dic setObject:object forKey:key];
    });
}

- (id)kc_safeObjectForKey:(NSString *)key {
    __block NSString *temp;
    //同步读数据。直接读取数据。(这里不能使用异步,同时访问name,age可能会导致混乱。因此可以允许多个任务同时进来,但是同步返回。读读并发)
    dispatch_sync(_concurrentQueue, ^{
        temp = [_dic objectForKey:key];
    });
    return temp;
}

@end
延时方法:dispatch_after

在指定时间(例如3秒)之后执行某个任务。可以用 GCD 的 dispatch_after 函数来实现。
需要注意的是:dispatch_after 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 函数是很有效的。
代码:

- (void)after {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"after---begin");
    // 2.0秒后异步追加任务代码到主队列,并开始执行
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"after---%@",[NSThread currentThread]);// 打印当前线程
    });
    NSLog(@"after---end");
}

log:

2018-04-10 19:08:03.125108+0800 应用加载时间的优化[7439:1332291] currentThread---<NSThread: 0x600000077e40>{number = 1, name = main}
2018-04-10 19:08:03.125318+0800 应用加载时间的优化[7439:1332291] after---begin
2018-04-10 19:08:03.125458+0800 应用加载时间的优化[7439:1332291] after---end
2018-04-10 19:08:05.125544+0800 应用加载时间的优化[7439:1332291] after---<NSThread: 0x600000077e40>{number = 1, name = main}

总结:可以看到是异步执行,没有卡主线程。延迟两秒后执行 block 内任务。

只执行一次代码:dispatch_once

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

+ (instancetype)instance
{

    static CPReachability *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      instance = [[self alloc] init];
      instance.netType = CPAPPNetTypeWIFI;
      instance.wifiName = [instance GetWifiName];
    });
    return instance;
}

总结:多用于单例。dispatch_once_t 其实是 long 类型,取其地址作为唯一标识符保证 block 内部任务执行且仅被执行一次。

快速迭代方法:dispatch_apply

通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数 dispatch_apply 。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(6, queue, ^(size_t index) {
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}

log:

2018-04-10 19:18:38.640868+0800 应用加载时间的优化[7506:1345125] apply---begin
2018-04-10 19:18:40.642368+0800 应用加载时间的优化[7506:1345125] 0---<NSThread: 0x6000000714c0>{number = 1, name = main}
2018-04-10 19:18:40.646225+0800 应用加载时间的优化[7506:1345197] 3---<NSThread: 0x600000265e80>{number = 5, name = (null)}
2018-04-10 19:18:40.646225+0800 应用加载时间的优化[7506:1345215] 1---<NSThread: 0x60400046c2c0>{number = 4, name = (null)}
2018-04-10 19:18:40.646272+0800 应用加载时间的优化[7506:1345198] 2---<NSThread: 0x600000265800>{number = 3, name = (null)}
2018-04-10 19:18:42.643982+0800 应用加载时间的优化[7506:1345125] 4---<NSThread: 0x6000000714c0>{number = 1, name = main}
2018-04-10 19:18:42.647137+0800 应用加载时间的优化[7506:1345197] 5---<NSThread: 0x600000265e80>{number = 5, name = (null)}
2018-04-10 19:18:42.647443+0800 应用加载时间的优化[7506:1345125] apply---end

总结:首先我们能看到 dispatch_apply 是并发执行的,因为在 queue 中,所以不能保证执行顺序。但是结果是同步的,会等待所有任务结束后继续线程,最后执行 apply---end。优点在于大规模循环时比起 for 效率更高,比起手动开线程能防止线程数过多导致线程爆炸。
应用场景:
如果我们从服务器获取一个数组的数据,那么我们可以使用该方法从而快速的批量字典转模型。

dispatch_group

dispatch_group 是 GCD 中的一组方法,他有一个组的概念,可以把相关的任务归并到一个组内来执行,通过监听组内所有任务的执行情况来做相应处理。

dispatch_group_create

用于创建任务组
dispatch_group_t dispatch_group_create(void);

dispatch_group_async

把异步任务提交到指定任务组和指定下拿出队列执行

void dispatch_group_async(dispatch_group_t group,
                          dispatch_queue_t queue,
                          dispatch_block_t block);
  • group ——对应的任务组,之后可以通过dispatch_group_wait或者dispatch_group_notify监听任务组内任务的执行情况
  • queue ——block任务执行的线程队列,任务组内不同任务的队列可以不同
  • block —— 执行任务的block
dispatch_group_notify

待任务组执行完毕时调用,不会阻塞当前线程
代码:

- (void)groupNotify {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"group---begin");
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        // 追加任务1
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        // 追加任务2
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);// 打印当前线程
        }
        NSLog(@"group---end");
    });
}

log:

2018-04-10 19:31:53.446408+0800 应用加载时间的优化[7570:1360370] currentThread---<NSThread: 0x600000075ac0>{number = 1, name = main}
2018-04-10 19:31:53.446553+0800 应用加载时间的优化[7570:1360370] group---begin
2018-04-10 19:31:55.450356+0800 应用加载时间的优化[7570:1360470] 1---<NSThread: 0x600000272640>{number = 3, name = (null)}
2018-04-10 19:31:55.450352+0800 应用加载时间的优化[7570:1360463] 2---<NSThread: 0x600000273c00>{number = 4, name = (null)}
2018-04-10 19:31:57.450882+0800 应用加载时间的优化[7570:1360470] 1---<NSThread: 0x600000272640>{number = 3, name = (null)}
2018-04-10 19:31:57.450882+0800 应用加载时间的优化[7570:1360463] 2---<NSThread: 0x600000273c00>{number = 4, name = (null)}
2018-04-10 19:31:59.452501+0800 应用加载时间的优化[7570:1360370] 3---<NSThread: 0x600000075ac0>{number = 1, name = main}
2018-04-10 19:32:01.454278+0800 应用加载时间的优化[7570:1360370] 3---<NSThread: 0x600000075ac0>{number = 1, name = main}
2018-04-10 19:32:01.454655+0800 应用加载时间的优化[7570:1360370] group---end

总结:异步并发执行组一内任务和组二内任务,使用 dispatch_group_notify 监测以上两任务完成后回主线程进入 block 内执行任务三。全程异步,不卡线程。

dispatch_group_wait

暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
代码:

- (void)groupWait {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"group---begin");
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        // 追加任务1
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        // 追加任务2
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"group---end");
}

log:

2018-04-10 19:49:33.996536+0800 应用加载时间的优化[7766:1383789] currentThread---<NSThread: 0x604000067f40>{number = 1, name = main}
2018-04-10 19:49:33.996684+0800 应用加载时间的优化[7766:1383789] group---begin
2018-04-10 19:49:36.000831+0800 应用加载时间的优化[7766:1383883] 2---<NSThread: 0x60400026ff00>{number = 3, name = (null)}
2018-04-10 19:49:36.000831+0800 应用加载时间的优化[7766:1383886] 1---<NSThread: 0x60000026ffc0>{number = 4, name = (null)}
2018-04-10 19:49:38.001697+0800 应用加载时间的优化[7766:1383883] 2---<NSThread: 0x60400026ff00>{number = 3, name = (null)}
2018-04-10 19:49:38.001987+0800 应用加载时间的优化[7766:1383886] 1---<NSThread: 0x60000026ffc0>{number = 4, name = (null)}
2018-04-10 19:49:38.003862+0800 应用加载时间的优化[7766:1383789] group---end

总结:使用 dispatch_group_wait(group, DISPATCH_TIME_FOREVER);卡线程来等待 group 内容全部执行完之后继续下面任务,实现同步。如果去掉 dispatch_group_wait则不卡线程,第一时间执行 group---end,然后执行 group 中内容。

dispatch_group_enter、dispatch_group_leave

void dispatch_group_enter(dispatch_group_t group);
用于添加对应任务组中的未执行完毕的任务数,执行一次,未执行完毕的任务数加1,当未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞和dispatch_group_notify 的 block执行。
void dispatch_group_leave(dispatch_group_t group);
用于减少任务组中的未执行完毕的任务数,执行一次,未执行完毕的任务数减1,dispatch_group_enter 和 dispatch_group_leave 要匹配,不然系统会认为 group 任务没有执行完毕。
代码:

- (void)groupEnterAndLeave{
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"group---begin");
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务1
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);// 打印当前线程
        }
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务2
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);// 打印当前线程
        }
        dispatch_group_leave(group);
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程.
        for(int i =0; i <2; ++i) {
            [NSThread sleepForTimeInterval:2];// 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);// 打印当前线程
        }NSLog(@"group---end");
    });//
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    //    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);//
    NSLog(@"group---end"); 
}

log:

2018-04-10 20:05:44.440633+0800 应用加载时间的优化[7796:1394822] currentThread---<NSThread: 0x60000007f000>{number = 1, name = main}
2018-04-10 20:05:44.440802+0800 应用加载时间的优化[7796:1394822] group---begin
2018-04-10 20:05:44.441216+0800 应用加载时间的优化[7796:1394822] group---end
2018-04-10 20:05:46.443538+0800 应用加载时间的优化[7796:1394930] 2---<NSThread: 0x600000474440>{number = 3, name = (null)}
2018-04-10 20:05:46.443549+0800 应用加载时间的优化[7796:1394928] 1---<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-04-10 20:05:48.445104+0800 应用加载时间的优化[7796:1394930] 2---<NSThread: 0x600000474440>{number = 3, name = (null)}
2018-04-10 20:05:48.445101+0800 应用加载时间的优化[7796:1394928] 1---<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-04-10 20:05:50.445690+0800 应用加载时间的优化[7796:1394822] 3---<NSThread: 0x60000007f000>{number = 1, name = main}
2018-04-10 20:05:52.447481+0800 应用加载时间的优化[7796:1394822] 3---<NSThread: 0x60000007f000>{number = 1, name = main}
2018-04-10 20:05:52.447792+0800 应用加载时间的优化[7796:1394822] group---end

总结:等同于 dispatch_group_async

信号量:dispatch_semaphore

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

Dispatch Semaphore提供了三个函数。

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

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

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

Dispatch Semaphore 线程同步

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

- (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);
    });
    NSLog(@"main task continue");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}

log:

2018-04-10 21:58:57.974515+0800 应用加载时间的优化[8026:1443631] currentThread---<NSThread: 0x600000073900>{number = 1, name = main}
2018-04-10 21:58:57.974641+0800 应用加载时间的优化[8026:1443631] semaphore---begin
2018-04-10 21:58:57.974790+0800 应用加载时间的优化[8026:1443631] main task continue
2018-04-10 21:58:59.978823+0800 应用加载时间的优化[8026:1443709] 1---<NSThread: 0x604000269240>{number = 3, name = (null)}
2018-04-10 21:58:59.979016+0800 应用加载时间的优化[8026:1443631] semaphore---end,number = 100

总结:使用 semaphore 实现线程同步。可以在添加 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 之前实现主线程异步并发操作,在之后进行线程同步,等待子线程任务结束后执行后续操作。

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

线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步:
可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。
举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

以售票为例:
非线程安全:(不使用 semaphore):
代码:

- (void)initTicketStatusNotSafe {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"semaphore---begin");
    self.ticketSurplusCount = 50;
    // 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;
        }
    }
}

Log:

2018-04-10 22:25:01.047999+0800 应用加载时间的优化[8218:1472786] currentThread---<NSThread: 0x604000064880>{number = 1, name = main}
2018-04-10 22:25:01.048150+0800 应用加载时间的优化[8218:1472786] semaphore---begin
2018-04-10 22:25:01.048378+0800 应用加载时间的优化[8218:1472878] 剩余票数:48 窗口:<NSThread: 0x6000002651c0>{number = 4, name = (null)}
2018-04-10 22:25:01.048387+0800 应用加载时间的优化[8218:1472892] 剩余票数:49 窗口:<NSThread: 0x604000272800>{number = 3, name = (null)}
2018-04-10 22:25:01.249347+0800 应用加载时间的优化[8218:1472878] 剩余票数:47 窗口:<NSThread: 0x6000002651c0>{number = 4, name = (null)}
2018-04-10 22:25:01.249354+0800 应用加载时间的优化[8218:1472892] 剩余票数:46 窗口:<NSThread: 0x604000272800>{number = 3, name = (null)}
2018-04-10 22:25:01.450904+0800 应用加载时间的优化[8218:1472878] 剩余票数:45 窗口:<NSThread: 0x6000002651c0>{number = 4, name = (null)}
2018-04-10 22:25:01.450904+0800 应用加载时间的优化[8218:1472892] 剩余票数:45 窗口:<NSThread: 0x604000272800>{number = 3, name = (null)}
2018-04-10 22:25:01.651358+0800 应用加载时间的优化[8218:1472892] 剩余票数:44 窗口:<NSThread: 0x604000272800>{number = 3, name = (null)}
2018-04-10 22:25:01.651358+0800 应用加载时间的优化[8218:1472878] 剩余票数:44 窗口:<NSThread: 0x6000002651c0>{number = 4, name = (null)}
.
.
.
2018-04-10 22:25:07.753135+0800 应用加载时间的优化
[8218:1472878] 剩余票数:2 窗口:<NSThread: 0x6000002651c0>{number = 4, name = (null)}
2018-04-10 22:25:07.753135+0800 应用加载时间的优化[8218:1472892] 剩余票数:2 窗口:<NSThread: 0x604000272800>{number = 3, name = (null)}
2018-04-10 22:25:07.958573+0800 应用加载时间的优化[8218:1472878] 剩余票数:1 窗口:<NSThread: 0x6000002651c0>{number = 4, name = (null)}
2018-04-10 22:25:07.958574+0800 应用加载时间的优化[8218:1472892] 剩余票数:0 窗口:<NSThread: 0x604000272800>{number = 3, name = (null)}

总结:剩余票数错乱,且重复,存在多个线程同时访问的情况,也就说存在同一张票卖两次的情况。

线程安全:(使用 semaphore 加锁):
使用 semaphoreLock = dispatch_semaphore_create(1); 创建锁
使用 dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER); 加锁
使用 dispatch_semaphore_signal(semaphoreLock); 解锁
用加锁和解锁包含住可能会被多线程同时执行修改的代码。
代码:

static dispatch_semaphore_t semaphoreLock;

- (void)initTicketStatusSafe {
    NSLog(@"currentThread---%@",[NSThread currentThread]);// 打印当前线程
    NSLog(@"semaphore---begin");
    //value表示最大线程同时访问数,此处为1,更改会出问题
    semaphoreLock = dispatch_semaphore_create(1);
    self.ticketSurplusCount =50;
    // 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 saleTicketSafe];
    });dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

- (void)saleTicketSafe {
    while(1) {
        // 相当于加锁
        dispatch_semaphore_wait(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(semaphoreLock);
            break;
        }// 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}

log:

2018-04-10 22:41:43.466847+0800 应用加载时间的优化[8424:1495856] currentThread---<NSThread: 0x60000007b700>{number = 1, name = main}
2018-04-10 22:41:43.466991+0800 应用加载时间的优化[8424:1495856] semaphore---begin
2018-04-10 22:41:43.467215+0800 应用加载时间的优化[8424:1495959] 剩余票数:49 窗口:<NSThread: 0x60400046e840>{number = 3, name = (null)}
2018-04-10 22:41:43.671865+0800 应用加载时间的优化[8424:1495961] 剩余票数:48 窗口:<NSThread: 0x600000270840>{number = 4, name = (null)}
2018-04-10 22:41:43.874748+0800 应用加载时间的优化[8424:1495959] 剩余票数:47 窗口:<NSThread: 0x60400046e840>{number = 3, name = (null)}
2018-04-10 22:41:44.078812+0800 应用加载时间的优化[8424:1495961] 剩余票数:46 窗口:<NSThread: 0x600000270840>{number = 4, name = (null)}
2018-04-10 22:41:44.280894+0800 应用加载时间的优化[8424:1495959] 剩余票数:45 窗口:<NSThread: 0x60400046e840>{number = 3, name = (null)}
2018-04-10 22:41:44.483298+0800 应用加载时间的优化[8424:1495961] 剩余票数:44 窗口:<NSThread: 0x600000270840>{number = 4, name = (null)}
2018-04-10 22:41:44.686228+0800 应用加载时间的优化[8424:1495959] 剩余票数:43 窗口:<NSThread: 0x60400046e840>{number = 3, name = (null)}
2018-04-10 22:41:44.890615+0800 应用加载时间的优化[8424:1495961] 剩余票数:42 窗口:<NSThread: 0x600000270840>{number = 4, name = (null)}
.
.
.
2018-04-10 22:41:53.450842+0800 应用加载时间的优化[8424:1495961] 剩余票数:0 窗口:<NSThread: 0x600000270840>{number = 4, name = (null)}
2018-04-10 22:41:53.656188+0800 应用加载时间的优化[8424:1495959] 所有火车票均已售完
2018-04-10 22:41:53.656569+0800 应用加载时间的优化[8424:1495961] 所有火车票均已售完

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

死锁

使用dispatch_semaphore给线程加锁可以解决数据同步问题,但是同时要注意造成死锁导致应用崩溃。
例如:某方法中,等待服务器接口请求数据成功后,返回数据,实现异步网络请求的同步实现。

-(void)requestA{
    
    //创建信号量并设置计数默认为0
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    
    [AFNmanager GET:@"" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {        
        dispatch_semaphore_signal(sema);

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        //计数+1操作        
        dispatch_semaphore_signal(sema);
        
    }];

    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

}

注意:该方法可能会造成死锁。首先,该方法在主线程中加锁,阻塞主线程,等待子线程完成网络请求任务后,发出解锁信号。解除主线程锁,继续运行。
但是在AFNetworking在完成请求的block中,AF内部的completion会回到主线程进行回调。而主线程加锁已经阻塞了主线程,因此无法发出解锁信号,造成死锁。
对于AFNetworking中,有completionQueue属性,可以设置请求完成的回调线程,设置成子线程,即可解决死锁问题。
对于其他网络框架,与dispatch_semaphore结合使用时,均需要注意不要造成死锁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容