iOS多线程——你要知道的GCD都在这里

你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

转载请注明出处 http://www.jianshu.com/p/e9d8a087f6c0

本系列文章主要讲解iOS中多线程的使用,包括:NSThread、GCD、NSOperation以及RunLoop的使用方法详解,本系列文章不涉及基础的线程/进程、同步/异步、阻塞/非阻塞、串行/并行,这些基础概念,有不明白的读者还请自行查阅。本系列文章将分以下几篇文章进行讲解,读者可按需查阅。

GCD的使用姿势全解

经过前一篇文章的学习,可以发现直接使用NSThread来编写多线程程序有不少问题,线程在执行完成后就会退出,每次执行任务都需要创建一个线程很浪费资源,其次是需要我们自行进行同步操作,自行管理线程的生命周期,如果要编写并发的代码或者多核的真正并行处理的代码就比较复杂了。

本篇文章将会介绍一个抽象层次更高的多线程编写方式GCDGCD全称Grand Central Dispatch是苹果提供的一个多核编程的解决方案,在真正意义上实现了并行操作,而不是并发。

GCD使用线程池模型来执行用户提交的任务,所以它比较节约资源,不需要为每个任务都重新创建一个新的线程,GCD不需要自行编写并行代码,而是自动进行多核的并行计算,自动管理线程的生命周期,如:使用线程池管理线程的创建和销毁,线程的调度,任务的调度等,用户只需要编写任务代码并提交即可。

GCD中有两个比较重要的概念:任务和队列。

GCD的任务

任务顾名思义就是我们需要执行的代码块,可以是一个方法也可以是一个block,就是我们需要线程为我们完成的工作,编写完成的任务只需提交给GCD的队列,即可自动帮我们完成任务的调度,以及线程的调度,可以很方便的以多线程的方式执行。

GCD的队列

队列用于管理用户提交的任务,GCD的队列有两种形式,串行队列和并发队列:

  • 串行队列: GCD底层只维护一个线程,任务只能串行依次执行。
  • 并发队列: GCD底层使用线程池维护多个线程,任务可并发执行。

不论是串行队列还是并发队列都使用FIFO 先进先出的方式来管理用户提交的任务。

对于串行队列来说,GCD每次从串行队列的队首取一个任务交给唯一的一个线程来处理,直到前一个任务完成后,才继续从队列中取下一个任务来执行,因此,串行队列中的任务执行严格按照提交顺序,并且后一个任务必须等前一个任务执行完成后才可以执行。

对于并发队列来说,GCD每次从并发队列的队首取一个任务,并将这个任务按照任务调度分发给多个线程中的某一个线程,此时不需要等待其完成,如果队列中还有其他任务继续从队列中取出并分发给某一个线程来执行,由于底层由线程池管理多个线程,每个任务的时间复杂度不同再加上线程调度的影响,后提交的任务可能先执行完成。但对于单个线程来说,只能按顺序执行,比如某个线程被安排了多个任务,那这个线程就只能按提交顺序依次执行任务。

所以,我们在使用GCD时也就很简单了,只需要创建或获取系统队列、编写任务并提交任务到队列即可。首先举一个下载图片的栗子,这个栗子和第一篇讲解NSThread的栗子一样,但是使用GCD来实现:

//获取一个优先级默认的全局并发队列,并以异步的方式提交任务执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //下载图片
        UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1509003055&di=ef9641b620fc103323df445bf796cb13&imgtype=jpg&er=1&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
        //获取主队列,在主线程中更新UI,并以异步方式提交任务
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
        
    });

上面的栗子非常简单,不需要我们手动创建线程即可实现多线程的并发编程,如果现在还看不懂没关系,学完本章内容你一定会懂。

首先看一下GCD为我们提供了哪些创建队列或获取系统队列的方法:

//获取当前执行该方法的队列,被废弃了,最好不要使用
dispatch_queue_t dispatch_get_current_queue(void);

