为什么要学习多线程编程?
多线程编程能够更加充分合理的利用操作系统多核cpu,能让多核cpu并发执行多个任务,比如将耗时线程放入子线程中执行,让主线程能够更好的处理逻辑事件与UI事件,利用子线程在后台不知不觉的做一些事情,对用户操作UI事件的影响降到最低,提升用户操作体验等,所以多线程是每个优秀的开发人员必备技能,并且需要熟练掌握,早掌握早受益。
你将从本文学到什么?
1、ios中常见的多线程方案
2、详细介绍C语言经典多线程方案GCD
3、线程同步技术
4、各种锁的原理及使用
注:笔者也是初学者,水平有限,文中难免出现错误的地方,还望大家纠正。
ios中4种常见的多线程方案:pthread、NSThread、GCD、NSOperation
- pthread:一套通用的多线程API,采用C语言编写,优点在于适用于各大操作系统,跨平台与可移植性强;缺点:使用难度大。其他三类多线程技术底层全是pthread,都是对其的封装。
- NSThread:采用OC语言编写,使用更加面向对象,使用简单,可直接操作线程对象,需要手动管理线程生命周期。
- GCD:旨在替代NSThread多线程技术,充分利用设备多核资源,采用C语言编写,是ios非常常用的多线程技术,需要重点掌握,本文也将着重介绍。
- NSOperation:对GCD面向对象的封装,比GCD多了一些简单实用的功能,使用OC语言编写,是ios开发者较常用的多线程技术。
GCD常用函数
GCD源码下载地址:https://github.com/apple/swift-corelibs-libdispatch
GCD有两个用来执行任务的函数:dispatch_sync、dispatch_async。
- dispatch_sync:同步执行任务,不具备开启线程的能力。
- dispatch_async:异步执行任务,具备开启子线程的能力。
GCD队列
GCD队列有两种类型,并发队列与串行队列
- 并发队列:可以让多个任务并发执行,自动开启多个线程同时执行任务。
- 串行队列:任务只能按序执行。
可使用dispatch_get_global_queue获取全局并发队列,也可使用dispatch_queue_create创建新的队列,创建队列可指定队列类型,
DISPATCH_QUEUE_SERIAL:串行队列,
DISPATCH_QUEUE_CONCURRENT:并行队列
同步与异步指的是是否具备开启新线程的能力,串行与并行指的是任务的执行方式。
- 使用异步与同步的方式执行并行队列观察情况:
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue2 = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
//queue1是全局并发队列,queue2是手动创建的并发队列,两个队列的能力相同
dispatch_async(queue1, ^{
NSLog(@"queue1开启了新线程:%@",[NSThread currentThread]);
});
dispatch_sync(queue1, ^{
NSLog(@"queue1未开启子线程:%@",[NSThread currentThread]);
});
- 使用异步与同步的方式执行串行队列观察情况:
dispatch_queue_t queue3 = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue3, ^{
NSLog(@"queue3开启了新线程:%@",[NSThread currentThread]);
});
dispatch_sync(queue3, ^{
NSLog(@"queue3未开启子线程:%@",[NSThread currentThread]);
});
GCD队列组
GCD队列组可以用来解决任务依赖问题,例如任务1与任务2需要并发执行,任务3需要等待任务1与任务2执行完成后方可执行,这个时候就可以使用队列组来解决这个问题。运行以下代码观察情况:
//创建队列组
dispatch_group_t group = dispatch_group_create();
//创建并发队列
dispatch_queue_t queue = dispatch_queue_create("groupQueue", DISPATCH_QUEUE_CONCURRENT);
//创建队列组多线程任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"task 1");
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"task 2");
}
});
//任务1与任务2执行完毕通知队列组
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"task 3");
}
});
运行结果:
2021-11-23 21:52:55.453484+0800 SwiftApp[1111:26411] task 1
2021-11-23 21:52:55.453519+0800 SwiftApp[1111:28289] task 2
2021-11-23 21:52:55.453724+0800 SwiftApp[1111:26411] task 1
2021-11-23 21:52:55.453796+0800 SwiftApp[1111:28289] task 2
2021-11-23 21:52:55.453861+0800 SwiftApp[1111:26411] task 1
2021-11-23 21:52:55.453911+0800 SwiftApp[1111:28289] task 2
2021-11-23 21:52:55.453945+0800 SwiftApp[1111:26411] task 1
2021-11-23 21:52:55.454002+0800 SwiftApp[1111:28289] task 2
2021-11-23 21:52:55.454032+0800 SwiftApp[1111:26411] task 1
2021-11-23 21:52:55.455312+0800 SwiftApp[1111:28289] task 2
2021-11-23 21:52:55.457068+0800 SwiftApp[1111:28289] task 3
2021-11-23 21:52:55.457613+0800 SwiftApp[1111:28289] task 3
2021-11-23 21:52:55.458144+0800 SwiftApp[1111:28289] task 3
2021-11-23 21:52:55.458563+0800 SwiftApp[1111:28289] task 3
2021-11-23 21:52:55.458910+0800 SwiftApp[1111:28289] task 3
可以看到task 1与task 2交替执行,而task 3需要等到task 1与task 2执行完毕后才能执行。
多线程的安全隐患
- 死锁
使用sync向当前串行队列里面添加任务,会产生死锁卡住当前线程。
dispatch_queue_t queue3 = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue3, ^{
NSLog(@"queue3开启了新线程:%@",[NSThread currentThread]);
dispatch_sync(queue3, ^{
NSLog(@"向当前队列添加同步任务,这里造成了死锁。");
});
NSLog(@"此处代码不会执行");
});
防止死锁,只需要破坏它的两个条件之一即可:sync、当前串行队列
- 线程保活
思考以下代码的执行结果
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"2");
});
- (void)test {
NSLog(@"3");
}
执行结果:
2021-11-23 22:11:43.234225+0800 SwiftApp[1317:41935] 1
2021-11-23 22:11:43.234956+0800 SwiftApp[1317:41935] 2
可以看到“3”并没有输出,这是为什么呢?
原因是performSelector:withObject:afterDelay:的本质是向runloop中添加了定时器,以此来达到afterDelay的效果,但是子线程默认是没有开启runloop的,也就是子线程执行完当前任务就挂了,所以runloop中的afterDelay自然也就没有效果了。
如何解决这个问题呢?
我们只需要保住子线程的生命周期,不要让他挂了得那么快即可,如何保住子线程的生命周期?只需要开启子线程的runloop即可。代码改进如下:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"2");
//开启运行循环,保住子线程的生命周期
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
这个时候的输出结果就是完整的了。与此类似的还有以下情况:
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:true];
-
资源共享
顾名思义就是多个线程对同一个对象进行操作,很容易引起数据错乱与数据安全问题,如下图:
线程A与线程B访问了同一对象,并且都进行了+1操作,由于他们拿到的值都是17所以他们+1后的值都是18,很明显这不是我们想要的结果19,那么如何解决这个问题呢?请看下一节。
线程同步技术
线程同步技术顾名思义就是协同步调,按照预定的先后顺序执行,常见的线程同步技术是:加锁,加锁的目的是为了让队列里面的任务同步执行,从而避免资源竞争的问题。
ios线程同步方案有以下几种:
- OSSpinLock
OSSpinLock本质是“自旋锁”,等待锁的线程会处于忙等状态,一直占着cpu的资源,目前已不再安全,可能会出现优先级反转的问题,如果等待锁的线程优先级比较高,它会一直占着cpu的资源,持有锁的线程优先级比较低得不到cpu的资源执行任务会一直持有锁无法释放。因此OSSpinLock从ios10开始弃用,使用OSSpinLock需要导入头文件#import<libkern/OSAtomic.h>,用法如下:
//初始化OS_SPINLOCK_INIT
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&lock);
//...加锁代码
//解锁
OSSpinLockUnlock(&lock);
- os_unfair_lock
互斥锁,等到锁的线程会进入休眠状态,直到锁解开才会继续执行任务。os_unfair_lock从ios10开始使用,其目的是用来取代OSSpinLock。使用此锁需要引入头文件#import <os/lock.h>,其用法如下:
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//尝试加锁
os_unfair_lock_trylock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
- pthread_mutex_t
此锁为互斥锁,等待锁的线程会进入休眠状态, 需要注意的是此锁需要手动释放内存,使用此锁需要引入头文件#import <pthread/pthread.h>,用法:
//声明pthread_mutex_t锁
pthread_mutex_t lock;
//初始化
pthread_mutex_init(&lock, NULL);
//加锁
pthread_mutex_lock(&lock);
//解锁
pthread_mutex_unlock(&lock);
//尝试加锁
pthread_mutex_trylock(&lock);
//销毁锁
pthread_mutex_destroy(&lock);
- pthread_mutex_t 递归锁
pthread_mutex_t在初始化pthread_mutex_init的时候需要传入两个参数,第一个参数是锁地址,第二个参数是锁的属性pthread_mutexattr_t,此属性可以用来指定锁的类型:
/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2//递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
我们如果需要可以把锁设置成PTHREAD_MUTEX_RECURSIVE递归锁,PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_DEFAULT为常规锁。用法:将以下属性att放入pthread_mutex_init初始化方法第二个参数即可:
//属性申明
pthread_mutexattr_t att;
//初始化
pthread_mutexattr_init(&att);
//设置属性
pthread_mutexattr_settype(&att, PTHREAD_MUTEX_RECURSIVE);
//销毁属性
pthread_attr_destroy(&att);
- pthread_mutex_t 条件锁,用法如下:
//条件申明
pthread_cond_t cond;
//初始化
pthread_cond_init(&cond, NULL);
//等待条件,进入休眠状态
pthread_cond_wait(&cond, &lock);
//发送条件信号
pthread_cond_signal(&cond);
//发送条件广播,激活所有等待此锁的线程
pthread_cond_broadcast(&cond);
//销毁条件
pthread_cond_destroy(&cond);
- 其他OC类型的锁
1.NSLock:对pthread_mutex_t普通锁的OC封装。
2.NSRecursiveLock:对pthread_mutex_t递归锁的OC封装。
3.NSCondition:对pthread_mutex_t条件锁pthread_cond_t的OC封装。
4.NSConditionLock:对NSCondition的进一步封装。
NSLock *lock = [[NSLock alloc]init];
[lock lock];
[lock tryLock];
[lock unlock];
NSLock *recursiveLock = [[NSRecursiveLock alloc]init];
[recursiveLock lock];
[recursiveLock tryLock];
[recursiveLock unlock];
NSCondition *cond = [[NSCondition alloc]init];
[cond wait];
[cond signal];
[cond broadcast];
NSConditionLock *condLock = [[NSConditionLock alloc]init];
[condLock lockWhenCondition:0];
[condLock unlockWithCondition:1];
- dispatch_semaphore
semaphore叫做信号量,可以用来控制最大线程并发数,可以将信号量设置为1,也就是控制最大线程并发数为1,从而达到线程同步的目的。用法:
//创建信号量dispatch_semaphore_t对象,1为最大线程并发数
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
//信号量大于1时减1继续往下执行代码,信号量小于1时线程阻塞,等待信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//释放信号量 +1操作
dispatch_semaphore_signal(semaphore);
- dispatch_queue
串行队列也可以用来解决线程同步问题,任务本身就是按序执行的,不存在并发一说。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
});
- @synchronized
是对mutex递归锁的封装,可以在objc源码里面查看。
@synchronized (self) {
}
以上就是ios中所有的线程同步方案了,推荐使用dispatch_semaphore信号量或pthread_mutex来加锁。os_unfair_lock与OSSpinLock虽然加锁效率更高,但是不适用所有ios系统,其他的OC对象锁效率都比较低,因为都是对pthread_mutex锁的封装。@synchronized因为本质是递归锁效率是最低的。除OSSpinLock是自旋锁以为其余的锁全是互斥锁。
atomic
atomic叫做原子操作,其本质是在属性的getter与setter方法内部添加了pthread_mutex普通同步锁,它并不能保证使用属性的过程是线程安全的。由于atomic的加锁解锁需要消耗额外的cpu资源并不推荐使用。我们在使用属性的时候可以使用多线程同步技术来保证属性的线程安全即可。
读写安全方案
很明显我们在只读的时候并不需要考虑多线程问题,只要没有线程进行写操作就不会对多线程读造成影响。而写的过程中肯定是不能进行多线程写入的。同理也不能边写边读。我们再来考虑多线程同步技术是否能解决此问题?多线程同步技术很明显是控制最大线程并发数来解决多线程安全问题,而我们在读的时候希望是并发多线程读取不想受到多线程同步技术的限制,所以多线程同步技术并不适合用在这个场景。ios提供两套方案来保证多线程读写安全,pthread_rwlock与dispatch_barrier_async。
- pthread_rwlock 读写锁
//申明
pthread_rwlock_t lock;
//初始化
pthread_rwlock_init(&lock, NULL);
//读加锁
pthread_rwlock_rdlock(&lock);
//写加锁
pthread_rwlock_wrlock(&lock);
//读尝试加锁
pthread_rwlock_tryrdlock(&lock);
//写尝试加锁
pthread_rwlock_trywrlock(&lock);
//解锁
pthread_rwlock_unlock(&lock);
//销毁锁
pthread_rwlock_destroy(&lock);
- dispatch_barrier_async 异步栅栏调用
dispatch_queue_t queue = dispatch_queue_create("barrierQueue", DISPATCH_QUEUE_CONCURRENT);
//多线程读方案
dispatch_async(queue, ^{
});
//写方案,一旦进入此流程,并发队列queue将停止派发任务
dispatch_barrier_async(queue, ^{
});
异步栅栏调用的原理是利用dispatch_barrier_async函数特性,进入此任务的队列不会再向其他线程派发任务,除非栅栏调用执行完成。需要注意的是此方案需要使用dispatch_queue_create手动创建并发队列方可有效。
GNUstep
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值,源码地址:http://www.gnustep.org/resources/downloads.php