前言
平时常用的多线程api有多个:NSThread、GCD、NSOperation
尤其是GCD和NSOperation,功能非常之强大
提到线程就会提到进程,下面简单介绍下区别:
进程:进程是操作和分配资源和独立运行的最小单位,也是程序执行的最小单位,可以说是一个独立的app应用程序,每个进程都有一片独立的内存,使得各个应用之间产生隔离
线程:线程是应用程序执行的最小单位,一个进程包含有多个线程,并与之绑定,一旦某一个线程出现问题,他所在的进程也会出现问题出现crash
并发、并行: 并行,n个核心线程对应n个任务同时执行;并发,n个线程,大于n个任务,以时间片的形式轮流执行,达成类似并行的效果
注意:无论是哪套API都是操作线程的工具,线程本身的属性都是一样的,例如:开始、结束、是否是主线程等
NSThread
NSThread为一套OC的多线程API,使用非常简单
开启一个线程
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
//实现如下所示
NSThread detachNewThreadWithBlock:^{
NSLog(@"我是开启的block子线程");
}];
[NSThread detachNewThreadSelector:@selector(selectorThread) toTarget:self withObject:nil];
- (void)selectorThread {
NSLog(@"我是通过开启的Selector子线程");
}
切换线程运行
//回到主线程运行某个方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
//进入到子线程运行某个方法
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
//切换到指定线程运行某个方法
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
//回到主线程运行
[self performSelectorOnMainThread:@selector(selectorThread) withObject:nil waitUntilDone: NO];
//进入到子线程运行
[self performSelectorInBackground:@selector(selectorThread) withObject:nil];
//切换到指定线程执行
[self performSelector:@selector(selectorThread) onThread:[NSThread mainThread] withObject:nil waitUntilDone: NO];
线程开始、结束和状态
//线程是否正在执行中
@property (readonly, getter=isExecuting) BOOL executing;
//线程结束
@property (readonly, getter=isFinished) BOOL finished;
线程处于被取消状态,注意线程被取消后不会立即结束,而是等当前任务执行完毕后才执行清理操作
@property (readonly, getter=isCancelled) BOOL cancelled;
+ (void)exit; //直接强制结束线程的运行,但可能任务没执行完毕,造成内存泄露等问题,不推荐
- (void)cancel; //取消线程,该线程不会立即结束,标记为cancelled状态,然后执行完毕当前任务后,开始清理
- (void)start; //开始线程
注意:NSThread的一些通用方法,例如exit、cancel、isExecuting、cancelled、finished、name、isMainThread等等,在以其他方式创建的线程中照样使用,只不过是操作的api不通罢了
GCD
GCD,全名Grand Central Dispatch,是一套非常强大的基于c语言的API,下面就来领略下他的强大吧
队列queue
创建的队列,开启线程要以队列为基础进行,从队列分发任务到一个或者多个线程中执行
队列分为串行队列和并发队列,即独木桥和宽敞的大桥的区别
注意通过dispatch_get_global_queue获取的队列也为并发队列,但任务也包括一些系统的任务,因此后续的一些操作在这里可能不是很好使
dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL); //串行队列,里面的任务只能一个个走
dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT); //并发队列,里面的任务可以齐头并进
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//全局队列,除了自己任务还有别的任务
开启线程sync、async
注意:只有异步任务放到并发队列中才会并发执行;在同一个串行队列中,同步开启执行另一个任务,会因为资源抢夺而陷入思索,即无法正常执行(例如主线程中开启一个串行队列到主线程执行,会死锁)
将任务放到队列中,以同步的方式执行(即串行执行)
dispatch_sync(queue, ^{
});
将任务放到队列中,以异步的方式执行(即并发执行)
dispatch_async(queue, ^{
});
//此过程会陷入死锁
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"我在主线程中串行执行任务");
});
多次执行apply
调用此方法会重复执行设置次数的回调,如果放到并发队列,则多次执行顺序不确定
int a = 10;
dispatch_apply(10, queue, ^(size_t) {
NSLog(@"%d", a++);
});
分组功能group
分组功能可以将队列中的任务分到一组中,并监视任务的执行情况,当分组内任务执行完毕后会执行notifiy的回调方法
注意:任务要放到一个group中去,组内任务完成执行会执行回调
dispatch_group_notify:监听组内任务执行完毕后的回调,下面可以看到,分组的观察可所在队列无关
dispatch_group_async:
//创建分组
dispatch_group_t group = dispatch_group_create();
将任务1加入分组中,并监听任务1执行
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"我开始执行任务1");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"我执行完了1");
});
});
将任务2加入分组中,并监听任务2执行
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"我开始执行任务2");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"我执行完了2");
});
});
//所有任务都执行完毕后的回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任务都执行了");
});
log:
2021-03-03 16:41:20.932220+0800 001---多线程[2690:122386] 我开始执行任务1
2021-03-03 16:41:20.932223+0800 001---多线程[2690:122391] 我开始执行任务2
2021-03-03 16:41:20.945339+0800 001---多线程[2690:122232] 任务执行完毕了
2021-03-03 16:41:22.030228+0800 001---多线程[2690:122232] 我执行完了1
2021-03-03 16:41:22.030369+0800 001---多线程[2690:122232] 所有任务都执行了
下面可以看到上面任务执行的结果,可以发现如果任务不是异步的,监听正常,一旦出现异步耗时的操作,则监听正常
因此引出了group的enter和leave操作,如下所示
//创建分组
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
NSLog(@"我开始执行任务1");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"我执行完了1");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
NSLog(@"我开始执行任务2");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"我执行完了2");
dispatch_group_leave(group);
});
//所有任务都执行完毕后的回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任务都执行了");
});
log:
2021-03-03 16:45:39.174988+0800 001---多线程[2743:124474] 我开始执行任务1
2021-03-03 16:45:39.175123+0800 001---多线程[2743:124474] 我开始执行任务2
2021-03-03 16:45:40.175255+0800 001---多线程[2743:124474] 我执行完了1
2021-03-03 16:45:40.175483+0800 001---多线程[2743:124474] 我执行完了2
2021-03-03 16:45:40.175699+0800 001---多线程[2743:124474] 所有任务都执行了
可以看到enter和leave的使用更为灵活,异步操作(例如:网络请求)中使用更为灵活
信号量semaphore
信号量比较特殊,介绍使用之前,先介绍机制
当信号量设置的值小于0时,会阻塞当前线程;当信号量值大于等于0是,会恢复当前线程的使用
因此信号量的使用一般是控制代码块访问的线程数量,一般设置为1,可以保证代码块只会被多线程访问一次
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); //创建信号量,默认为1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //信号量值减少1
dispatch_semaphore_signal(semaphore); //信号量值增加1
//创建一个单例对象,避免多线程下被多次创建访问,一般用于锁定模块中经常读写的公共集合
static id instance = nil;
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //信号量值减少1
if (!instance) {
instance = [[NSObject alloc] init];
}
dispatch_semaphore_signal(semaphore); //信号量值增加1
栅栏barrier
栅栏函数可以把同一个队列的多个方法给给前后隔离开,等栅栏前加入的任务都执行完毕后,才执行栅栏后的任务
注意要使用自己创建的队列,global队列由于是全局的,不仅有加入当前的任务,还有一些系统的任务,因此使用栅栏函数会存在问题
创建自己的线程
dispatch_queue_t queue = dispatch_queue_create("测试使用", DISPATCH_QUEUE_CONCURRENT);
栅栏前的函数执行
dispatch_async(queue, ^{
NSLog(@"执行了1");
});
dispatch_async(queue, ^{
NSLog(@"执行了2");
});
中间放置栅栏函数,同步和异步方法一样,都是先同步执行栅栏函数的任务,在执行后面的任务
dispatch_barrier_async(queue, ^{
NSLog(@"我是屏障,我可以把前后的任务隔开执行");
});
注意:如果放置了global线程中,sync栅栏任务会被提前执行,就挡不住了
// dispatch_barrier_sync(queue, ^{
// NSLog(@"我是屏障,我可以把前后的任务隔开执行");
// });
栅栏后的函数执行
dispatch_async(queue, ^{
NSLog(@"执行了3");
});
dispatch_async(queue, ^{
NSLog(@"执行了4");
});
log:
2021-03-03 17:01:44.980580+0800 001---多线程[2797:131038] 执行了1
2021-03-03 17:01:44.980614+0800 001---多线程[2797:131037] 执行了2
2021-03-03 17:01:44.980832+0800 001---多线程[2797:130880] 我是屏障,我可以把前后的任务隔开执行
2021-03-03 17:01:44.980979+0800 001---多线程[2797:131038] 执行了3
2021-03-03 17:01:44.980989+0800 001---多线程[2797:131037] 执行了4
延时执行after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"延时执行一个任务,设置单位为s");
});
计时器timer
//计时器,可以设置间隔执行回调,和timer用法一样,优点比timer精准
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//第二个参数设置执行间隔,第三个参数设置误差,一般为0,较为精准
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"定时执行一个任务");
});
dispatch_resume(timer);
NSOperation
NSOperation是一套基于OC的api,使用起来不像GCD那样生涩,且使用非常之方便
NSOperation、NSInvocationOperation、NSBlockOperation
NSInvocationOperation、NSBlockOperation他们两个都是继承自NSOperation,因此都拥有这NSOperation的基本功能,唯一不同的是,他们一个是通过Selector的方式添加操作,一个是通过block的方式添加操作
因此也可以自行继承NSOperation来实现自定义NSOperation
NSBlockOperation:
NSBlockOperation *ob = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
//添加执行代码块
[ob addExecutionBlock:^{
NSLog(@"这是一个执行代码块 - %@",[NSThread currentThread]);
}];
[ob addExecutionBlock:^{
NSLog(@"这是一个执行代码块2 - %@",[NSThread currentThread]);
}];
[ob setCompletionBlock:^{
NSLog(@"都执行完毕后的回调");
}];
[ob start]
NSInvocationOperation:
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
[op start];
- (void)handleInvocation {
NSLog(@"这是一个执行代码块");
}
通过上面可以直接创建任务代码块,可以通过start方法直接执行任务
注意:通过NSOperation加入的任务(代码块),都会加入到并发队列中,异步执行的,一次加入多个,那么多个任务都是异步执行的,因此无法管理,如果执行完毕后,不可以加入到NSOperationQueue中,会崩溃
下面看看NSOperation的基本方法
addDependency:添加依赖
可以重复添加,也就意味着可以有多个依赖(GCD中的group是不是就代替了),可以将多个NSOperation对象关联起来,被依赖对象里面的任务会被先执行,依赖的会被后执行
removeDependency: 移除依赖
queuePriority:任务队列执行优先级,即先后顺序
start、cancel运行和取消队列中任务的执行,自定义NSOperation时,可以重写这个两个方法来处理取消等情境(例如:YYWebImageOperation)
completionBlock: operation中任务都执行完毕后的回调(对应group_notify的回调)
NSOperationQueue
看到上面的NSOperation功能很多,NSOperationQueue可以完美解决线程同步运行等问题
NSOperationQueue为操作队列可以操控NSOperation的执行,NSOperation在Queue中,都是在此队列中按照其指定规则执行NSOperation中的任务
该队列中有一个非常重要的属性maxConcurrentOperationCount,通过设置该数量可以设置NSOperation的最大并发数量(每个NSOperation都会一个的在这里面运行)
当maxConcurrentOperationCount参数设置为1时,那么该队列就变成了NSOperation的串行队列,加入的NSOperation只能按照顺序执行(每个NSOperaation中一个任务时,则会串行执行);当队列设置大于1是,则可以并发指定数量的NSOperation(NSOperation中如果多个任务,那么Operation中的任务顺序无法控制)
注意:NSOperationQueue只能同步管理NSOperation,如果NSOperation中的有多个乱序任务,那么这个NSOperation中的多个任务仍然是乱序的
创建一个操作对象
NSBlockOperation *ob = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
//添加执行代码块
[ob addExecutionBlock:^{
NSLog(@"这是一个执行代码块 - %@",[NSThread currentThread]);
}];
NSBlockOperation *ob2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
[ob2 addExecutionBlock:^{
NSLog(@"这是一个执行代码块2 - %@",[NSThread currentThread]);
}];
加入到队列中,会立即执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1; 设置队列数量,当为1时为同步队列
[queue addOperation:ob];
[queue addOperation:ob2];
另外NSOperationQueue也可以直接添加block运行,并且完全接受maxConcurrentOperationCount的控制
addOperationWithBlock:(void (^)(void))block
suspended
队列处于挂起状态,即停止运行(已经运行起来的无法停止),挂起的队列可以重新start
cancelAllOperations 可以取消队列中的所有NSOperation运行(已经在运行的无法停止),因为队列都被移除了,所以重新start也没用,需要重新添加任务