/*
获取主队列,即与主线程相关联的队列
如果需要提交任务到主线程使用该方法获取主线程的主队列即可
主队列是串行队列因为只维护主线程一个线程
*/
dispatch_queue_t dispatch_get_main_queue(void);

/*
获取一个全局的并发队列
identifier指定该队列的优先级可选值有:
    DISPATCH_QUEUE_PRIORITY_HIGH 2
    DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    DISPATCH_QUEUE_PRIORITY_LOW (-2)
    DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags未用到传个0得了
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);

/*
创建一个队列
label 队列的名称
attr 队列的属性可选值有:
    DISPATCH_QUEUE_SERIAL 创建一个串行队列
    DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
通过这种方式可以自己维护一个队列
*/
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

具体获取相关队列的方法如下:

//获取串行主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//获取一个默认优先级的并发队列
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT);

//自定义创建一个名称为myConcurrentQueue的并发队列
dispatch_queue_t myConcurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);

队列创建完成以后就可以编写任务并提交了,接下来将介绍两种提交执行了,接下来介绍两种执行方式,同步执行和异步执行。

  • 同步执行: 阻塞当前线程,直到任务执行完成当前线程才可继续执行
  • 异步执行: 不阻塞当前线程,可能使用其他线程来执行任务,不需要等待任务完成当前线程即可立即继续执行

关于同步/异步,阻塞/非阻塞建议看UNIX网络编程 卷一有详细的解释,此处不再赘述了。

看一下GCD提交执行任务的具体方法:

/*
以异步方式执行任务,不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

/*
同上
context 是一个void*的指针,作为work的第一个形参
work 是一个函数指针,指向返回值为void 形参为void*的函数,且形参不能为NULL,也就是说context一定要传
使用起来不方便,一般不怎么用,需要使用C函数,也可以使用OC方法通过传递IMP来执行但是会有编译警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式执行任务,阻塞当前线程,必须等待任务完成当前线程才可继续执行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任务,并重复执行iterations次
iterations 迭代执行次数
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值为void形参为iterations迭代次数
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue,  DISPATCH_NOESCAPE void (^block)(size_t));

//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));

/*
以异步方式提交任务,在when时间点提交任务
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以异步方式提交任务,会阻塞queue队列,但不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
需要说明的是,即时使用并发队列,该队列也会被阻塞,前一个任务执行完成才能执行下一个任务
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任务,会阻塞queue队列,也会阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
同样的,即时是并发队列该队列也会被阻塞,需要等待前一个任务完成,同时线程也会阻塞
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
底层线程池控制block任务在整个应用的生命周期内只执行一次
predicate 实际为long类型,用于判断是否执行过
block block形式的任务,该block返回值、形参都为void
该方法常用于实现单例类,以及结合RunLoop创建一个常驻内存的线程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

猛的一看常用的方法就有十二种呢,但是可以发现每一类方法都提供了block任务和function任务两种形式,所以常用的也就七种,只是对应了block版本和函数版本。接下来先介绍最常用的同步执行和异步执行,其他的方法后文会讲。

单拎出来同步/异步很好理解,但是结合了串行队列和并发队列以后情况就有点复杂了,同步/异步执行和串行/并发队列两两组合就有四种组合方式,接下来我们一一查看相关栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //手动创建了一个并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //也可以获取全局的并发队列,效果一样
    //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //异步提交一个任务到异步队列
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    //异步提交一个任务到异步队列
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    //异步提交一个任务到异步队列
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //使用传递函数指针的方式有点复杂,以后的栗子不再赘述
    int context = 0;
    dispatch_async_f(concurrentQueue, &context, cFuncTask);
    //也可以使用OC方法,传入IMP,但会有警告
    //dispatch_async_f(concurrentQueue, &context, [self methodForSelector:@selector(ocFuncTask:)]);
}

//该函数是C函数
void cFuncTask(void* context)
{
    for (int i = 0; i < 500; i++)
    {
        NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
    }
}
//OC方法
- (void)ocFuncTask:(void*) context
{
    for (int i = 0; i < 500; i++)
    {
        NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
    }
}

上述代码输出的东西比较多,现在的手机CPU都很强大,可能一个时间片就一次性处理完了,就看不到并发的输出结果了,所以这里输出的次数比较多,最终的结果就是Task1-4乱序输出,摘取四个输出结果如下:

Task4 <NSThread: 0x1c04776c0>{number = 7, name = (null)} 88
Task3 <NSThread: 0x1c04770c0>{number = 6, name = (null)} 63
Task1 <NSThread: 0x1c0474980>{number = 4, name = (null)} 99
Task2 <NSThread: 0x1c427c5c0>{number = 5, name = (null)} 0

可以发现每一个任务都是用了不同的线程来执行,所以通过异步提交任务到一个并发队列是真正实现了并发执行。

接下来看一下异步提交到一个串行队列,栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    //创建一个串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
    //这里也可以用主队列,因为主队列也是串行队列
    //dispatch_queue_t serialQueue = dispatch_get_main_queue();

    //异步提交一个任务到串行队列
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    //异步提交一个任务到串行队列
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    //异步提交一个任务到串行队列
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
}

执行程序后可以发现,不论输出再多次都是按照Task1-3顺序输出,也就是后一个任务必须在前一个任务完成后才能执行,摘取三个输出结果如下:

Task1 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
Task2 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
Task3 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0

通过结果不难发现三个任务使用的是同一个线程,因为串行队列的底层只维护一个线程,所以三个任务只能使用同一个线程来执行,而且单个线程的执行是串行的,所以才会造成上述输出结果,这里使用异步执行没有阻塞当前线程。

异步执行的栗子实验完了,可以发现,异步执行仅仅不会阻塞当前线程,但是否是并发执行需要依靠传入的队列,如果传递的是串行队列就是串行执行,传入的是并发队列就是并发执行,接下来看一下同步执行的实验。

- (void)viewWillAppear:(BOOL)animated
{
    /*
    创建一个串行队列
    这里不可以使用主队列了,因为执行该方法的是主线程,如果使用同步执行提交到主队列会造成死锁,后文会有具体讲解
    */
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
    
    //同步提交一个任务到串行队列
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一个任务到串行队列
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一个任务到串行队列
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
}

