iOS多线程开发—GCD (一)

971 阅读15分钟

GCD是什么?

作为一个iOS开发者,无论你是大神还是像我这样的菜鸟,每一个人应该都不会对多线程开发陌生,即便你没有听说过pthread,NSThread,NSOperation,但你至少多少听说过或者使用过这样的代码

dispatch_async(dispatch_get_main_queue,{
  //在这里搞事情
});

那么恭喜你,你会GCD! 其实当我第一次使用这个代码的时候,我并不确切的理解以上这段代码干了什么,我只知道这样干不会让我的界面处于没有反应的状态。随着开发经验的累积,越来越多的使用了有关多线程的知识,因此在这里把我的一些浅薄的理解记录下来,帮助自己,也希望能够帮助到其他需要的人。

在这里我们先给GCD做个定义吧: 1.GCD是Grand Central Dispatch的缩写,中文可以称为巨牛X的中央派发。这其实是苹果公司为我们提供的一种多线程编程API。 2.GCD通过block让我们可以很容易的将要执行的任务放入队列中,我们不需要关心任务在哪一个线程中执行,这就让开发者能够更容易的使用多线程技术进行编程而不用实际操作线程。 3.GCD为我们提供了创建队列的方法,并且提供了任务同步和异步执行的方法。我们所需要关心的只是如何去定义一个任务。

进程,线程,同步,异步,并行,并发,串行?傻傻分不清😂

我们常常说多线程编程,那么究竟什么是多线程编程,我们为什么要使用多线程编程技术呢? 要搞清这么多概念我们首先要简单的说一下计算机的CPU! 现在的计算机多是所谓的多核计算机,在一个物理核心以外还会使用软件技术创造出多个虚拟核心。但是无论是有多少个核心,一个CPU核心在同一时间内只能执行一条无分叉的代码指令这条无分叉的代码指令就是我们常说的线程(后面会给出线程更具体的定义),因此如果想提高计算机的运行效率,我们除了让CPU具有更多内核以外,还需要使用多线程的方式,让同一个内核在多条线程之间做切换以此来提高CPU的使用效率。

1.进程(process)

  • 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程。
  • 每个进程之间是相互独立的, 每个进程均运行在其专用且受保护的内存空间内。
  • 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。
  • 进程状态:进程有三个状态,就绪,运行和阻塞。就绪状态其实就是获取了除cpu外的所有资源,只要处理器分配资源马上就可以运行。运行态就是获取了处理器分配的资源,程序开始执行,阻塞态,当程序条件不够时,需要等待条件满足时候才能执行,如等待I/O操作的时候,此刻的状态就叫阻塞态。

2.线程(thread)

  • 一个进程要想执行任务,必须要有线程,至少有一条线程。
  • 一个进程的所有任务都是在线程中执行。
  • 每个应用程序想要跑起来,最少也要有一条线程存在,其实应用程序启动的时候我们的系统就会默认帮我们的应用程序开启一条线程,这条线程也叫做'主线程',或者'UI线程'。

3.进程和线程的关系

  • 线程是进程的执行单元,进程的所有任务都在线程中执行!
  • 线程是 CPU 调用的最小单位。
  • 进程是 CPU 分配资源和调度的单位。
  • 一个程序可以对应过个进程,一个进程中可有多个线程,但至少要有一条线程。
  • 同一个进程内的线程共享进程资源。

有关进程和线程的内容我参考了《iOS开发之多线程编程总结(一)》,这篇文章对于这方面的总结很到位,有兴趣的朋友可以看一下。 下面我谈一下我自己对于同步,异步,并行,并发,串行的理解。

4.同步(Synchronous) VS. 异步(Asynchronous) 在这里呢我也不谈什么理论了,就我开发过程中的理解谈一下吧。

NSLog(@"Hellot, task1");
    dispatch_queue_t queue = dispatch_queue_create("Sereal_Queue_1", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        for (int i = 0; i < 10 ; i ++) {
            
            NSLog(@"%d",i);
        }
    });
    
NSLog(@"Hello, taks2");
dispatch_async(queue, ^{
        for (int i = 0; i < 10 ; i ++) {
            
            NSLog(@"%d",i);
        }

    });
NSLog(@"Hello,task3");

打印结果如下

