iOS中的多线程技术
从很多年前开始,CPU 的频率增长就出现停滞,转而向多核的方向发展。增加核心远远比提升制程、架构要更简单。因此多线程技术也有着越来越重要的地位。
一、多线程相关知识
1.1 进程
- 进程是指在系统中正在运行的一个应用程序,比如同时打开微信和Xcode,系统会分别启动2个进程;
- 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内;
1.2 线程
-
一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程),是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位;
-
一个程序有且只有一个主线程,程序启动时创建(调用
main
来启动),主线程的生命周期是和应用程序绑定,程序退出时,主线程也停止;
1.3 多线程
- 概念:一个进程中可以开启多条线程,每一条线程可以并行(同时)执行不同的任务
- 原理:同一时间,CPU只能处理一条线程,只有一条线程在工作,多线程并发(同时)执行,其实是CPU快速的在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
- 注意:如果线程很多,CPU会在N多线程之间调度,会消耗大量CPU资源,每条线程被调度执行的频次会降低(线程的执行效率会降低)
1.4 多线程的优缺点
-
优点: 能适当的提高程序的执行效率以及资源利用率(CPU、内存利用率)
-
缺点: 每创建一个线程是会占用资源的,比如内存开销等;线程太多,会降低程序的性能; 程序开发复杂度上升
1.5 主线程
-
一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
-
作用: 显示/刷新UI界面, 处理UI事件(点击事件,滚动事件,拖拽事件)
-
使用注意:不要将耗时的操作放到主线程中,耗时操作应放在子线程(后台线程,非主线程); 凡是和UI相关的操作应放在主线程中操作
1.6 iOS中多线程的实现方案
- pthread :一套通用的多线程API,很少用到,c语言,线程生命周期由程序员管理。
- NSTread:oc语言,面向对象,简单易用,可直接操作线程对象 ,线程生命周期由程序员管理
- GCD: 常用,替代NSThread等线程技术,充分利用设备的多核,c语言,线程生命周期自动管理
- NSOperation: 常用,是对GCD封装,使用更加面向对象,线程生命周期自动管理
二、Pthreads
Pthreads 是POSIX 多线程开发框架,是跨平台的 C 语言框架,需要自己管理线程的创建销毁等操作。这些 API 全都以 pthread_
作为前缀。iOS 中 CFRunLoop就是基于Pthreads来管理的。更加详细的关于 Pthreads的学习可以参考 这里
pthread_create():创建一个线程
pthread_exit():终止当前线程
pthread_cancel():中断另外一个线程的运行
pthread_join():阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init():初始化线程的属性
pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate():获取脱离状态的属性
pthread_attr_destroy():删除线程的属性
pthread_kill():向线程发送一个信号
pthread_equal(): 对两个线程的线程标识号进行比较
pthread_detach(): 分离线程
pthread_self(): 查询线程自身线程标识号
...
三、NSThread
一个NSThread对象就代表一条线程
3.1 NSThread常用方法
// 创建、启动线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
// 创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"OC"];
// 隐式创建并启动线程
[self performSelectorInBackground:@selector(run:) withObject:@"OC"];
// 主线程相关用法
[NSThread mainThread]; // 获得主线程
[NSThread isMainThread]; // 是否为主线程
[thread isMainThread]; // 是否为主线程
// 获得当前线程
NSThread *current = [NSThread currentThread];
// 休眠线程
[NSThread sleepForTimeInterval:2]; //休眠2s
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]]; //休眠2s
// 强制退出线程,不推荐使用此方式退出子线程,可能会造成内存泄漏
[NSThread exit];
3.2 NSThread创建常驻线程
当然,我们也可以增加一个特殊的线程常驻RunLoop,防止线程退出。
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(onRunLoop) object:nil];
[runLoopThread start];
[self performSelector:@selector(dothingOnRunLoop:) onThread:runLoopThread withObject:@[@"常驻RunLoop线程"] waitUntilDone:YES];
// 常驻runLoop
- (void)onRunLoop {
@autoreleasepool {
[NSThread currentThread].name = @"常驻RunLoop线程";
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
// 在runloop上执行操作
- (void)dothingOnRunLoop:(id)param {
// 可以在此打断点测试线程是否已经加入runloop
NSLog(@"跑在runloop上的线程: %@", param);
}
3.3 NSThread线程间通讯
NSThread通过以下四种方式进行线程之间的通信
// 指定方法在主线程中执行, 参数1. SEL 方法 2.方法参数 3.是否等待当前执行完毕 4.指定的Runloop model
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 指定方法在某个线程中执行, 参数1. SEL 方法 2.方法参数 3.是否等待当前执行完毕 4.指定的Runloop model
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
// 指定方法在开启的子线程中执行, 1. SEL 方法 2.方法参数
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg
四、GCD
有关GCD相关内容,请参考另一篇文章 关于GCD的那些事儿
五、NSOperation、NSOperationQueue
有关NSOperation、NSOperationQueue相关内容,请参考另一篇文章 iOS多线程:NSOperation、NSOperationQueue总结
六、 多线程的安全隐患
一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源;或者说是多个线程访问同一个对象、变量、文件等等。这时候,如果不采取一定的措施,很容易引发数据错乱和数据安全的问题。
6.1 iOS多线程安全问题及方案
举个例子,银行账目上原先有1000,在取钱500的同时,又存钱500,这时候大家第一时间就是觉得,还剩余1000,这只是正常现象,如果存取操作同时进行的话,因为基础都是1000,所以,取完钱剩余500,存完钱剩余1500,这两个数据任何一个放回账目上都是不正确的。这就是多条线程同时操作一个对象所引发的问题。
- (void)moneyTest {
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 存钱任务
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self __saveMoney];
}
});
// 取钱任务
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self __drawMoney];
}
});
}
// 存钱
- (void)__saveMoney {
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
// 取钱
- (void)__drawMoney {
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
最终日志:
2019-11-22 17:05:18.577892+0800 多线程-demo[67341:21672333] 存50,还剩440元 - <NSThread: 0x600001807740>{number = 6, name = (null)}
在正常流程下,最后的结果是钱剩余400,票剩余0,可以看到,钱最终的结果是错的,这就是多线程同事操作同一个对象或者方法造成的隐患。这个,我们解决问题的方案就是:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)。而在iOS领域中,常见的线程同步技术就是:加锁。
先来看看我们iOS线程同步技术的一些方案,也就是我们接下来要分析的方案:
// 性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
NSRecursiveLock
NSConditionLock
@synchronized
6.2 OSSpinLock
第一种OSSpinLock,这个方案已经被苹果废弃,当然,现在仍然可用,只是苹果不推荐使用。OSSpinLock是一个自旋锁,通过加锁解锁,可以有效解决多线程隐患问题。
// 首先,我们创建一个子类BLOSSpinLockDemo,继承BLBaseDemo,然后定义个锁,初始化对象的同时对它们进行初始化。然后重写卖票和存取钱的方法,在方法中加锁解锁:
@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock moneyLock;
@end
@implementation OSSpinLockDemo
- (instancetype)init {
if (self = [super init]) {
self.moneyLock = OS_SPINLOCK_INIT;
}
return self;
}
- (void)__drawMoney {
OSSpinLockLock(&_moneyLock);
[super __drawMoney];
OSSpinLockUnlock(&_moneyLock);
}
- (void)__saveMoney {
OSSpinLockLock(&_moneyLock);
[super __saveMoney];
OSSpinLockUnlock(&_moneyLock);
}
@end
最终打印结果如下:
2019-11-22 17:10:18.577892+0800 多线程-demo[67341:21672333] 存50,还剩400元 - <NSThread: 0x600001807740>{number = 6, name = (null)}
钱的结果正是我们想要的,并且是正确的结果
6.3 os_unfair_lock
OSSpinLock自旋锁会出现一种状况,当优先级低的线程加锁之后,优先级高的线程在等待的过程中,可能出现优先级高的线程会一直占着CPU资源,导致优先级低的线程没法释放锁,出现线程死锁状态。而互斥锁虽然在唤醒线程的时候会消耗CPU,但是不会出现死锁状态,相对比较安全,所以目前苹果从iOS10开始就推荐大家用os_unfair_lock来替换OSSpinLock。接下来,我们来看看os_unfair_lock的使用
@interface OSUnfairLockDemo()
@property (assign, nonatomic) os_unfair_lock moneyLock;
@end
@implementation OSUnfairLockDemo
- (instancetype)init {
if (self = [super init]) {
self.moneyLock = OS_SPINLOCK_INIT;
}
return self;
}
- (void)__drawMoney {
os_unfair_lock_lock(&_moneyLock);
[super __drawMoney];
os_unfair_lock_unlock(&_moneyLock);
}
- (void)__saveMoney {
os_unfair_lock_lock(&_moneyLock);
[super __saveMoney];
os_unfair_lock_unlock(&_moneyLock);
}
@end
最终打印结果如下:
2019-11-22 17:13:18.577892+0800 多线程-demo[67341:21672333] 取20,还剩400元 - <NSThread: 0x600001807740>{number = 7, name = (null)}
6.4 pthread_mutex
pthread_mutex,c语言编写的锁,也是一种互斥锁。先看看基本的使用
@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t moneyLock;
@end
@implementation MutexDemo
- (void)initMutex:(pthread_mutex_t *)mutex {
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化mutex,参数一是pthread_mutex_t(锁) 参数二是pthread_mutexattr_t(属性)
pthread_mutex_init(mutex, &attr);
//使用完属性之后 要销毁
pthread_mutexattr_destroy(&attr);
}
- (instancetype)init {
if (self = [super init]) {
[self initMutex:&_moneyLock];
}
return self;
}
- (void)__drawMoney {
pthread_mutex_lock(&_moneyLock);
[super __drawMoney];
pthread_mutex_unlock(&_moneyLock);
}
- (void)__saveMoney {
pthread_mutex_lock(&_moneyLock);
[super __saveMoney];
pthread_mutex_unlock(&_moneyLock);
}
@end
最终打印结果如下:
2019-11-22 17:18:18.577892+0800 多线程-demo[67341:21672333] 取20,还剩400元 - <NSThread: 0x600001807740>{number = 6, name = (null)}
6.5 dispatch_semaphore
GCD中的信号量dispatch_semaphore,这也是同步技术方案中,比较方便的一直方案。
@interface GCDSemaphoreDemo ()
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@end
@implementation GCDSemaphoreDemo
- (instancetype)init {
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)__drawMoney {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
[super __drawMoney];
dispatch_semaphore_signal(self.semaphore);
}
- (void)__saveMoney {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
[super __saveMoney];
dispatch_semaphore_signal(self.semaphore);
}
@end
最终打印结果如下:
2019-11-22 20:43:02.167794+0800 多线程-demo[80757:22055581] 取20,还剩400元 - <NSThread: 0x600002f94e00>{number = 3, name = (null)}
6.6 其它方案
NSCondition,其实就是对pthread_cond_t加pthread_mutex_t的封装使用,而NSConditionLock,则是对NSCondition的进一步封装。
对于dispatch_queue(DISPATCH_QUEUE_SERIAL)大家都知道,因为串行队列,不管你是同步还是异步,它都是依次执行任务的,所以可以达到加锁效果。
对于@synchronized,可能大家在日常的项目中已经有所用到,大概就是@synchronized(self),其实,在()中,只要是同一个对象,就可以达到加锁的效果,这里我就不做过多说明了,但是这个加锁方式性能上不是很好,因为这简单一句话,封装了很多内容,不建议大家使用。
本文首发于我的个人博客 https://limeng99.club/,转载请标明出处。