执行程序后可以发现,不论输出再多次都是按照Task1-3顺序输出,也就是后一个任务必须在前一个任务完成后才能执行,但这里的顺序执行和前一个异步提交到串行队列不同,异步提交不会造成线程阻塞,所以三个任务都被提交到了串行队列中,但是由于线程的执行是按顺序的,所以三个任务按次序依次执行。而这里是使用同步提交到串行队列去执行任务,当第一个dispatch_sync方法执行后会阻塞当前线程,必须得等第一个任务完成后才能继续,所以这里的执行顺序是提交第一个任务后就开始执行而且得等到第一个任务完成后再去执行第二个dispatch_sync方法用于提交第二个任务,以此类推。虽然结果是一致的,但执行顺序是有差别的,需要注意,摘取三个输出结果如下:

Task1 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
Task2 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
Task3 <NSThread: 0x1c0072c80>{number = 1, name = main} 0

可以发现三个任务使用了同一个线程来执行,但是这个线程有点特殊,它是主线程,由于viewWillAppear:方法是在主线程中执行的,所以这里也就直接使用了主线程。

接下来进行最后一个实验,同步提交到并发队列执行:

- (void)viewWillAppear:(BOOL)animated
{
    //手动创建了一个并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //也可以获取全局的并发队列,效果一样
    //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //同步提交一个任务到并发队列
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一个任务到并发队列
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一个任务到并发队列
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
}

执行程序后可以发现,不论输出多少次都是按Task1-3顺序输出,相信大家应该明白是为什么了,因为同步提交阻塞当前线程,第一个dispatch_sync提交的任务完成以后当前线程才能去执行第二个dispatch_sync方法然后执行第二个任务。所以,即时是并发队列,采用同步提交也没什么卵用了。摘取三个输出如下:

