多线程

288 阅读12分钟

前言

平时常用的多线程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也没用,需要重新添加任务