2017-07-01 20:40:56.914 GCD_Test[85651:1471470] Hellot, task1
2017-07-01 20:40:56.914 GCD_Test[85651:1471470] 0
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 1
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 2
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 3
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 4
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 5
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 6
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 7
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 8
2017-07-01 20:40:56.916 GCD_Test[85651:1471470] 9
2017-07-01 20:40:56.916 GCD_Test[85651:1471470] Hello, taks2
2017-07-01 20:40:56.916 GCD_Test[85651:1471470] Hello,task3
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 0
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 1
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 2
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 3
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 4
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 5
2017-07-01 20:40:56.917 GCD_Test[85651:1471599] 6
2017-07-01 20:40:56.917 GCD_Test[85651:1471599] 7
2017-07-01 20:40:56.917 GCD_Test[85651:1471599] 8
2017-07-01 20:40:56.922 GCD_Test[85651:1471599] 9

从上面的打印结果我们可以看出,在hello,task1和hello,task2之间我们执行了一个同步任务,这个任务被放在了一个串行队列当中。因此,Hello,task2必须要等待队列中的任务被执行完毕之后才会执行。当我们在hello,task2和hello,task3中执行了异步任务的时候,hello,task3不需要等待队列中的任务被执行完再执行。 因此我们可以这样认为,同步任务必须等到任务执行结束之后才会返回,异步任务不需要等待任务结束可以立即返回执行下面的代码。

5.并行(Paralleism) VS. 并发(Concurrency) 其实简单来说并行和并发都是计算机同时执行多个线程的策略。只是并发是一种"伪同时",前面我们已经说过,一个CPU内核在同一个时间只能执行一个线程,并发是通过让CPU内核在多个线程做快速切换来达到让程序看起来是同时执行的目的。这种上下文切换(context-switch)的速度非常快,以此来达到线程看起来是并行执行的目的。而并行是多条线程在多个CPU内核之间同时执行,以此来达到提高执行效率的目的。

6.并行(Paralleism) VS. 串行(Serial) 从下面的代码我们先来看一下串行

dispatch_queue_t queue = dispatch_queue_create("Sereal_Queue_1", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10 ; i ++) {
        dispatch_async(queue, ^{
            NSLog(@"%d",i);
        });
    }
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 0
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 1
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 2
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 3
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 4
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 5
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 6
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 7
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 8
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 9

从打印结果我们可以清晰的看到,所有被添加到串行队列的任务都是按照添加顺序依次执行的,也就是说串行的基本特点是任务按照顺序执行。

并行

dispatch_queue_t concurrentQueue = dispatch_queue_create("Global_Queue_1", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0 ; i < 10 ; i ++) {
        dispatch_async(concurrentQueue, ^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"%d",i);
        });
    }

2017-07-02 02:02:29.759 GCD_Test[86327:1705240] 2
2017-07-02 02:02:29.759 GCD_Test[86327:1705243] 1
2017-07-02 02:02:29.759 GCD_Test[86327:1705239] 0
2017-07-02 02:02:29.759 GCD_Test[86327:1705268] 5
2017-07-02 02:02:29.759 GCD_Test[86327:1705264] 3
2017-07-02 02:02:29.759 GCD_Test[86327:1705267] 4
2017-07-02 02:02:29.759 GCD_Test[86327:1705270] 7
2017-07-02 02:02:29.759 GCD_Test[86327:1705269] 6
2017-07-02 02:02:29.759 GCD_Test[86327:1705271] 8
2017-07-02 02:02:29.759 GCD_Test[86327:1705273] 9

打印结果表明任务的添加顺序和执行顺序无关,并且在使用了 [NSThread sleepForTimeInterval:1.0];的情况下,所有人物的执行时间是一样的,这说明它们是并行执行的,如果你有兴趣的话还可以打印一下它们执行任务的线程,这样将会获得更清楚的显示。

GCD出场👏👏👏

随着一阵啪啪啪(键盘的声音)🙈GCD 出场了。 1,为什么使用GCD? 可以这样说,GCD为我们提供了一个更加容易的方式来实现多线程编程,我们不用直接去建立,管理线程,而只需要通过GCD帮助我们把任务放入相应的队列中来实现多线程编程的特点。 2,为什么使用多线程编程? 就我现在不多的经验来说,(1),多线程编程使我们在编程的过程中将一下耗时的操作放在非主线程当中,避免了阻塞主线程。(2),在与网络的交互当中提高了效率,比如说我们可以使用多线程并行上传和下载来提高速度。 3,多线程编程需要注意什么? (1),避免死锁,后面我们会具体说到。 (2),数据竞争,后面我们会具体说到。 (3),避免建立大量的无用线程,线程的创建和维护是需要消耗系统资源的,线程的堆栈都需要创建和维护,因此创建大量线程是百害而无一利。因此我们要记住,多线程技术本身是没有好处的,关键是要使用多线程完成并行操作以此来提高效率。