Task1 <NSThread: 0x1c0263200>{number = 1, name = main} 0
Task2 <NSThread: 0x1c0263200>{number = 1, name = main} 0
Task3 <NSThread: 0x1c0263200>{number = 1, name = main} 0

从上面的输出可以看出,三个任务都是用主线程来执行,按照并发队列的特性,这里的三个任务完全可能由不同的三个线程来执行,但由于viewWillAppear:方法是主线程执行的,而且主线程又被阻塞了,底层可能因此选择了主线程来执行,多运行几次就会发现也有可能使用其他线程来执行。

到此为止,四个实验都结束了,没有单独把主队列拿出来做实验,因为主队列本质还是一个串行队列,其实验结果和串行队列是一样的。通过四组实验不难发现,想要实现并发只能通过异步提交到并发队列来执行任务,实验分析如下表:

type Serial串行队列 Concurrent并发队列
async异步执行 不阻塞当前线程,使用其他线程串行执行任务,只有一个线程用于执行任务 不阻塞当前线程,并发执行任务,使用多个线程执行任务
sync同步执行 阻塞当前线程,使用同一线程串行执行任务,只有一个线程用于执行任务 阻塞当前线程,可能使用同一线程串行执行任务

所以,针对异步执行/同步执行和串行队列/并发队列,只需要掌握其关键就可以了,同步/异步的区别在于是否阻塞线程,串行/并发队列的区别在于有多少个线程参与执行任务。即时存在嵌套结构也能够很好理解了。举一个嵌套结构的例子:

- (void)viewWillAppear:(BOOL)animated
{

    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(serialQueue, ^{
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 500; i++)
            {
                NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
            }
        });
        
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 500; i++)
            {
                NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
            }
        });
        
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 500; i++)
            {
                NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
            }
        });
        
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Complete.");
        }
    });    
}

外层dispatch不论使用串行队列还是并发队列,由于只有一个任务,只会有一个线程来执行这个block块的内容,而同步和异步的区别就在于是否会阻塞当前线程,接下来看block块的内容,采用了三个异步提交到并发队列,所以并发队列里就有了三个不同的任务,就可以真正执行并发,由于都是异步提交没有阻塞当前线程,所以输出Complete的代码也会掺杂在Task1-3中乱序输出。

dispatch_apply

通过上面的讲解就已经很清楚的了解了GCD同步/异步提交到串行/并发队列的执行过程了。接下来再继续讲解几个常用的方法,再举个栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //执行该方法的是主线程,不能传入主队列否则会死锁,后文会讲解
    dispatch_apply(20000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
        NSLog(@"Task %@ %ld", [NSThread currentThread], t);
    });
}

dispatch_apply方法是以同步方式提交执行任务,这里传入了一个全局的并发队列,因此讲道理重复执行任务时就应该有多个线程并发执行,但是不管我迭代多少次运行多少次都只有一个输出是其他线程输出的,剩余的都是同一个线程输出,有懂的读者可以留言讲解一下。如果传入的是串行队列,那么迭代就是按照顺序依次执行。

dispatch_after

再看一个栗子:

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"Before");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"In %@", [NSThread currentThread]);
    });
    NSLog(@"After");
}

dispatch_after是在when时间点异步提交任务,所以不会阻塞当前线程,这里设置的时间点是当前时间的5s后,观察输出:

2017-10-19 17:44:19.274305+0800 StudyOCTest[25385:13635522] Before
2017-10-19 17:44:19.274365+0800 StudyOCTest[25385:13635522] After
2017-10-19 17:44:24.745273+0800 StudyOCTest[25385:13635542] In <NSThread: 0x1c027ae40>{number = 3, name = (null)}

可以看出这个5s并不是精确的5s,因为该方法是在when时间点到达的时候去提交任务到队列,所以是延迟提交,而不是延迟执行,队列什么时候安排线程去执行是未知的,所以不要用这个方法去实现定时器这样的功能。

dispatch_barrier _ (a)sync

