进入文章前,我们先了解一下多线程中的基本概念。
一、进程
- 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元
- 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个APP
- 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。
二、线程
- 程序执行流的最小单元,线程是进程中的一个实体
- 一个进程要想执行任务,必须至少有一条线程,应用程序启动的时候,系统会默认开启一条线程,也就是主线程
三、进程和线程的关系
- 线程是进程的执行单元,进程的所有任务都在线程中执行
- 线程是cpu分配资源和调度的最小单位
- 一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少有一条线程
- 同一个进程内的线程共享进程资源。
四、多进程
- 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的
- 进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,所有由用户启动的进程都是用户进程。进程是操作系统进行资源分配的单位。
- 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。
五、多线程
- 同一时间,cpu只能处理一条线程,只有1条线程在执行。多线程并发执行,其实是cpu快速的在多条线程之间调度(切换),如果cpu调度线程的时间足够快,就造成了多线程并发执行的假象。
- 如果线程非常多,cpu会在N多线程之间调度,小号大量的cpu资源,每条线程被调度执行的频次会降低(线程的执行效率降低)
六、多线程的优缺点
- 优点
1、 能适当提高程序的执行效率
2、 能适当提高资源利用率(cpu,内存利用率)
- 缺点
1、开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
2、线程越多,CPU在调度线程上的开销就越大。
3、程序设计更加复杂,比如线程之间的通信,多线程的数据共享。
- 线程开销:
线程是需要内存和性能开销的,内存开销包括系统内核内存和应用程序内存:用来管理和协调线程的内核结构存储在内核,线程的栈空间和每个线程的数据存储在程序的内存空间,占用内存的这些结构大部分是在线程创建的时候生成和初始化的。因为线程要和内核交互,这个过程是非常耗时的,这是线程性能开销的主要原因。线程创建大概的开销如下(其中第二线程的栈空间是可以配置的):
内核数据结构:大约1kb
栈空间:主线程大约1MB,第二线程 大约 512kb
线程创建时间:大约90毫秒
另一个开销就是程序内线程同步的开销
七、任务,队列
-
任务
就是执行操作的意思,也就是在线程中执行的那段代码。在GCD中是放在block 中的。执行任务有两种方式:同步执行(sync),异步执行(async)。 -
同步:同步添加任务到指定的队列中,在添加的任务执行结束之前会一直等待,直到队列里面的任务执行完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。
-
异步:线程会立即返回,无需等待就会继续执行下面的任务,不会阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务。
-
队列:这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。在GCD中有两种队列:串行队列和并发队列。两种都符合FIFO(先进先出)的原则。两者的区别是执行顺序不同以开启线程数不同。
-
串行队列:
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后才能执行下一个任务(串行队列只开启一个线程,队列中的所有任务只在这一个线程中执行,所以要等待一个任务执行完毕才能执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的。 -
并发队列
同时运行多个任务并发执行。(并发队列可以开启多个线程,并发队列中的任务不用等待前一个任务执行完毕再执行,不同线程中的任务可以同时执行),并发队列的并发功能只有在异步(asyn)函数下才有效(在syn同步函数下,会卡当前线程,队列中的任务只在当前线程中执行,所以会等待前一个任务执行完毕 再执行下一个任务) -
线程间通信
一个线程传递数据给另一个线程,在一个线程中执行完特定任务后,转到另一个线程继续执行任务。线程间通信的典型例子 就是 异步下载图片,下载完成后在主线程刷新UI。
八、iOS中的多线程
iOS中的多线程主要有三种:NSThread ,NSoperationQueue,GCD
下面我们一一讲解:
NSThread:
是对pthread 的上层封装,把线程处理为面向对象的逻辑。一个NSThread代表一个线程。
优点:NSThread是一种轻量级的多线程实现方式
缺点:需要自己管理线程的生命周期,线程同步。同时使用NSThread线程同步 对数据加锁 会有一定的性能开销。
常用API
1、显示创建方法,并且手动启动线程
/*
target :selector消息发送的对象
selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值
object: 为传递给selector方法的参数
*/
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadRun:) object:nil];
//启动线程
[thread start];
//iOS10 的创建方法:
NSThread * thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"手动创建线程启动 = %@",[NSThread currentThread]);
}];
[thread start];
通过以上方法创建,可以得到一个thread 对象,可以对对象设置一些相关属性:
//设置线程名
1、thread.name = @"name";
2、threadPriority:设置线程的优先级 (iOS8 之后: setThreadPriority),
优先级为0.0-1.0的double类型,1.0最高。每一个新的线程都有一个默认的优先级,系统的内核调度算法根据线程的优先级来决定线程的执行顺序。通常情况下我们不要改变线程的优先级,提高一些线程的优先级可能会导致低优先级的线程一直得不到执行,如果在我们的应用内存在高优先级线程和低优先级线程的交互的话,因为低优先级的线程得不到执行可能阻塞其他线程的执行。这样会对应用造成性能瓶颈。
脱离线程(detach Thread):线程完成后,系统自动释放它所占用的内存空间
可连接线程(Joinable Thread):线程完成后,不回收可连接线程的资源
在应用程序退出时,脱离线程可以立即被中断,而可连接线程则不可以。每个可连接线程必须在进程被允许可以退出的时候被连接。所以当线程出于周期性工作而不被允许被中断的时候,比如保存数据到硬盘,可连接线程是最佳选择。
当然在iOS开发过程中,很少需要我们创建可连接的线程。通过NSThread创建的线程都是脱离线程。如果你想创建可连接线程,唯一的办法是使用POSIX线程。POSIX默认创建的线程是可连接的。通过pthread_attr_setdetachstate 函数设置是否脱离属性。
iOS8 以后优先级值:
3、stackSize:配置线程栈空间
栈空间是用来存储为线程创建的本地变量的,栈空间的大小必须在线程的创建之前设定,即在调用NSThread的start方法之前 通过setStackSize 设定新的栈空间大小
4、threadDictionary:配置线程的本地存储
每个线程都维护一个在线程任何地方都能获取的字典。我们可以使用NSThread的threadDictionary方法获取一个NSMutableDictionary对象,然后添加我们需要的字段和数据。
iOS8 以后设置优先级的值:
NSQualityOfServiceUserInteractive = 0x21,//最高优先级,用于用户交互事件
NSQualityOfServiceUserInitiated = 0x19,//次高优先级,用于用户需要马上执行的事件
NSQualityOfServiceUtility = 0x11,//默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
常用方法
[thread start];//实例方法,启动线程
[thread cancel];//实例方法,取消线程
[thread isCancelled];//判断线程是否已经取消
[thread isFinished];//判断线程是否结束
[thread isExecuting];//判断线程是否正在执行
//线程休眠
[NSThread sleepForTimeInterval:1.0];
//线程休眠
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
//终止主线程以外的所有线程
[NSThread exit];
[NSThread isMainThread]://判断的是当前线程是否为主线程
[thread isMainThread]: //判断的是thread 是否为主线程
2 创建并自启动线程的方法
//方法返回值为空,方法内自己创建线程对象,并由系统自动启动
[NSThread detachNewThreadSelector:@selector(threadRun:) toTarget:self withObject:nil];
//ios10 以后的方法
[NSThread detachNewThreadWithBlock:^{
NSLog(@"10.0之后 创建并自启动线程...");
}];
3、隐式创建,用于线程间通信
/*
@selector 定义我们要执行的方法
withObject:arg 定义了我们执行方法时,传入的参数对象,类型是id。
waitUntilDone:YES:是指当前线程是否要被阻塞,直到主线程将我们指定的代码块执行完
modes:array 指定时间运行的模式
*/
[self performSelectorOnMainThread:@selector(threadRun) withObject:@"" waitUntilDone:YES];
[self performSelectorOnMainThread:@selector(threadRun) withObject:nil waitUntilDone:YES modes:nil];
/*
以上方法的作用是在主线程中执行指定的方法。该方法主要用来回调到主线程来修改页面UI的状态。
*/
//在指定线程中执行
[self performSelector:@selector(testRun2) onThread:[NSThread mainThread] withObject:nil waitUntilDone:false];
// 在主线程指定在后台线程执行
[self performSelectorInBackground:@selector(threadRun) withObject:nil];
//在当前线程执行的方法
[self performSelector:@selector(threadRun)];
// 传递参数,指定函数在当前线程执行
[self performSelector:@selector(threadRun) withObject:@"123"];
// 传递参数 指定函数 2s后在当前线程执行
[self performSelector:@selector(threadRun) withObject:@"dd" afterDelay:2];
//--------
需要注意的是:如果afterDelay的延时函数,会在内部创建一个NSTimer,然后添加到当前线程的runloop中。也就是如果当前线程没有开启runloop,该方法会失效,在子线程中需要启动runloop
[[NSRunLoop currentRunLoop] run];
//-----------
而 performSelector:withObject: 只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的runloop中也能执行。
-
线程同步
有时候需要我们设置线程同步,但是线程同步往往会产生很多问题:
1、线程的死锁,即较长时间的等待或资源竞争以及死锁等多线程症状。
2、对共有变量的同时读或写。当多个线程需要对共有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外,当公用变量的读写操作是非原子性时,在不同的机器上,终端时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。 -
数据同步锁
在多个线程访问相同的数据时,有可能会造成数据的冲突。比如常见的售票问题。通过加锁的方式解决该问题是最常见的方式.Foundation框架中提供了NSLock对象来实现锁。
[lock lock]//加锁
[lock unlock]//解锁
//例如 多个线程 修改一个变量操作
NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(taskRun) object:nil];
thread1.name = @"task1";
NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(taskRun) object:nil];
thread2.name = @"task2";
NSThread *thread3 = [[NSThread alloc]initWithTarget:self selector:@selector(taskRun) object:nil];
thread3.name = @"task3";
[thread1 start];
[thread2 start];
[thread3 start];
//------taskRun-----------
- (void)taskRun {
while (count > 0) {
[NSThread sleepForTimeInterval:0.1];
count --;
NSLog(@"threadName = %@ count: %ld",[NSThread currentThread].name,count);
}
}
可以从以下的打印结果中看出,数据是错乱的,没有顺序的,那是因为多个线程对count 同时做了更改操作,在一个线程还没有结束的时候,另一个线程也开始执行这个方法。

