1 什么是线程?
- 线程(英语:thread)是操作系统能够进行运算调度的最小单位。
- 线程是独立调度和分派的基本单位。
- 同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
- 一个进程可以有很多线程,每条线程并行执行不同的任务。
2 什么是进程?
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.NSThread
NSThread有两种创建方法,创建的时候我们既可以使用SEL也可以使用Block
1 .类方法
- 类方法创建的时候需要确定好执行的任务,没有任何返回,它会自己创建一个新线程去执行指定任务,
- 是实例方法
- 实例方法则需要我们手动开启这个线程
//1. 类方法创建
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"%d",i);
[NSThread sleepForTimeInterval:1];
}
}];
//2.实例方法创建
NSThread *thead = [[NSThread alloc]initWithBlock:^{
for (int i = 97; i < 102; i++) {
NSLog(@"%c",i);
[NSThread sleepForTimeInterval:1];
}
}];
[thead start];
第一次输出结果:

第二次输出结果:

我们可以看到这两个线程是完全异步的。运行几次,结果也不相同。
值得注意的是,一个NSThread线程的启动,有三种方式,我们前面已经看到了两种,一种是类方法直接创建并启动,另一种是[thread start];
方法,还有一种是[thread main];
,但是苹果并不建议我们直接使用main
方法,它可以在子类化的时候重写并实现你的线程主体,而不用调用super
,任何时候,启动线程都应该用start方法。
前面我们简单得用NSThread的方式创建了两个线程,并且让它们各自执行了自己的任务。但是NSThread可不止这么几个方法,它还有很多比较有用的方法:
//设置名字
[thread setName:@"myThread"];
//设置优先级,由0到1.0的浮点数指定,其中1.0是最高优先级。
[thread setThreadPriority:1];
//退出当前线程
[NSThread exit];
//睡眠 单位是秒
[NSThread sleepForTimeInterval:1];
//获取当前线程
[NSThread currentThread];
//获取主线程
[NSThread mainThread];
//判断是否在主线程
[NSThread isMainThread];
接下来,我们用经典的卖票问题来模拟并解决NSThread中的线程同步问题。有两个售票员,同时开始卖票,一共有20张票,模拟该场景:
//剩余票数
@property (nonatomic, assign) NSInteger tickets;
@end
@implementation Saler
-(void)salerTicket {
NSLog(@"salerTicket");
//初始有20张余票
self.tickets = 20;
//创建两个线程来充当两个售票员
[NSThread detachNewThreadWithBlock:^{
while (self.tickets>0) {
self.tickets--;
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}];
[NSThread detachNewThreadWithBlock:^{
while (self.tickets>0) {
self.tickets--;
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}];
}
运行结果:

- 仔细数数竟然卖出去了23张票。
- 为了避免这种情况,通常的做法就是上锁,当某一条线程对数据进行操作时,先给数据上锁,别的线程阻塞,等到这条线程操作结束,在开锁,别的线程再进去,上锁,操作,开锁……这样就保证了数据的安全性。
- 我们可以使用
@synchronized (object) {}
来进行上锁,括号里的参数可以填任意对象,但是要注意的是,必须填写线程共有的变量才能实现上锁,局部变量是无效的,原因是,如果用局部变量,就会创建多个锁,这些锁之间并无关联,所以与不上锁没有区别:
//初始有20张余票
self.tickets = 20;
//创建两个线程来充当两个售票员
[NSThread detachNewThreadWithBlock:^{
@synchronized (self) {
while (self.tickets>0) {
self.tickets--;
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}
}];
[NSThread detachNewThreadWithBlock:^{
@synchronized (self) {
while (self.tickets>0) {
self.tickets--;
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}
}];
运行结果:耗时19秒

因为在锁内进行数据操作时,其它线程都会阻塞在外面,这个时候,其实线程不是并发执行的,所以我们不难想到,锁内执行的任务越少,那么这段代码执行的效率就越高。在此基础上,我们可以对前面的加锁进行一个小修改:
//初始有20张余票
self.tickets = 20;
//创建两个线程来充当两个售票员
[NSThread detachNewThreadWithBlock:^{
while (self.tickets>0) {
@synchronized (self) {
self.tickets--;
}
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}];
[NSThread detachNewThreadWithBlock:^{
while (self.tickets>0) {
@synchronized (self) {
self.tickets--;
}
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}];
运行结果:耗时9秒

注意看一下时间,修改以后,我们的卖票效率提升了一倍,之前那种方式要19秒才能卖完,现在只需要9秒。
- 当然也可以用NSLock来进行上锁,使用NSLock需要创建一个NSLock实例,然后调用lock和unlock方法来进行加锁和解锁的操作:
@interface Saler ()
//剩余票数
@property (nonatomic, assign) NSInteger tickets;
@property (nonatomic, strong) NSLock *lock;
@end
@implementation Saler
-(void)salerTicket {
NSLog(@"salerTicket");
//初始有20张余票
self.tickets = 20;
self.lock = [[NSLock alloc]init];
//创建两个线程来充当两个售票员
[NSThread detachNewThreadWithBlock:^{
while (self.tickets>0) {
[self.lock lock];
self.tickets--;
[self.lock unlock];
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}];
[NSThread detachNewThreadWithBlock:^{
while (self.tickets>0) {
[self.lock lock];
self.tickets--;
[self.lock unlock];
NSLog(@"剩余票数 :%ld",self.tickets);
[NSThread sleepForTimeInterval:1];
}
}];
}
执行结果:耗时9秒

不知道你还记得不记得atomic,这个就修饰了属性的原子性,如果直接把属性修饰改为atomic,会不会就不需要我们加锁了呢?我试过,不行!这是因为atomic只会对该属性的Getter和Setter方法上锁,而我们很显然是在别的方法里面对数据进行操作,所以并没什么卵用。同时也因为atomic太耗性能,所以在实际开发中,我们一般都不使用它来修饰变量。
3.GCD
Grand Central Dispatch (GCD)是什么?
GCD中文翻译过来是宏伟的中枢调度,是一种基于C语言的并发编程技术。它是苹果为多核的并行运算提出的解决方案,会自动调度系统资源,所以它的效率很高。
GCD并不直接操作线程,而是操作队列和任务。我们只需要把任务添加到队列里,然后指定任务执行的方式,GCD就会自动调度线程执行任务。
GCD的任务都是以Block形式存在的。
1. 队列有两种:串行队列/并发队列。
- 串行队列只能等一个任务执行完毕才可以继续调度下一个任务
/* 创建一个串行队列
* 参数:1.名字2.类型,DISPATCH_QUEUE_SERIAL(串行队列) DISPATCH_QUEUE_CONCURRENT(并发队列)
*/
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
- 并发队列可以同时调度多个任务
/* 创建一个并发队列
* 参数:1.名字2.类型,DISPATCH_QUEUE_SERIAL(串行队列) DISPATCH_QUEUE_CONCURRENT(并发队列)
*/
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
2.执行任务也有两种方式:同步执行/异步执行。
- 同步执行会等待当前任务完成才会执行下一个任务,不会开启新线程。
/* 同步执行任务
* 参数:1.队列2.block(任务)
*/
dispatch_sync(dispatch_queue_t _Nonnull queue, ^(void)block);
- 异步执行不会等待当前任务完成就会执行下一个任务,可以开启新线程(如果是主队列,则不会开启新线程,因为主队列的任务都会在主线程执行)。
/* 异步执行任务
* 参数:1.队列2.block(任务)
*/
dispatch_async(dispatch_queue_t _Nonnull queue, ^(void)block);
队列和任务都有两种,排列组合以后就有四种情况,在不同的情况下,执行的结果可能会有差异,如果不清楚原理比较容易混淆。这里有一个简单的方法去分析执行情况:队列的类型决定了能不能同时执行多个任务(串行队列一次只能执行一个任务,并发队列一次可以执行多个任务),执行的方式决定了会不会开启新线程(同步执行不会开启新线程,异步执行可以开启新线程)。
3. 四种情况
1.同步执行串行队列任务
//创建一个串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
//同步执行串行队列任务
for (int i = 0; i < 10; i ++) {
dispatch_sync(serialQueue, ^{
NSLog(@"%d %@",i,[NSThread currentThread]);
});
}

我们可以看到,同步执行的方式并没有开启新线程,打印结果也是顺序的。
2.同步执行并发队列任务
//创建一个并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//同步执行并发队列任务
for (int i = 0; i < 10; i ++) {
dispatch_sync(concurrentQueue, ^{
NSLog(@"%d %@",i,[NSThread currentThread]);
});
}

可以看到同步执行的情况下,无论是串行队列还是并发队列,结果并没有区别,这是因为在同步执行的情况下并不会开启新的线程,所有任务都只能在一条线程上执行,而同一条线程上的任务只能串行执行,所以即使并发队列拥有同时调度多个任务的能力,但是在一条线程的情况下,也只能等前一个任务执行完毕再调度新的任务去执行。所以,在同步执行任务的情况下,串行队列和并发队列的运行结果是一致的。
3.异步执行串行队列任务
//创建一个串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
//异步执行串行队列任务
for (int i = 0; i < 10; i ++) {
dispatch_async(serialQueue, ^{
NSLog(@"%d %@",i,[NSThread currentThread]);
});
}

可以看到,这种情况下,任务是顺序执行的,但是它是在子线程执行的。这是因为,异步执行可以开启新线程,但是由于是串行队列,所以任务只能一个一个顺序执行。
4.异步执行并发队列任务
//创建一个并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//异步执行并发队列任务
for (int i = 0; i < 10; i ++) {
dispatch_async(concurrentQueue, ^{
NSLog(@"%d %@",i,[NSThread currentThread]);
});
}

可以看到,这种情况下,执行任务的顺序不固定,并且会开启多条线程同时执行,所以这种时候执行任务的效率最高。
在实际的开发中,我们更多运用到的还是异步执行,毕竟我们运用多线程技术是为了在另一条线程上执行任务,至于选择串行队列还是并发队列就要根据实际情况来判断了:
如果队列里的任务必须按照顺序执行,那就选择串行队列。
如果队列里的任务没有执行顺序的需求,那最好选择并发队列,因为并发队列的执行效率更高。
系统也为我们提供了两种队列,分别是:全局队列dispatch_get_global_queue(long identifier, unsigned long flags)、主队列dispatch_get_main_queue()。
全局队列本质上是一个并发队列,可以通过前面的测试来证明,获取时需要传递参数,第一个参数是服务质量的选择(以前叫优先级),第二个是保留参数,暂时只需要传0就可以了:
//全局队列1.优先级或服务质量,2.保留参数,目前传0
/*
* 优先级和服务质量的对应关系:
* - DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
* - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
* - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
* - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
*/
//默认优先级的全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
这里值得注意的是,一般情况下最好不要随意选择优先级,默认就够用了。优先级本质上是一个概率的问题,优先级越高,CPU调度的概率越高,并不具备确定性,如果出现问题,很难查找原因。当然,如果你很了解这些,并且就是为了性能和资源的考虑而做了优先级的选择,那么你可以无视这些。
主队列不需要参数可以直接获取,不过主队列并不会开启新线程,主队列上的所有任务都只会在主线程上执行,所以我们在平时的编程中,往往是在子线程中处理耗时操作,然后在主线程更新UI。在实际开发中,我们经常会遇到一种场景,就是在界面上显示一张网络图片,要显示图片肯定得先下载,而下载是一个耗时操作,如果在主线程下载,那就会使界面卡住不能进行其它操作,所以我们一般都会在子线程下载,下载好以后再去主线程更新界面:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(globalQueue, ^{
NSLog(@"在子线程执行耗时操作!");
dispatch_async(mainQueue, ^{
NSLog(@"在主线程更新UI");
});
});
以上的使用可以满足一些简单业务的需求,但是实际开发中有很多复杂业务,比如说在用户登录的时候需要同步多种信息,而这些信息从不同的接口获取,只有所有信息全部同步结束才可以正常操作,同步各种信息应该各自在子线程进行,我们可以异步执行并发队列中的任务来做这些耗时操作,但是我们怎么知道所有任务都执行完了呢?
4. GCD Group
GCD为我们提供了另一个东西,叫做Group(调度组)。调度组是用来协调一个或多个任务提交到队列异步触发的。 应用程序可以使用调度组等待所有调度组中的所有任务的完成。
所有异步队列执行完毕后得到一个通知。
调度组的使用并不复杂,它有两种用法:
//创建一个调度组
dispatch_group_t group = dispatch_group_create();
//获取全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//为队列添加任务,并且和给定的调度组关联
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"同步信息1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"同步信息2");
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:.5];
NSLog(@"同步信息3");
});
//所有任务执行完毕通知
dispatch_group_notify(group, queue, ^{
NSLog(@"全部都完了");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"更新UI");
});
});
或者:
//创建一个调度组
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_async(queue, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"同步信息1");
//该任务执行完毕从调度组移除
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"同步信息2");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:.5];
NSLog(@"同步信息3");
dispatch_group_leave(group);
});
//等待所有任务执行完毕 参数:1.对应的调度组 2.超时时间
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//所有任务执行完毕才会来这里
dispatch_async(queue, ^{
NSLog(@"全部都完了");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"更新UI");
});
});

