目前最普遍被认知的几种多线程有:程序员
- pthread
- NSThread
- GCD
- NSOperation
其中经常使用的是后面两种:GCD 和 NSOperation,这边文章就是对后两种常见用法的简单介绍,以及前两种 pthread 和 NSThread 的简单说明。编程
一套通用的多线程API,适用于Unix\Linux\Windows等系统,跨平台,可移植性强,由纯C语言编写的API,且线程的生命周期须要程序员本身管理,使用难度较大,因此在实际开发中一般不使用,在这里也不详细说明。数组
注意:在使用pthread的时候必定要手动把当前线程结束掉。若是有想从底层进行定制多线程的操做,可使用ptherad。安全
基于OC语言由苹果进行封装的API,使得其简单易用,彻底面向对象操做。线程的声明周期由程序员管理,在实际开发中偶尔使用。bash
- 使用NSThread的init方法显式建立
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
//线程名
[thread setName:@"thread"];
//优先级 优先级从0到1
[thread setThreadPriority:0.9];
//启动
[thread start];
复制代码
- 使用NSThread类方法显式建立并启动线程
[NSThread detachNewThreadSelector:@selector(threadMethod:) toTarget:self withObject:nil];
复制代码
- 隐式建立并启动线程
[self performSelectorInBackground:@selector(threadMethod:) withObject:nil];
复制代码
注意:添加线程的名字、更改优先级等操做,要使用第一种方式来建立线程。由于只有使用NSThread的init方法建立的线程才会返回具体的线程实例,此方法须要使用start方法来手动启动线程。多线程
- 启动
[thread start];
复制代码
- 阻塞
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[NSThread sleepForTimeInterval:1];
复制代码
- 结束(注意:当使用cancel方法时,只是改变了线程的状态标识,并非结束线程,要配合isCancelled方法进行判断,退出线程使用)
[[NSThread currentThread] cancel];
if([[NSThread currentThread] isCancelled]) {
[NSThread exit];//执行exit,后边的语句再也不执行,能够经过 start 再次启动线程
}
if([[NSThread currentThread] isCancelled]) {
return;//后边的语句再也不执行,不能够经过 start 再次启动线程
}
复制代码
线程间通讯,最经常使用的就是开启子线程进行耗时操做,操做完毕后回到主线程,进行数据赋值以及刷新主线程UI。并发
[self performSelectorOnMainThread:@selector(backToMainThread:) withObject:image waitUntilDone:YES];
复制代码
[self performSelector:@selector(backToMainThread:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
复制代码
多线程操做会存在必定的安全隐患。缘由是多线程会存在不一样线程的资源共享,也就是说咱们可能在同一时刻两个线程同时操做了某一个变量的值(资源竞争),可是线程的对变量的操做不一样,致使变量的值出现偏差。app
例如:若是有一个变量x = 100,有两个线程A和B,A线程取x的值(x=100),B线程取x的值(x=100),B线程给x+1 (x=101),A线程给x+1 (x = 101),B 线程取x的值 (x = 101)或者( x = 102 )。变量出行了偏差。异步
解决方案添加线程锁,有多种线程锁,在这里很少介绍。async
基于C语言编写由苹果公司提供的的一套多线程开发解决方案,使用时会以函数形式出现,且大部分函数以dispatch开头。它会自动利用多核进行并发处理和运算,它能提供系统级别的处理,而再也不局限于某个进程、线程,线程的生命周期由系统自动管理(建立,调度、运行,销毁),只须要告诉GCD执行什么任务,不须要编写管理线程的代码。
- 串行列队(一次执行一个任务)
dispatch_queue_t queue = dispatch_queue_create("10900900",DISPATCH_QUEUE_SERIAL);
复制代码
- 并发列队(一次可执行多个任务)
dispatch_queue_t queue = dispatch_queue_create("10900901",DISPATCH_QUEUE_CONCURRENT);
复制代码
- 全局列队(本质是一个并发队列,由系统提供,全部应用程序共享的,方便编程,能够不用建立就直接使用)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
复制代码
- 主列队(专门调度主线程的任务,不开辟新的线程。在主队列下的任务不论是异步仍是同步都不会开辟新线程,任务只会在主线程顺序执行)
dispatch_queue_t queue = dispatch_get_main_queue();
复制代码
- 同步任务(同步串行和同步并发,任务执行的方式是同样的,没有开辟新的线程,全部的任务都是在一条线程里面执行。)
dispatch_sync(queue, ^{
});
复制代码
- 异步任务(异步串行和异步并发,任务执行的方式是有区别的,异步串行会开辟一条新的线程,队列中全部任务按照添加的顺序一个一个执行,异步并发会开辟多条线程,至于具体开辟多少条线程,是由系统决定的。)
dispatch_async(queue, ^{
});
复制代码
- 全部任务都是在当前线程中执行,没有开启新的线程(同步方法不具有开启新线程的能力)
- 同步任务须要等候列队中的任务执行结束,才会执行下一个
- 并发列队能够开启多线程,而且能够同时执行多个任务,可是同步任务没法建立新线程,因此只有当前一个线程,并且同步任务须要等待列队中前一任务执行结束才能继续执行下面的操做,所以任务只能一个一个顺序执行
- 和同步任务,并发列队类似
- 全部任务在当前线程中执行,没有开启新的线程
- 任务是按照顺序执行的,同步任务,线程须要等待列队中的任务执行完毕,才能够开启新的任务
- 在主线程中调用会出现死锁,互相等待
- 死锁缘由:当咱们在主线程中添加这个列队的时候,添加列队的这个操做自己就是一个任务,咱们把它看成任务A,这个任务也被添加到了主线程的列队中。而同步任务,会等待当前列队中前面的任务执行完毕后接着执行,咱们把添加到主线程中的列队中的任务称为任务B,这就产生了一个矛盾,任务B要执行须要等任务A执行完毕后才会执行,而任务A执行完毕须要任务B执行结束(由于任务B在任务A中),这就产生了任务互相等待的状况
- 有几个异步任务就开启了几个新的线程,任务也是同时执行的(异步方法具有开启新线程的能力,能够同时执行多个任务)
- 异步执行,当前线程不等待,直接开启新的线程来执行,在新线程中执行任务(异步任务,添加异步任务的线程不作等待,可继续执行别的任务)
- 开启了一条新的线程来执行异步任务(异步任务能够开启新线程,串行列队只能开启一个线程)
- 线程不会等待任务执行完毕,任务的执行是按照顺序来的,每次只有一个任务被执行,任务一个接一个的执行下去
- 没有开启新线程,全部任务都是在主线程中执行的(虽然异步任务有开启新线程的能力,但由于是在主列队,因此没法开启新线程,全部任务都在主线程中执行)
- 因为只有一个线程可使用,因此全部任务都是按顺序一个个执行的,一个完毕,执行下一个
线程间的通信比较经常使用的就是在其余线程获取数据,而后返回主线程刷新UI界面
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@",[NSThread currentThread]);
}
// 回到主线程
dispatch_async(mainQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@",[NSThread currentThread]);
});
});
复制代码
有时咱们须要异步执行两组操做分别为A组和B组,当A组完成后,再执行B组操做,所以咱们须要把把两组操做分割开来。这时可用
dispatch_barrier_async
方法来实现,在添加两组任务之间添加一个分栏,函数会先把分栏前添加的任务执行完毕以后,在把分栏后的任务添加到队列中
dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_barrier_async(queue, barrierBlk);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
复制代码
栅栏函数也能够执行队列上的操做(参数列表中有queue和block),也有对应的
dispatch_barrier_sync
函数。
栅栏函数中传入的参数队列必须是由
dispatch_queue_create
方法建立的队列,不然,与dispatch_async
无异,起不到“栅栏”的做用了,对于dispatch_barrier_sync
也是同理。
栅栏函数以前和以后的操做执行顺序都不固定,可是前面三个必然先执行,而后再执行栅栏函数中的操做,最后执行后面的三个
dispatch_barrier_sync
和dispatch_barrier_async
的区别:
- 当栅栏先后添加的都是同步任务,二者没有区别,按照顺序依次执行
- 当栅栏先后添加的是异步任务,sync 会先执行栅栏前的任务,而后不等待栅栏后部任务。async栅栏先后的任务都不等待
因而可知sync和async对于栅栏函数的区别做:
dispatch_barrier_sync
将本身的任务插入到队列的时候,须要等待本身的任务结束以后才会继续插入被写在它后面的任务,而后执行它们。dispatch_barrier_async
将本身的任务插入到队列以后,不会等待本身的任务结束,它会继续把后面的任务插入到队列,而后等待本身的任务结束后才执行后面任务
。
当遇到要求在指定时间后执行代码(例如5秒后执行代码),可用
dispatch_after
来实现,须要注意的是这个并不严谨,这个是指在指定时间后,再把代码加入列队中去,并非严格的在多少时间后开始执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(second * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// second秒后异步追加任务代码到主队列,并开始执行
NSLog(@"after---%@",[NSThread currentThread]); // 打印当前线程
});
复制代码
在建立单例,或者有代码要求在整个程序的运行过程当中之执行一次的话可使用GCD中的
dispatch_once
函数,这个函数保证即便在多线程的环境下也能够保证只调用一次,保证线程安全
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
}
复制代码
快速迭代函数
dispatch_apply
按照指定的次数把指定的任务加入到指定的列队中去,并等待所有的任务执行完毕后,结束
- (void)applyTime:(NSInteger)time {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(time, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
复制代码
注意:因为是在并发列队中异步执行,因此里面的执行完成顺序不固定
GCD中的信号量,是一种持有计数的信号,计数为0时,不可经过,要等待。计数为1或大于1时,可经过,不需等待。
dispatch_semaphore_create
:建立一个Semaphore并初始化信号的总量dispatch_semaphore_signal
:发送一个信号,让信号总量加 1dispatch_semaphore_wait
:可使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),不然就能够正常执行。
- 保持线程同步,使异步执行的任务转化为同步执行
- 保护线程安全,为线程加锁
例如:当异步执行耗时操做时,须要使用该异步操做的结果进行一些额外的操做,例如:同时进行两个异步操做A和B,执行B的时候,须要对A的运行结果来进行操做,这个时候就能够对B加一个信号量,让B等待,当A执行完毕后,对B操做发送信号,继续执行B操做
- (void)semaphoreSync {
NSLog(@"semaphore---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//建立初始信号量 为 0 ,阻塞全部线程
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任务A
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
number = 100;
// 执行完线程,信号量加 1,信号总量从 0 变为 1
dispatch_semaphore_signal(semaphore);
});
//原任务B
////若计数为0则一直等待,直到接到总信号量变为 >0 ,继续执行后续代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %d",number);
}
复制代码
例如:线程安全方面:若每一个线程中对全局变量、静态变量只有读操做,而无写操做,通常来讲,这个全局变量是线程安全的;如有多个线程同时执行写操做(更改变量),通常都须要考虑线程同步,不然的话就可能影响线程安全。(可理解为线程 A 和 线程 B 一块配合,A 执行到必定程度时要依靠线程 B 的某个结果,因而停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操做。)
有时候会遇到须要异步执行两个耗时任务,而后当两个任务都执行完毕后,在回到主线程执行任务,这时候咱们能够用GCD的队列组的功能来实现这个要求
- 调用队列组的
dispatch_group_async
先把任务放到列队中,而后把列队放入到列队组中。也可用dispatch_group_enter
和dispatch_group_leave
两个组合来实现dispatch_group_async
dispatch_group_enter
:加入,一个任务追加到group,执行一次,至关于group中的未执行任务加1dispatch_group_leave
:离开,一个任务离开了group,执行一次,至关于group中的未执行任务减1
当group中的未执行完毕的任务数为0的时候才会执行
dispatch_group_notify
中的任务,以及不会使dispatch_group_wait
堵塞当前线程(相似于信号量)
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_sync(queue, ^{
// 追加任务A
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
dispatch_group_leave(group);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
复制代码
- 调用队列组的
dispatch_group_notify
回到指定线程执行任务(监听group中的全部任务的完成状态,当全部任务都完成后,把notify中的任务添加到group中,并执行任务)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务A
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务B
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 追加任务Main
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"main---%@",[NSThread currentThread]); // 打印当前线程
}
});
复制代码
- 调用队列组的
dispatch_group_wait
回到当前线程继续向下执行(暂停当前线程中的操做,阻塞当前线程,执行wait中的group操做,执行完后,继续执行当前线程中的操做)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务A
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
//执行A任务,执行完成后继续执行该线程后续任务
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"current---%@",[NSThread currentThread]);
复制代码
基于OC语言的API,底层是GCD,增长了一些更加简单易用的功能,使用更加面向对象。线程生命周期由系统自动管理,在实际开发中常用。
- 能够添加代完成的代码块,在操做完成后执行
- 添加操做之间的依赖关系,控制执行顺序
- 设定操做执行的优先级
- 能够很方便的取消一个操做的执行
- 使用KVO观察对象操做执行状态的更改:isExecuteing(执行) isFinished(结束) isCancelled(取消)
- 线程中执行的操做代码
- 与GCD不一样,GCD是放在block块中。在
NSOperation
中,可使用它的子类NSInvocationOperation
,NSBlockOperation
来实现,也能够自定义子来封装操做
- 这个列队不一样于GCD中的列队中先进先出的原则,这里的列队对于添加到列队中的操做首先进入准备就绪的状态(这个根据操做之间的依赖关系来决定),而后进能够开始执行的操做的执行顺序要先按照操做之间的相对的优先级来决定,而后再根据进入的顺序决定执行顺序
- 操做的队列经过设置 最大并发操做数 (
maxConcurrentOperationCount
)来控制并发和串行NSOperationQueue
提供了两种不一样的列队,主列队和自定义列队。主列队运行在主线程之上,自定义列队在后台执行。
NSInvocationOperation
-(void)useNSInvocationOperation{
NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest) object:nil];
[op start];
}
复制代码
是在没有使用NSOperationQueue的状况下,在主线程中单独使用子类执行一个操做,操做是在当前线程执行的,并无开启新线程,想要在其余线程来执行这个操做的话可使用:
[NSThread detachNewThreadSelector:@selector(useNSInvocationOperation) toTarget:self withObject:nil];
复制代码
NSBlockOperation
-(void)useNSBlockOperation{
NSBlockOperation * op = [NSBlockOperation blockOperationWithBlock:^ {
for (NSInteger i = 0 ; i < 2 ; i ++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}];
[op start];
}
复制代码
和上一个同样也是在当前线程调用,想要在其余线程使用的话能够:
[NSThread detachNewThreadSelector:@selector(useNSBlockOperation) toTarget:self withObject:nil];
复制代码
注意:NSBlockOperation有方法能够添加额外的操做addExecutionBlock:
[op addExecutionBlock:^{
for (NSInteger i = 0 ; i < 2 ; i ++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@", [NSThread currentThread]);
}
}];
复制代码
能够在不一样的线程中并发执行,只有全部添加的操做所有完成,才视为这个block操做完成。通常状况下,一个operation对象若是封装了多个操做,是否开启新线程来执行这些操做,取决于操做的个数,由系统来决定是否开启新线程。
NSOperation
自定义的子类能够经过重写 main 或者 start 方法来自定义操做对象 重写 main 方法比较简单,不用管理状态属性 isExecuting 等。
-(void)useCustomOperation{
JMOperation * op = [[JMOperation alloc] init];
[op start];
}
复制代码
操做的具体实现写在重写的类中,方便管理和统一修改。
NSOperationQueue * queue = [NSOperationQueue mainQueue];
复制代码
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
复制代码
-(void)createCustomQueue{
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
NSInvocationOperation * op_1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest) object:nil];
NSBlockOperation * op_2 = [NSBlockOperation blockOperationWithBlock:^ {
for (NSInteger i = 0 ; i < 2 ; i ++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@", [NSThread currentThread]);
}
}];
[op_2 addExecutionBlock:^{
for (NSInteger i = 0 ; i < 2 ; i ++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"3---%@", [NSThread currentThread]);
}
}];
[queue addOperation:op_1]; // 操做加入队列
[queue addOperation:op_2];
}
复制代码
-(void)createMainQueue{
NSOperationQueue * queue = [NSOperationQueue mainQueue];
// 队列加入block方法
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
复制代码
关键词(
maxConcurrentOperationCount
),最大并发操做数,用来控制一个列队中能够有多少个操做同时并发执行
注意:这个数值控制的并非并发线程的数量,而是一个队列中同时能并发执行的最大操做数,一个操做并不是只能在一个线程中运行。
开启线程的数量由系统决定,没法人为管理。
queue.maxConcurrentOperationCount = 4;
- 默认为-1,表示不限制,能够进行并发执行
- 值为1时,表示只可串行执行
- 大于1时,表示并发执行,不可超过系统限制
NSOperation的独有功能,操做依赖,经过操做依赖,咱们能够根据设置的依赖关系,很方便的控制操做的执行顺序,NSOperation提供3个接口供咱们管理和查看依赖
[op_1 addDependency:op_2]
;[op_1 removeDependency:op_2]
;@property (readonly, copy) NSArray<NSOperation *> *dependencies
;上面的三个操做分别是添加依赖,移除依赖,获取当前操做所依赖的全部操做的数组
NSOperation提供了 queuePriority (优先级)属性。queuePriority 属性适用于同一操做列队中的操做,不适用于不一样操做列队中的操做。默认状况下,全部操做对象优先级都是 NSOperationQueuePriorityNormal 。可是咱们能够经过setQueuePriority: 方法来改变当前操做在同一个列队中的优先级:
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
复制代码
注意:
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
for (NSInteger i = 0 ; i < 2 ; i ++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@", [NSThread currentThread]);
}
}];
//返回主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操做
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操做
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
复制代码
经过
[NSOperationQueue mainQueue]
函数返回主线程的操做
多线程有多种枷锁方式来保护线程安全,经常使用的是NSLock,经过NSLock来给进程加锁,解锁。加锁后其余进程没法再访问这个方法和属性
NSLock * lock = [[NSLock alloc] init]
;[lock lock]
;[lock unlock]
;初始化lock方法,加锁,解锁方法
某个A线程调用lock方法,这样,nslock将被上锁,能够执行被锁住的关键方法,完成后A线程调用unlock方法解锁。若是在A线程调用unlock方法以前,有B线程须要调用被锁住的关键方法,那么将没法访问,一直等待,直到A线程调用了unlock方法。
其余还有多种锁的方式,如:自旋锁,互斥锁,递归锁,条件锁,读写锁等,具体进程锁在下篇文章再来介绍。iOS开发基础——线程安全(进程锁)