改进:
/**
@synchronized(锁对象) { // 需要锁定的代码 }
注意:锁定一份代码 只用1把锁,多把锁是无效的
互斥锁的优缺点:
优点:能有效防止因多线程抢夺资源造成数据安全问题
缺点: 需要消耗大量的CPU资源
互斥锁的使用前提:多条线程抢夺统一资源
线程同步:多条线程按顺序的执行任务
互斥锁:使用了线程同步技术
*/
- (void)taskRun {
while (count > 0) {
@synchronized (self) {//需要锁定的代码
[NSThread sleepForTimeInterval:0.1];
count --;
NSLog(@"threadName = %@ count: %ld",[NSThread currentThread].name,count);
}
}
}

//或者使用NSLock
while (count > 0) {
[_threadLock lock];
[NSThread sleepForTimeInterval:0.1];
count --;
NSLog(@"threadName = %@ count: %ld",[NSThread currentThread].name,count);
[_threadLock unlock];
}
-
通过设置属性的原子性
atomic:原子属性,为setter方法加锁,默认是该属性,这个属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥锁代码,避免该变量的读写不同步问题。
nonatomic:非原子性,不会为setter方法添加锁。如果该对象无需考虑多线程的情况,就加入这个属性,这样会让编译器少生成一些互斥锁代码,提高效率。
atomic:意思就是setter/getter这个函数是一个原子操作。如果有多个线程同时调用setter方法,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter的情况。相当于函数头尾加了锁一样,可以保证数据的完整性。
nonatomic 不保证setter ,getter的原语行,所以你可能会取到不完整的东西。因此在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。比如setter函数里面改变两个成员变量,如果你用nonatomic的话,getter可能会取到只更改了其中一个变量时候的状态,这样取到的东西就会有问题,是不完整的。当然如果不需要多线程支持的话,用nonatomic就够了,因为不涉及到线程锁的操作,所以它执行相对快些。
一般iOS中,所有属性都声明为nonatomic。这样做的原因是:在iOS中使用同步锁的开销比较大,这样会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证线程安全,若要实现线程安全的操作还需要采用更为深层的锁定机制才行。例如:一个线程在连续多次读取到某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读取到不同的属性值。因此,iOS程序一般都会使用nonatomic属性。但是在mac os x 程序时,使用atomic属性通常都不会有性能瓶颈。
- 同步等待
NSCondition:一个锁 和一个线程检查器,锁主要是为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
NSCondition *condition = [[NSCondition alloc]init];
[condition lock]//一般用于多线程同时访问,修改同一个数据源,保证在同一时间内数据源只被访问一次,修改一次,其他线程的命令需要在lock外等待,只有unlock,才可访问
[condition unlock]//与lock 同时使用
[condition wait]//让线程处于等待状态
[condition signal]//CPU发信号告诉线程不用再等待,可以继续执行
//常用API
//释放互斥量,使当前线程等待,切换到其他线程执行
- (void)wait;
//释放互斥量,使当前线程等待到某一个时间,切换到其他线程执行
- (BOOL)waitUntilDate:(NSDate *)limit;
//唤醒一个其它等待该条件变量的线程
- (void)signal;
//唤醒所有其它等待该条件变量的线程
- (void)broadcast;
//应用实例:生产者 消费者,每生产一个商品 就让消费者取走一个商品
@interface NSConditionSample ()
@property(nonatomic, strong)NSCondition *condition;
@property(nonatomic, strong)NSMutableArray *products;
@end
@implementation NSConditionSample
- (instancetype)init
{
self = [super init];
if (self) {
_condition = [[NSCondition alloc]init];
_products = [NSMutableArray array];
}
return self;
}
//消费者
- (void)createConsumenr {
while (YES) {
NSLog(@"createconsumer before lock");
[_condition lock];
NSLog(@"createconsumer after lock");
while (_products.count == 0) {
NSLog(@"wait for products");
[_condition wait];
}
[_products removeObjectAtIndex:0];
NSLog(@"consume a product");
[_condition unlock];
}
}
//生产者
- (void)createProducter {
while (YES) {
NSLog(@"createProducter before lock");
[_condition lock];
NSLog(@"createproducter after lock");
[_products addObject:[[NSObject alloc]init]];
NSLog(@"produce a product");
[_condition signal];
[_condition unlock];
}
}
//执行
- (void)viewDidLoad {
[super viewDidLoad];
NSConditionSample *sample = [[NSConditionSample alloc]init];
[NSThread detachNewThreadSelector:@selector(createConsumenr) toTarget:sample withObject:nil];
[NSThread detachNewThreadSelector:@selector(createProducter) toTarget:sample withObject:nil];
}

网友评论