前言
多线程是iOS开发中很重要的一个环节,无论是开发过程还是在面试环节中,多线程出现的频率都非常高。今天我们来分析多线程知识。
线程 和 进程
线程和进程的定义
线程
线程时进程的基本执行单元,一个进程的所有任务都在线程中执行- 进程要想执行任务,必须的有线程,
进程至少要有一条线程 程序启动会默认开启一条线程,这条线程被称为主线程或者UI线程
进程
进程是指在系统中正在运行的一个应用程序- 每个
进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内 - 通过“活动监视器”可以查看mac系统中所开启的线程
所以,可以简单的理解为:进程是线程的容器,而线程用来执行任务。在iOS中是单进程开发,一个进程就是一个app,进程之间是相互独立的,如支付宝、微信、qq等,这些都是属于不同的进程
进程与线程的关系
进程与线程之间的关系主要涉及两个方面:
-
地址空间- 同一个进程的
线程共享本进程的地址空间 - 而
进程之间则是独立的地址空间
- 同一个进程的
-
资源拥有- 同一个进程内
线程共享本进程的资源,如内存、I/O、cpu等 - 但是进
程之间资源是独立的
- 同一个进程内
两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即 进程相当于一个工厂,线程相当于工厂中的一条流水线
针对进程和线程,还有以下几点说明:
-
1:
多进程要比多线程健壮一个进程崩溃后,在保护模式下不会对其他进程产生影响- 而
一个线程崩溃整个进 程都死掉
-
2: 使用场景:频繁切换、并发操作
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。- 同样如果
要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
-
3: 执行过程
- 每个独立的
进程有一个程序运行的入口、顺序执行序列和程序入口 - 但是
线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 每个独立的
-
4:
线程是处理器调度的基本单位,但是进程不是。 -
5:
线程没有地址空间,线程包含在进程地址空间中
线程和Runloop的关系
- 1:
runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局 的字典里。 - 2:
runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。 - 3:runloop在第一次获取时被创建,在线程结束时被销毁。
- 4:对于
主线程来说,runloop在程序一启动就默认创建好了。 - 5:对于
子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。
多线程
多线程原理
- 对于
单核CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作, - iOS中的
多线程同时执行的本质是CPU在多个任务直接进行快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片
多线程意义
优点
- 能适当
提高程序的执行效率 - 能适当
提高资源的利用率,如CPU、内存 - 线程上的任务执行完成后,
线程会自动销毁
缺点
开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB- 如果开启
大量线程,会占用大量的内存空间,降低程序的性能 线程越多,CPU在调用线程上的开销就越大- 程序设计更加复杂,比如线程间的通信,多线程的数据共享
多线程生命周期
多线程的生命周期主要分为5部分:新建 - 就绪 - 运行 - 阻塞 - 死亡,
-
新建:主要是实例化线程对象 -
就绪:线程对象调用start方法,将线程对象加入可调度线程池,等待CPU的调用,即调用start方法,并不会立即执行,进入就绪状态,需要等待一段时间,经CPU调度后才执行,也就是从就绪状态进入运行状态 -
运行:CPU负责调度可调度线城市中线程的执行,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责,开发人员不能干预。 -
阻塞:当满足某个预定条件时,可以使用休眠,即sleep,或者同步锁,阻塞线程执行。当进入sleep时,会重新将线程加入就绪中。下面关于休眠的时间设置,都是NSThread的sleepUntilDate:阻塞当前线程,直到指定的时间为止,即休眠到指定时间sleepForTimeInterval:在给定的时间间隔内休眠线程,即指定休眠时长- 同步锁:
@synchronized(self):
-
死亡:分为两种情况正常死亡,即线程执行完毕非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片),
- 如果
时间片用尽,线程就会进入就绪状态队列 - 如果
时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列 - 等待事件发生后,线程又会重新进入
就绪状态队列 - 每当一个
线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列中选择一个线程继续执行
线程的exit和cancel说明\
exit:一旦强行终止线程,后续的所有代码都不会执行
- `cancel`:取消当前线程,但是不能取消正在执行的线程
线程池原理
iOS中多线程的实现方案
iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation,汇总如图所示
下面是以上四种方案的简单示例
// *********1: pthread*********
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
/**
pthread_create 创建线程
参数:
1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
3. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
4. 传递给第三个参数(函数)的`参数`
*/
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
NSLog(@"成功");
} else {
NSLog(@"失败");
}
//*********2、NSThread*********
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
//*********3、GCD*********
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
//*********4、NSOperation*********
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
- (void)threadTest{
NSLog(@"begin");
NSInteger count = 1000 * 100;
for (NSInteger i = 0; i < count; i++) {
// 栈区
NSInteger num = i;
// 常量区
NSString *name = @"zhang";
// 堆区
NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
NSLog(@"%@", myName);
}
NSLog(@"over");
}
void *pthreadTest(void *para){
// 接 C 语言的字符串
// NSLog(@"===> %@ %s", [NSThread currentThread], para);
// __bridge 将 C 语言的类型桥接到 OC 的类型
NSString *name = (__bridge NSString *)(para);
NSLog(@"===>%@ %@", [NSThread currentThread], name);
return NULL;
}
C和OC的桥接
其中涉及C与OC的桥接,有以下几点说明
__bridge只做类型转换,但是不修改对象(内存)管理权__bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象__bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。
多线程相关知识
同步线程:dispatch中的sync函数,即是在当前线程做事情
异步函数:dispatch中的async函数,即在另外一条线程做事情
并发队列:允许多个任务同时执行
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步函数(dispatch_async)下才有效
串行队列:让任务一个接一个地执行(执行完一个再执行下一个)
注:通过CF开头的函数创建出来的变量,需要手动调用CFRealease去释放,但GCD的是不用的
同步与异步:能否开启新线程 (决定了是在哪个线程执行)
- 同步:在当前线程中执行任务.不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 同步函数:立马在当前线程执行任务,执行完毕后才能继续往下执行,即同步函数内的任务不执行完,该函数就会卡住,不会继续往下执行
- 异步函数:不要求立马在当前线程执行任务,会等上一个任务执行完再执行
并发和串行:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完成后,再执行下一个任务
主队列是一种特殊的串行队列
只要是放到主队列的任务,都是在主线程执行
死锁:
队列的特点:FIFO(First In First Out)先进先出
产生死锁的两个情况:
- 当同步函数内的队列是主队列时,会产生死锁
- 使用sync函数往当前串行队列中添加任务,就会产生死锁
队列组:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i<5; i++) {
NSLog(@"任务1 ----%@",[NSThread currentThread]);
};
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i<5; i++) {
NSLog(@"任务2 ----%@",[NSThread currentThread]);
};
});
//等前面的任务都执行完,会自动执行这个任务
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i<5; i++) {
NSLog(@"任务3 ----%@",[NSThread currentThread]);
};
});
});
多线程的安全隐患:
Q:一块资源可能会被多个线程共享,容易造成数据错乱和数据异常的问题
A:解决方法:使用线程同步技术(同步:协同步调,按预定的先后顺序次序进行)
常用的线程同步技术:加锁
- OSSpinLock(自旋锁):等待锁的线程会处于忙等状态,一直占用着CPU资源(high-level lock)
//需要导入头文件<libkern/OSAtomic.h>
_lock = OS_SPINLOCK_INIT;//初始化
/*
//也可以这么初始化锁
static OSSpinLock lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = 0;//OS_SPINLOCK_INIT的值就是0
});
*/
OSSpinLockLock(&_lock);//加锁
//需加锁代码
OSSpinLockUnlock(&_lock);//解锁
注意点:
- 所有线程应该共用一把锁,不然还是会存在问题,每次都创建一把新锁
若有好几个方法同一时间只能使用一个方法,需要共用一把锁 - 原理:类似写了一个while循环
- 目前已经不再安全,有可能出现优先级反转的问题
若有线程1和线程2,假设线程1的优先级大于线程2,线程2先进入代码,发现锁未加锁,故加锁执行代码,线程1后进入,发现该锁已经被加锁,故忙等,但由于线程1优先级大于线程2,CPU分配更多时间执行线程1的代码,可能导致线程2的代码没有时间执行,导致无法解锁会进入一个类似死锁的状态,目前苹果已经不推荐使用
也可以使用static静态初始化锁,使自旋锁唯一,故锁不一定需要使用属性的方式
无法在static变量初始化时动态调用函数,只能取个值,因为static是静态初始化,右边的值在编译就需要确定,若需要动态调用函数,需要再次使用once函数,在once代码里赋值
当多条线程需要同时修改同一个值时,基本需要加锁,读取则不需要
- os_unfair_lock(low-level lock)
底层调用看,为互斥锁,等待os_unfair_lock锁的线程会处于休眠,而并非忙等,等不到锁就休眠(ios10以上使用)
//需要导入头文件<os/lock.h>
_lock = OS_UNFAIR_LOCK_INIT;//初始化
os_unfair_lock_lock(&_lock);//加锁
//需加锁代码
os_unfair_lock_unlock(&_lock);//解锁
- pthread_mutex(low-level lock)
互斥锁,等待锁的线程会处于休眠状态,不需要使用时,要手动销毁
注意点:结构体静态初始化只允许定义的同时进行赋值,不允许后续使用set方法赋值
struct Date {
int year;
int month;
}
struct Date date = {2011,10}//这样是可以的
struct Date date;
date = {2011,10}//这样是不可以的
//就是因为这样,解释了为什么不能定义一个pthread_mutex,再通过self的点语法方式对其进行静态初始化的原因
//需要导入头文件<pthread.h>
//静态初始化
//self.lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
pthread_mutex_init(&_lock, &attr);
//锁和条件可以在对象销毁时销毁(declloc)
//属性可以立马销毁
pthread_mutexattr_destroy(&attr);
/*
pthread_mutex_init(mutex, NULL);
//初始化时,可以将&attr传NULL,即默认为PTHREAD_MUTEX_DEFAULT
*/
将PTHREAD_MUTEX_DEFAULT属性替换为PTHREAD_MUTEX_RECUESIVE,将锁变为递归锁,解决递归可能导致死锁的问题
递归锁的本质:允许同一个线程对一把锁进行重复加锁
//条件锁
- (void)define{
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
pthread_cond_init(&_con, NULL);//创建一个条件
pthread_mutex_init(&_lock, &attr);
pthread_mutexattr_destroy(&attr);
}
- (void)using1{
pthread_mutex_lock(&_lock);
//需要执行的代码
pthread_cond_wait(&_con, &_lock);//会休眠,将锁放开,等待信号量到来,再次加锁执行下面代码
pthread_mutex_unlock(&_lock);
}
- (void)using2{
pthread_cond_signal(&_con);//告知对应的条件锁,该锁已经放开,可以继续使用
}
通过创建条件锁后,pthread_mutex能够办到线程等待的效果(多线程的依赖问题)
tip:
- 在汇编中敲入si,就是汇编指令级别的一行,遇到函数调用会进入函数内部
- 汇编中敲入c,就是直接到断点的位置
- NSLock(low-level lock)
是pthread_mutex的普通锁的封装
遵守NSLocking协议会执行下面两个函数:
- (void)lock;- (void)unlock;
其常用的API有2个:
- (BOOL)tryLock; 会进行尝试加锁,若能加锁,则加锁执行后续代码,不能加锁,则返回NO,继续执行后续代码(即相当于没有锁)- (BOOL)lockBeforeDate:(NSDate *)limit;会判断在limit时间到来之前,能加锁成功,则加锁,执行后续代码,若等到limit时间到来时,还没加锁成功,则执行后续代码,返回加锁失败
self.lock = [[NSLock alloc] init];
[self.lock lock];
//执行的代码
[self.lock unlock];
- NSRecursiveLock(low-level lock)
是pthread_mutex的递归锁的封装,用法与NSLock一致
- NSCondition(low-level lock)
是对pthread_mutex和cond的封装,即加锁解锁条件都能使用
其常用的API有4个:
- (void)wait;- (BOOL)waitUntilDate:(NSDate *)limit;- (void)signal;- (void)broadcast;
self.condition = [[NSCondition alloc] init];
[self.condition lock];
//需要执行的代码
[self.condition wait];
[self.condition unlock];
[self.condition signal];//给NSCondition发送信号
[self.condition broadcast];//给NSCondition发送广播
- NSConditionLock(low-level lock)
是对NSCondition的进一步封装,可以设置具体的条件值
- (instancetype)initWithCondition:(NSInteger)condition; 会初始化条件值,若直接使用init方法,则默认初始条件值为0- (void)lockWhenCondition:(NSInteger)condition; 当条件值为condition时加锁- (void)unlockWithCondition:(NSInteger)condition; 该方法会解锁,并将条件值修改为condition
self.lock = [[NSConditionLock alloc]initWithCondition:1];//初始化条件值为1;
[self.lock lockWhenCondition:1];//当条件值为1时加锁
//要做的事
[self.lock unlockWithCondition:2];//该方法会解锁,并将条件值修改为2
注:若调用lock方法是不管条件值,即无论条件值为多少,lock方法都能进;
- GCD串行队列
直接使用gcd的串行队列也可以实现线程同步的
- dispatch_semaphore
semaphore:信号量
- wait函数:如果信号量的值>0,就让信号量的值减1,继续往下执行代码
如果信号量的值<=0,就会休眠等待,知道信号量的值变成>0,然后就让信号量的值减1,继续往下执行代码 - signal函数:让信号量的值+1
self.lock = dispatch_semaphore_create(5);//信号量的初始值,可以用来控制线程并发访问的最大数量,最大并发数量在这里就是5
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
//这里的代码最多只有最大并发数条线程能同时执行,即最多只有5条线程能同时执行
dispatch_semaphore_signal(self.lock);
- @synchronized
是对mutex递归锁的封装,底层实现是根据传入的对象,在hash表中寻找对应的锁
@synchronized([self class]) { // objc_sync_enter
//要做的事
}// objc_sync_exit
//()中可以传入任何对象为锁对象,当()中的锁对象一致时,表示共用一把锁
以上的锁的性能从高到低排序:
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- GCD串行队列
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
推荐使用:dispatch_semaphore和pthread_mutex,是相对来说性能最高的
当要实现一个方法一把锁时,可以通过static静态初始化变量,再通过dispatch_once方法,令该方法使用唯一一把锁(记得写在方法内部),也可以抽成宏定义,若多个地方需共用一把锁,只能把锁抽出来一起使用
属性的原子性与非原子性:
- atomic:给属性加上atomic修饰,可以保证属性的setter和getter都是原子性操作,也就是说,保证setter和getter内部是线程同步,即在setter和getter内部中做加锁解锁的操作
原子性操作:即保证多行代码能够按顺序执行完成,就如当做一个整体,同一行代码
atomic并不能保证使用属性的过程是线程安全的,即只能保证setter和getter内部是线程安全的
但atomic耗费性能,因为属性的setter和getter是调用很频繁的,但真正需要加锁操作时,再去进行加锁操作即可
文件I/O操作:
多读单写(读写安全):
1.读取文件是同一时间允许多条线程同时进行读的操作
2.只允许单条线程进行写的操作
3.不允许既有读的操作又有写的操作 ,常用于文件等数据的读写操作
- pthread_rwlock:等待锁的线程会进入休眠
//需要导入#import <pthread.h>
pthread_rwlock_init(&_lock, NULL);//初始化锁
pthread_rwlock_rdlock(&_lock);//读锁加锁
pthread_rwlock_wrlock(&_lock);//写锁加锁
pthread_rwlock_unlock(&_lock);//解锁
- dispatch_barrier_async
执行到barrier里面的任务时,会自动出现一个栅栏,执行任务,其他任务时无法再进行读取操作的
//这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的,如果传入的是一个串行或者是一个全局的并发队列,那这个函数等同于dispatch_async函数的效果
self.queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
- (void)read {
dispatch_async(self.queue, ^{
//读操作
});
}
- (void)write {
dispatch_barrier_async(self.queue, ^{
//写操作
});
}
注意点:这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的,如果传入的是一个串行或者是一个全局的并发队列,那这个函数等同于dispatch_async函数的效果