该方法用于阻塞队列,举个栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task0 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
        }
    });
}

上面的输出是按照Task0 Task1并发执行,Task2等待Task0 Task1执行完成后单独执行, 最后Task3 Task4等待Task2执行完成后开始并发执行 。

这里需要讲解一下阻塞队列的概念,前文讲过不论是并发队列还是串行队列都是使用FIFO 先进先出的方式管理的,队列会从队首获取要执行的任务并交由对应线程处理,串行队列只有一个线程所以是顺序执行,并发队列有多个线程,但获取任务依旧是FIFO按顺序获取,只是执行时有多个线程。阻塞线程即,获取一个任务后,这个任务必须要执行完成才能获取下一个任务,所以不管是并发还是串行队列,都得等前一个任务完成了才能从队列中获取下一个任务,这样就不难理解输出结果了,上述栗子改成串行队列结果也是一样的,如果使用同步提交效果也是一样的,读者可以自行尝试,篇幅问题不再赘述了。

dispatch_barrier_async方法常与并发队列共用,前一段任务使用dispatch_async异步并发执行,然后插入一个dispatch_barrier_async执行一个中间任务,这个中间任务必须要等待前面的并发任务执行完成后才能开始执行,接着这个中间任务完成后,继续异步并发执行接下来的任务。

dispatch_once

该方法能够保证在应用的生命周期内只执行一次提交的任务,所以常用于单例类的创建,举个单例类的栗子如下:

@interface MyUtil: NSObject <NSCopying>

+ (instancetype)sharedUtil;

@end

@implementation MyUtil

static MyUtil *staticMyUtil = nil;

+ (instancetype)sharedUtil
{
    //保证初始化创建只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticMyUtil = [[MyUtil alloc] init];
    });
    return staticMyUtil;
}

//防止通过alloc或new直接创建对象
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    //保证alloc函数只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticMyUtil = [super allocWithZone:zone];
    });
    return staticMyUtil;
}

//实现NSCopying协议的方法,防止通过copy获取副本对象
- (instancetype)copyWithZone:(NSZone *)zone
{
    return staticMyUtil;
}

@end

dispatch_once函数需要传入一个long类型的predicate,这个值必须是独一无二的,使用静态变量的地址最合适不过了,MyUtil实现了NSCopying协议的copyWithZone:方法,防止通过copy方法获取副本对象。

当使用alloc&&init方法初始化时,先调用allocWithZone:方法来分配存储空间,如果再次使用sharedUtil方法来获取的话,由于没有执行过,会执行到dispatch_once内部block,此时会再去执行allocWithZone:方法,但该方法内部dispatch_once已经执行过了会直接返回staticMyUtil,反过来调用是一样的道理,通过这样的方式就可以实现真正的单例了。

dispatch_ group_ t

dispatch_group_t是一个比较实用的方法,通过构造一个组的形式,将各个同步或异步提交任务都加入到同一个组中,当所有任务都完成后会收到通知,用于进一步处理,通过这样的方式就可以实现多线程下载,当下载完成后就可以通知用户了,举个简单的栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_group_notify(group, concurrentQueue, ^{
        NSLog(@"All Task Complete");
    });
}

像一个组中添加了三个异步的任务,最终三个任务完成后可以收到通知执行回调的block,上面的输出为,All Task Complete在前面三个输出都结束后才会输出。

防止GCD产生死锁

接下来将讲解一下GCD使用时可能会产生死锁的情况,首先举一个比较简单的栗子:

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"Before");
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"In");
    });
    
    NSLog(@"After");
}

上述代码就会产生死锁,分析下原因,首先,viewWillAppear:方法是在主线程中执行的,接着调用dispatch_sync方法,该方法会阻塞当前线程,也就是会阻塞主线程,主线程被阻塞是为了等待任务的完成,然后该代码将任务添加到了主队列,主队列会将任务交给主线程执行,但此时主线程阻塞了,任务添加进了主线程得不到运行,而主线程在等待任务的执行,因此就造成了死锁。