1.创建队列

在GCD中有三种不同的队列:

  • 主队列:这是一个特殊的串行队列,队列中的任务都在主线程中执行。
  • 串行队列:任务在队列中先进先出,每次执行一个任务。
  • 并发队列:任务在队列中也是先进先出,但是同时可以执行多个任务。
//获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//获取串行队列
 dispatch_queue_t serialQueue = dispatch_queue_create("com.jiaxiang.serialQueue", DISPATCH_QUEUE_SERIAL);
 
//获取并行队列
dispatch_queue_t concurrentQueue1 = dispatch_queue_create("com.jiaxiang.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t concurrentQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

当我们使用dispatch_queue_create的时候,我们创建了一个串行队列或者并行队列,当我们使用dispatch_get_main_queue();时我们获取了系统创建的主队列,dispatch_get_global_queue让我们获取了系统中的全局队列,并且通过参数为全局队列设定了优先级。

全局队列有四种优先级分别用宏定义

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

在这里要说两个问题: 1,串行队列,并行队列,同步,异步与线程之间的关系。 2,死锁问题是如何产生的。

队列 同步 异步
主队列 在主线程执行 在主线程执行
串行队列 在当前线程执行 在新建线程执行
并行队列 在当前线程执行 在新建线程执行

让我们看以下代码来验证上述结论:

  dispatch_queue_t mainQueue = dispatch_get_main_queue();
 
 //在主线程同步追加任务造成死锁
  dispatch_sync(mainQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
  });
    
  dispatch_async(mainQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
  });

2017-07-02 14:06:40.371 GCD_Test[86684:1876563] <NSThread: 0x600000079000>{number = 1, name = main}

第一个主队列同步追加任务会造成死锁,我们从栈调用可以看出以上代码是在主线程执行。第二主队列异步追加任务可以顺利执行,我们从打印可以看出是在主线程执行。

dispatch_queue_t serialQueue = dispatch_queue_create("com.jiaxiang.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
});
    
dispatch_async(serialQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
});
2017-07-02 14:16:58.578 GCD_Test[86721:1886346] <NSThread: 0x60000006f1c0>{number = 1, name = main}
2017-07-02 14:16:58.579 GCD_Test[86721:1886393] <NSThread: 0x608000074c00>{number = 3, name = (null)}

第一个是在当前线程,因为当前线程是主线程,所以也就是在主线程执行。第二个是新建线程执行。

dispatch_queue_t concurrentQueue1 = dispatch_queue_create("com.jiaxiang.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue1, ^{
    NSLog(@"%@",[NSThread currentThread]);
  });
    
dispatch_async(concurrentQueue1, ^{
      NSLog(@"%@",[NSThread currentThread]);
  });
2017-07-02 14:20:26.646 GCD_Test[86748:1890613] <NSThread: 0x60800007d300>{number = 1, name = main}
2017-07-02 14:20:26.646 GCD_Test[86748:1890662] <NSThread: 0x6080002675c0>{number = 3, name = (null)}

第一个是在当前线程,因为当前线程是主线程,所以也就是在主线程执行。第二个是新建线程执行。

通过以上的代码我们可以看出,并行队列不一定会新建线程,串行队列也不一定只在当前线程执行。因此,当我们考虑队列是在哪个线程执行的时候我们一定要考虑它是同步还是异步执行的问题

下面我们来看死锁: 其实对于死锁我觉得大家记住这句话就够了,在当前串行队列中同步追加任务必然造成死锁。 比如我们上面用到的在主队列中同步添加任务,因为dispatch_sync会阻塞当队列程直到block中追加的任务执行完成之后在继续执行,但是block中的任务是被添加到主队列最后的位置,那么主队列中其他任务如果不完成的话追加的block是不会执行的,但是队列被阻塞,block前面的任务无法执行,这就造成了在主队列中任务互相等待的情况,最终造成死锁。 在分析死锁问题是不要过多的考虑使用的是什么线程,因为我们在使用GCD的时候首先考虑的是队列和任务,至于线程的分配和维护是由系统决定的,如果我们总是考虑线程那样往往会使我们难以分析死锁的原因。 至于解决死锁的方法其实也很简单,使用dispatch_async避免当前队列被阻塞,这样我们就可以在不等待追加到队列最后的任务完成之前继续执行队列中的任务。并将追加的任务添加到队列末尾。

参考文章: 《iOS开发之多线程编程总结(一)》 《Objective-C高级编程:iOS与OS X多线程和内存管理》 《Effective Objective-C 2.0》