两种用法的运行结果是相同的,第二种更加灵活一些,但是代码量也相应多一些,使用调度组我们可以更好的对任务进行控制,并且在特定的场景满足我们的需求。灵活使用调度组,可以让我们对线程同步控制更加得心应手。
GCD还有一个很重要的功能,就是一次执行。用这个代码块包含的代码只会执行一次,在实际开发中经常使用,单例模式一般都会用GCD来做,因为它效率高:
for (int i = 0; i < 10; i++) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"你猜我会执行几次?");
});
}
运行以后只会打印一次。
5. NSOperation
NSOperation是对GCD面向对象的封装。它拥有GCD的高效,也拥有面向对象的编程思想。和GCD类似,它也是把任务放在队列里去执行,不过它比GCD少了一些概念,但是它也有了一些GCD没有的功能,接下来我们就从最开始了解NSOperation。
Operation翻译过来是操作的意思,其实跟GCD的任务是一样的。因为它本身就是GCD的封装,所以在理解上也差不多。我们顺着GCD的用法来使用NSOperation。
NSOperation本身是个抽象类,我们要使用它就得使用它的子类。系统给我们提供了两个,分别是:NSInvocationOperation
和NSBlockOperation
。一种是通过selector的形式添加操作,一种是以block的形式添加操作,我个人更喜欢NSBlockOperation,用起来更方便些。
分别用这两种方式创建两个操作(其实就是GCD的任务):
//NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"blockOperation");
}];
//NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationMethod) object:nil];
- (void)invocationMethod{
NSLog(@"invocationOperation%@",[NSThread currentThread]);
}
根据GCD的操作流程,这时候就需要创建队列了。NSOperation的队列也有一个类NSOperationQueue,它的创建也很简单:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
然后把前面创建的操作添加到队列中:
[operationQueue addOperation:blockOperation];
[operationQueue addOperation:invocationOperation];
这个地方也不需要指定它的执行方式,直接把操作添加到队列中就会自动异步执行:
NSOperationQueue本身也有通过block添加操作的方法,不需要我们专门去创建,这样就进一步简化了代码:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperationWithBlock:^{
NSLog(@"1%@",[NSThread currentThread]);
}];
[operationQueue addOperationWithBlock:^{
NSLog(@"2%@",[NSThread currentThread]);
}];
NSOperationQueue并没有全局队列,但是我们可以自己根据需求创建全局队列。NSOperationQueue也有获取主队列的类方法[NSOperationQueue mainQueue];,用起来也很简单方便,跟GCD中的主队列一样。
一个子线程处理耗时操作,然后刷新UI的代码,使用NSOperationQueue的方式就变成了这样:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperationWithBlock:^{
NSLog(@"子线程处理耗时操作%@", [NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"主线程更新UI%@", [NSThread currentThread]);
}];
}];
在线程同步上,NSOperation 没有group,但是有操作依赖,一样可以实现同样的效果。它的依赖,是操作的方法,所以如果要使用依赖,我们就得自己创建操作,然后操作之间设置好依赖关系,再把它们丢到队列里,比如说前面GCD中的那个同步信息的例子:
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"同步信息1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"同步信息2");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:.5];
NSLog(@"同步信息3");
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"更新UI");
}];
[op4 addDependency:op1];
[op4 addDependency:op2];
[op4 addDependency:op3];
NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];
这里,操作4依赖了操作1、2、3,所以它得等到其它3个操作完成才能开始执行,运行结果:
再对方执行完毕之后才满足自己的执行条件。
这里在给队列添加操作的时候,用了新的方法[queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];,这个方法可以一次性添加多个操作,但是后面有一个参数,看名字我们就可以知道,如果传YES,它会一直等所有任务都执行完毕才会继续执行下面的任务,有点类似于GCD里面group的那个wait,但是它会卡主当前线程,所以不能在主线程中使用,我们也可以在子线程里面执行这一段代码,稍加改造,达到同样的效果:
不知道你有没有注意到,NSOperationQueue本身并没有并发队列和串行队列的选项,它默认是并发队列,但是,它有一个maxConcurrentOperationCount属性(代表了最大并发数,也就是最多能够开几条线程执行操作),如果最大并发数量为1,它就变成了类似串行队列的模样。
NSOperationQueue还可以使用suspended属性来控制队列里操作的暂停和继续。使用cancelAllOperations方法来取消队列里的所有操作。这些简单的属性和方法就不专门演示了。
总结
以上就是OC中的多线程,在实际开发中,我们几乎不会使用pthread,很少会使用NSThread,不过NSThread的一些类方法会经常使用,比如获取当前线程,睡眠当前线程等。GCD和NSOperation使用起来都效率更高,并且操作简单,是我们更好的选择。它俩之间的选择一般没有明确的分界线,可以根据实际需求来选择,不过一般中小型项目多使用GCD,大型项目多使用NSOperation,可能是因为GCD更底层,更轻一些,而NSOperation更规范,同时也重了些。
网友评论