这个栗子一般人写不出来这样的代码,仅仅是为了讲解什么情况下会造成死锁,即,线程被阻塞需要等到任务执行完成,而任务由于线程阻塞得不到执行。前文举的几个串行队列的栗子很多是不能使用主队列的,原因也正在此。

再举个栗子:

dispatch_apply(1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
    dispatch_apply(2000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
        NSLog(@"===== %@ %ld", [NSThread currentThread], t);
    });
 }); 

上述栗子也会造成死锁,因为,dispatch_apply同样会阻塞当前线程,它需要等待内部的dispatch_apply执行完成,内部的需要等待外部的线程来执行它,产生了死锁。

可以看出死锁产生的条件一般都发生在同步执行方法中,所以,在使用同步执行方法时要避免任务再次派发到同一个线程中。

实现定时器的三种方法

定时器在开发中是比较常见的需求,常用的其实有三种方法:NSTimerGCD以及CADisplayLinkCADisplayLink是其中精度最高的,因为它试图与屏幕刷新率保持一致,由于涉及的内容比较多本小结只介绍基本使用方法,直接看栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //倒计时次数
    __block int count = 10;
    //间隔1s执行一次
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        //如果还在倒计时次数内
        if (count > 0)
        {
            //执行相关工作,如果有UI更新的操作需要放到主线程
            dispatch_async(dispatch_get_main_queue(), ^{
        
            });
            //次数--
            count --;
        }
        else
        {   
            //次数到达,取消定时器
            [timer invalidate];
        }
    }];
    //加入到RunLoop中,使用NSRunLoopCommonModes在滑动时也可以继续执行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

上面的栗子比较简单,使用了block的形式实现,也可以使用方法的形式执行:

- (void)viewWillAppear:(BOOL)animated
{
    self.count = 10;
    
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(countDown:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)countDown:(NSTimer*)timer
{
    if (self.count > 0)
    {
        //执行相关工作,如果有UI更新的操作需要放到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
        
        });
        self.count --;
    }
    else
    {
        //取消定时器
        [timer invalidate];
    }
}

以上比较重要的就是引用循环的问题,创建NSTimer传入的target对象,NSTimer会持有强引用,所以在重复执行NSTimer时一定要在任务结束后调用invalidate方法取消定时器打破引用循环,如果只执行一次可以不需要。

接下来看一下GCD如何实现:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:YES];
    //执行次数
    __block int count = 10;
    //获取一个全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //这里不能使用局部变量,因为当viewDidAppear函数返回后timer就会被释放
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置timer的执行时间和间隔时间,设置每秒执行一次
    dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), 1 * NSEC_PER_SEC, 0); 
    //设置timer执行事件的block块
    dispatch_source_set_event_handler(_timer, ^{
        if (count > 0)
        {
            //要执行的任务,更新UI需要放到主线程
            dispatch_async(dispatch_get_main_queue(), ^{
                
            });
            count --;
        }
        else
        {
            //执行次数达到预期就取消timer
            dispatch_source_cancel(_timer);
        }
    });
    //启动timer
    dispatch_resume(_timer);
}

上面代码比较简单,也可以传入一个C函数而不使用块,具体不再赘述了,有兴趣的读者可以自行实验。

最后看一下CADisplayLink的栗子:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:YES];
    
    self.count = 10;
    //CADisplayLink只有这一个构造方法
    CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(countDown:)];
    //每秒对多少帧感兴趣,也就是每秒要执行多少次回调方法
    timer.preferredFramesPerSecond = 1;
    //必须要添加进RunLoop才开始执行
    [timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

}

- (void)countDown:(CADisplayLink*)timer
{
    if (self.count > 0)
    {
        //执行相关工作,如果有UI更新的操作需要放到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
            
        });
        self.count --;
    }
    else
    {
        //取消定时器
        [timer invalidate];
    }
}

上面栗子不再赘述了,具体细节可以自行查阅。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

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

推荐阅读更多精彩内容