介绍多线程前先来理解下进程和线程的概念:
进程:一个在前台正在运行的应用程序就是一个进程。比如打开的微信APP就是一个进程。
线程:微信APP可以聊天,发图片,而做这些事情都是要通过线程来做的。线程就是执行任务的基本单元,是CPU调度和分派的基本单位。一个进程可以有多个线程,线程是进程的一部分。
多线程就是多个线程并发处理任务的技术,可以充分利用多核CPU的资源,提升执行性能。
iOS中处理和操作线程的方案有Pthreads
、NSThread
、GCD
、NSOperation && NSOperationQueue
。Pthreads
比较底层,没有用过,就不说了,主要来说下NSThread
、GCD
和NSOperation && NSOperationQueue
。
介绍之前先说下两个概念:同步/异步,串行/并发
同步
是指在执行任务的时候,会等待当前这个任务执行完毕后再继续向下执行。如果当前这个任务没有执行完毕,那么就会阻塞当前的线程直到这个任务执行完成。异步
是指在执行任务的时候,不会等待当前这个任务执行完毕就会继续向下执行。即使当前这个任务没有执行完,也会立刻执行下面的任务。
同步
和异步
的区别可以理解为:例如方法A内部中有个方法B,当前的方法A内部执行到了方法B,如果是同步
,会等这个方法B执行完返回后才会向下执行,如果方法B没有返回,当前的线程就会一直卡住不向下执行;如果是异步
的话这个方法B会立刻返回,然后继续向下执行。
//方法A
- (void)methodA {
....其他任务
[self methodB]
....其他任务
}
//方法B
- (void)methodB {
...
}
串行
:执行任务的时候按顺序执行,一次只能执行一个任务。当前的任务没有执行完,不会执行下一个。并发
:执行任务的时候可以多个任务同时执行。
NSThread
NSThread是一个直接面向线程的类,提供了很多可以操作线程的方法。最简单的创建一个线程:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doThead:) object:@"zzy"];
这样就创建了一个线程实例thread
,thread
通过doThead:
这个方法执行任务。但是这个时候创建的线程并没有启动执行任务,需要调用下start
方法:[thread start];
这样线程就开始执行任务了。object:@"zzy"
参数会在线程执行doThead:
方法的时候被作为参数传入。
我们也可以给一个线程设置一个名字作为标记:[thread setName:@"zzy"];
通过[NSThread currentThread]
获取当前线程的信息,打印出当前这个线程可以看到:
XXX[67222:788960] thread = {number = 3, name = zzy}
通过name
我们可以找到我们设置的是哪个thread
。
NSThread 也提供了一些关于线程的状态信息,例如:executing
,finished
,cancelled
,分别表示当前的线程正在执行,完成,取消的状态。但是cancelled
只是一个标记状态,并不会取消线程,如果想强制退出当前的线程,可以通过[NSThread exit];
。
NSThread的使用比较简单,主要是注意通过NSThread创建的子线程中指定NSTimer计时器的情况。由于子线程中的runloop默认不启动,所以当添加一个定时器重复执行任务的时候要指定启动runloop。
- (void)doThead:(NSString *)object {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRepeatAction:) userInfo:@{@"id":@"zzy"} repeats:YES];
[timer fire];
[[NSRunLoop currentRunLoop] run];
}
GCD
GCD是平时最常用的多线程处理的方案了,我们将任务放进block中追加到队列里,系统就会自动为我们创建相应的线程去处理任务,并且任务完成后在合适的时机销毁线程,整个过程中不需要我们去直接操作线程,所以使用起来比较方便。
GCD有三种队列:串行队列,并发队列,主队列(特殊的串行队列,队列中的任务一定会在主线程中执行)
GCD获取队列的方式有两种:
- 第一种通过
dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
函数直接创建队列,第一个参数的队列的名称,方便调试使用,第二个是队列的类型。传NULL
或DISPATCH_QUEUE_SERIAL
表示串行队列,传DISPATCH_QUEUE_CONCURRENT
表示并发队列。
dispatch_queue_t queue = dispatch_queue_create("zzy", NULL);
-
第二种是直接获取系统提供好的两个队列:
dispatch_get_main_queue
:主队列,运行在主线程中的串行队列dispatch_get_global_queue
:全局队列,也就是并发队列,通过这个函数的第一个参数还可以指定队列的优先级。
GCD中平时常用的有以下几个函数:
dispathc_once
:确保函数中的block只执行一次,而且是线程安全的。常用来实现单利对象。dispatch_after
:延迟指定的时间之后提交某个任务(这里是延迟某个时间提交任务,而不是延迟某个时间执行任务)。常用做一个定时操作。dispatch_suspend
&&dispatch_resume
:暂停和恢复某个队列dispatch_apply
:循环将某些任务加入到某个队列当中dispatch_set_target_queue
:可以设置目标队列的优先级,让目标队列的优先级和指定的队列优先级一样。(由于dispatch_queue_create
函数创建的队列没有提供设置优先级的参数,默认是默认的优先级,所以可以用这个函数来设置其他优先级,例如dispatch_set_target_queue(targetQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
)dispatch_barrier_async
:在一个并发队列中,先并行处理一部分任务,然后同步执行dispatch_barrier_async
中的任务(只执行当前block中的一个任务,其他任务不执行),dispatch_barrier_async
任务执行完成后,然后再恢复当前队列并行执行的任务。常用来处理在多线程数据读取的时候插入写入操作(写入操作必须是线程安全的)。
这里主要介绍下平时处理异步任务的时候比较有用的任务组dispatch_group_t
和信号量dispatch_semaphore_t
。
dispatch_group_t
dispatch_group_t
是一个任务组,可以将几个并发任务一起放到任务组里面,当这几个并发任务都执行完成后,同步得到通知回调。涉及到两个常用的函数:dispatch_wait
和dispatch_notify
。
dispatch_wait
是一个同步的函数,一旦被调用该函数就会一直处于调用的状态而不返回,直到dispatch group内的任务都执行完成或者经过dispatch_wait
中第二参数指定的时间后它才会返回,否则它会一直阻塞当前的线程,无法继续执行。如果dispatch_wait
函数返回值为0,说明group内的任务已经执行完毕;如果返回值不为0,说明经过了指定的时间group内的任务依然没有执行完。
- (void)dispathGroup {
/**
使用dispatch_group_t也可以向group中指定不同优先级的队列,不一定非要是同一个队列,他们都归属同一个group
*/
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
});
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
});
dispatch_group_async(group, highQueue, ^{
NSLog(@"高优先级任务");
});
dispatch_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"任务执行完成");
}
执行后:
2019-01-01 10:09:53.092345+0800 XXX[12671:1433314] 高优先级任务
2019-01-01 10:09:55.094704+0800 XXX[12671:1433327] 任务二
2019-01-01 10:09:55.094704+0800 XXX[12671:1433313] 任务一
2019-01-01 10:09:55.095105+0800 XXX[12671:1433235] 任务执行完成
可以看到dispatch_wait
会一直阻塞当前的线程,直到任务执行完成(这里指定的时间是永久等待,也可以自己定义临界时间)。
而dispatch_notify
提供了一个异步执行的函数,它不会阻塞当前的线程,但是会监听dispatch group的任务执行情况,一旦group内的都执行完成后就会调用dispatch_notify
函数。dispatch_notify(object, queue, notification_block)
函数提供了三个参数,第一个参数就是监听的dispatch group,另外两个参数提供了可以让某个任务在指定队列中执行的功能。也就说当dispatch group内的任务都完成后会通知dispatch_notify
然后可以进行一些完成后的处理操作。
- (void)dispathGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
});
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
});
dispatch_group_async(group, highQueue, ^{
NSLog(@"高优先级任务");
});
dispatch_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"dispatch group执行完成");
});
NSLog(@"任务执行完成");
}
执行后:
2019-01-01 10:28:40.864817+0800 XXX[12801:1454449] 任务执行完成
2019-01-01 10:28:40.864821+0800 XXX[12801:1454557] 高优先级任务
2019-01-01 10:28:42.865759+0800 XXX[12801:1454555] 任务二
2019-01-01 10:28:42.865760+0800 XXX[12801:1454558] 任务一
2019-01-01 10:28:42.866070+0800 XXX[12801:1454449] dispatch group执行完成
dispatch_notify
函数提供的这种获取dispatch group内任务完成后的通知,然后再异步执行处理很常用。
如果对同步、异步理解的不够深刻的话,使用dispatch_group_async
函数的时候可能会导致一个错误的认识。比如在dispatch_group_async
中添加几个网络请求的任务,会发现请求还没完成就执行了dispatch_notify
回调。这是因为发起网络请求本身就是一个任务,在把这个任务通过block追加到dispatch_group_async
中的队列后就算完成了,并不会等待网络请求的完成,因为网络请求本身也是异步的。所以在遇到这种异步操作情况的时候,可以用另外一对函数将来判定任务完成情况:dispatch_group_enter
(添加到group中)、dispatch_group_leave
(执行完成退出group)。
这两个函数必须同时存在,类似于引用计数,这是对group内的任务的递增和递减,如果只有递增没有递减,那么group的任务就会永远执行不完,也就一直不会回调dispatch_notify
函数,或者dispatch_wait
函数一直不会返回。
- (void)dispathGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_enter(group);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(highQueue, ^{
NSLog(@"高优先级任务");
dispatch_group_leave(group);
});
dispatch_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"dispatch group执行完成");
});
NSLog(@"任务执行完成");
}
dispatch_semaphore_t
dispatch_semaphore_t
信号量一共就三个函数,用起来比较简单,只是需要结合不同的场景去理解才能发挥很大的用处:
dispatch_semaphore_create
: 创建一个信号量,并指定信号的初始化个数dispatch_semaphore_wait
:锁住当前的线程,等待信号的计数大于等于1,然后将计数减去1,并且该函数返回。如果信号是0,则该函数一直不返回,阻塞当前的线程。dispatch_semaphore_signal
:释放信号,让信号计数加1
在很多开源库中都使用dispatch_semaphore_t
作为锁来处理多线程时对数据写入的保护。例如下面示例代码
- (void)dispatchSemaphore {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
});
}
}
在一个异步并发线程中,对数组进行添加元素的操作。由于会有多个线程同时对array进行写入操作,如果不加dispatch_semaphore_t则很可能会导致内存访问错误导致程序终止。
加了dispatch_semaphore_t
后,将信号的计数指定为1,每当执行一次dispatch_semaphore_wait
函数后,信号就会减1,此时信号为0,则阻塞线程无法执行下面的[array addObject:[NSNumber numberWithInt:i]];
方法,直到前面的线程执行完对数组的操作然后执行dispatch_semaphore_signal
函数使信号加1,当前的线程才能访问数组,这样一来,每次对数组的访问操作都只能有一个线程,就保护了数据访问的安全。当然对本例也可以用其他方法来处理,例如指定一个串行队列。
通过dispatch_semaphore_t
信号量和dispatch_group_t
的组合,也能实现代替上面例子中使用dispatch_group_enter
、dispatch_group_leave
的效果。
- (void)dispatchSemaphoreGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, defaultQueue, ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
dispatch_group_async(group, defaultQueue, ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
dispatch_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"dispatch group执行完成");
});
NSLog(@"任务执行完成");
}
在上面的代码中,每次追加block中的任务到group的时候,都先创建一个信号计数为0的信号量,然后开启一个block内部的异步任务的执行,在block的最后使用dispatch_semaphore_wait
锁住当前的线程,让当前的block无法返回,则group的任务就一直无法完成。直到block内部的这个异步任务执行完成后释放信号,通过dispatch_semaphore_signal
让信号加1,则block返回,group内的任务完成,再回调dispatch_notify
函数。
信号量在网络请求的同步处理,资源竞争等情况下可以发挥很大的用处,可以使用信号量来解决这些问题。
NSOperation && NSOperationQueue
相对于GCD都是纯C的函数,NSOperation提供了一个更高层面面向对象的多线程处理方案。NSOperation是用来封装任务的一个类。我们把需要执行的任务封装进NSOperation实例当中,通过调用start
方法它会自动去执行这些任务。它也提供了很多种的状态便于我们观测了解当前任务的执行状态,例如:isCancelled
,isReady
,isExecuting
,isFinished
等。NSOperation是一个抽象类,我们不能直接使用NSOperation类的去处理任务,系统提供了两个子类NSInvocationOperation
和NSBlockOperation
方便我们使用,也可以自己创建一个NSOperation的子类去实现。
NSInvocationOperation
提供了一种target - action
模式响应的处理任务类,通过指定target
,去对应执行action
操作。例如:
- (void)invocationOperationAction {
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationAction:) object:@"zzy"];
[operation start];
}
- (void)invocationOperationAction:(id)userInfo {
NSLog(@"userInfo = %@", userInfo);
}
NSBlockOperation则提供了block的方式去处理任务,通过把任务封装进block块中对应去执行。
NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation1");
}];
[operation start];
还可以给operation中添加多个任务:
[operaion addExecutionBlock:^{
NSLog(@"operation2");
}];
[operaion addExecutionBlock:^{
NSLog(@"operation3");
}];
[operation start];
只有当这些添加的Block中的任务都执行完,这个operation才算执行完成。
需要注意的是,如果通过手动执行NSOperation的start
方法去启动任务的话,start
方法是同步
的。NSOperation提供了一个只读属性isAsynchronous
(也可以通过concurrent
)来标识当前NSOperation是否是异步执行,默认这个属性是NO。如同上面关于同步
的解释,start
方法会阻塞当前调用它的线程,直到operation的操作都完成。看下例子:
- (void)blockOperationThread {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(synchronousOperation) object:nil];
[thread start];
}
- (void)synchronousOperation {
NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"operationBlock1");
NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
}];
[operaion addExecutionBlock:^{
NSLog(@"operationBlock2");
NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
}];
[operaion start];
NSLog(@"synchronousOperation....");
}
打印如下:
2019-01-01 11:17:30.504408+0800 XXX[11341:1269966] operationBlock2
2019-01-01 11:17:30.504966+0800 XXX[11341:1269966] operation2 Thread = {number = 4, name = (null)}
2019-01-01 11:17:32.506224+0800 XXX[11341:1269987] operationBlock1
2019-01-01 11:17:32.506513+0800 XXX[11341:1269987] operation1 Thread = {number = 3, name = (null)}
2019-01-01 11:17:32.506980+0800 XXX[11341:1269987] synchronousOperation....
可以看到直到两秒之后才执行NSLog(@"synchronousOperation....");
也就是说NSOperation的执行是同步的。同时我们通过打印执行NSOperation任务的线程发现,同一个operaion内添加的多个任务,执行这些任务的线程并不是同一个。也就是说虽然NSOperation是同步的,但是operaion内部添加的任务可能是在不同线程中并发执行的,这些任务执行完成后才会向下执行,有点类似GCD的dispatch_group
。
那么如何实现异步执行的NSOperation呢?可以通过两种方式:一种是通过创建NSOperation子类自己去实现异步操作,另外一种是通过NSOperationQueue。我们平时用的比较多的是通过使用NSOperationQueue来实现异步处理NSOperation。
NSOperationQueue
通过NSOperationQueue
来实现NSOperation
的执行比较简单,直接将NSOperation
放进NSOperationQueue
的队列中就可以了。NSOperationQueue
会自动为NSOperation
创建线程并且调用它的start
方法启动任务。当然NSOperation
添加到NSOperationQueue
中后也不一定会立刻被执行,如果NSOperationQueue
中有很多个operation,当前的operation会在队列排到它以后自动执行。
通过NSOperationQueue来执行operation:
//将上面的代码改为NSOperationQueue来执行
- (void)synchronousOperation {
NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"operationBlock1");
NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
}];
[operaion addExecutionBlock:^{
NSLog(@"operationBlock2");
NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
}];
// [operaion start];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operaion];
NSLog(@"synchronousOperation");
}
打印如下:
2019-01-01 11:42:41.789280+0800 XXX[11463:1298262] synchronousOperation
2019-01-01 11:42:41.789611+0800 XXX[11463:1298233] operationBlock2
2019-01-01 11:42:41.791127+0800 XXX[11463:1298233] operation2 Thread = {number = 4, name = (null)}
2019-01-01 11:42:43.790340+0800 XXX[11463:1298241] operationBlock1
2019-01-01 11:42:43.790851+0800 XXX[11463:1298241] operation1 Thread = {number = 5, name = (null)}
可以看到operation不会阻塞当前的线程,会自动的异步去执行任务。
自定义NSOperation
通过自定义NSOperation可以实现更多的定制化任务处理,例如异步执行NSOperation。自定义NSOperation要实现以下几个方法:
start
:在start方法中实现异步处理的操作,不论是调用异步处理函数还是说开启新的线程。而且要及时通过KVO更新operation当前的状态:isExecuting
,isFinished
,以便于通知它的监测者。main
官方文档中建议在这个方法中实现执行任务的操作。当然也可以不实现这个方法,直接在start
中实现处理任务也可以。SDWebImage
中的自定义operation就只实现了start
方法。isConcurrent
是否是并发,返回YES即可。isExecuting
:是否正在执行isFinished
是否完成,包括任务完成和取消,取消也是Finished
。
我自己没有实现过自定义NSOperation,之前看SDWebImage
的时候看过里面自定义SDWebImageDownloaderOperation
类实现了异步处理图片下载请求,写了很多东西,要控制各种状态,并自行手动实现KVO(因为以上几种状态都是readonly),感兴趣的可以参考SDWebImage。
添加任务依赖
NSOperation可以指定任务间的依赖,一个任务A可以依赖于另外一个任务B,任务B没有执行完成,任务A不会开始执行。这里要注意的是,任务B不一定非要是执行成功,因为取消也算是完成isFinished
。
[operation2 addDependency:operaion1];
添加依赖一定要在operation 执行start方法或者添加到NSOperationQueue前,否则无效。
而且这两个operation不一定非要是在同一个NSOperationQueue中,不同的NSOperationQueue中的operation也可以指定依赖。
- (void)blockOperationAction {
NSBlockOperation *operaion1 = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"operation1");
NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation2");
NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
}];
[operation2 addDependency:operaion1];
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
[queue1 addOperation:operaion1];
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
[queue2 addOperation:operation2];
}
打印如下:
2019-01-01 12:29:44.008728+0800 XXX[11812:1350215] operation1
2019-01-01 12:29:44.009073+0800 XXX[11812:1350215] operation1 Thread = {number = 3, name = (null)}
2019-01-01 12:29:44.009623+0800 XXX[11812:1350214] operation2
2019-01-01 12:29:44.009877+0800 XXX[11812:1350214] operation2 Thread = {number = 4, name = (null)}
NSOperation还有很多功能:
可以通过
queuePriority
属性指定在队列中的优先级;可以通过
setCompletionBlock:
方法设置任务完成的回调;可以手动
cancel
取消掉一个任务;也可以通过调用NSOperationQueue
的cancelAllOperations
方法取消队列中所有的任务。可以通过
NSOperationQueue
的suspended
来暂停队列中的任务。不过它只是暂停operation queue调度新的任务,并不会暂停正在执行的任务。还可以通过
NSOperationQueue
的maxConcurrentOperationCount
属性来设置operation queue中任务的最大并发数。如果设置为1那就是串行执行。但是这里的串行执行顺序并不一定,要看当然operation的优先级和isReady
的状态。
NSOperation提供的对于任务的控制很丰富很灵活,可以做很多事情。