我们平时在开发中比较常用的多线程主要包括三个:NSThread、NSOperation和GCD,当然还有一个较底层的pthread,这三种底层实现都是基于pthread,本文着重讲述GCD的使用以及线程同步问题
先看下两个概念,同步异步任务,串行并行队列
同步异步决定着是否有开启子线程的能力,串行并行决定着任务执行的先后顺序
再看下GCD的简单实用
如果有三个线程A、B、C(C是主线程),需求是先让A、B并行执行完后再执行C!这个时候就可以使用GCD里面的线程组了
-(void)groupTest{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 子线程任务A
dispatch_group_async(group, queue, ^{
sleep(0.1);
for(inti =0; i <5; i ++) {
NSLog(@"--1--%@--",[NSThreadcurrentThread]);
}
});
// 子线程任务B
dispatch_group_async(group, queue, ^{
sleep(0.1);
for(inti =0; i <5; i ++) {
NSLog(@"--2--%@--",[NSThreadcurrentThread]);
}
});
dispatch_group_notify(group, queue, ^{
// 主线程任务C
dispatch_async(dispatch_get_main_queue(), ^{
for(inti =0; i <5; i ++) {
NSLog(@"--3--dispatch_group_notify -- %@--",[NSThread currentThread]);
}
});
});
}
再看下打印结果:
2019-12-02 15:28:42.106699+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--
2019-12-02 15:28:42.106869+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--
2019-12-02 15:28:42.106985+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--
2019-12-02 15:28:42.107052+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--
2019-12-02 15:28:42.107114+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--
2019-12-02 15:28:42.107177+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--
2019-12-02 15:28:42.107257+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--
2019-12-02 15:28:42.107319+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--
2019-12-02 15:28:42.107377+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--
2019-12-02 15:28:42.107435+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--
2019-12-02 15:28:42.119624+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--
2019-12-02 15:28:42.119739+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--
2019-12-02 15:28:42.119803+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--
2019-12-02 15:28:42.119862+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--
2019-12-02 15:28:42.119920+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--
可以看出线程A和B并行执行完后才执行C,需要注意的是group和queue必须是同一个,而且queue必须是并发队列,如果是串行队列,线程A和B就是按顺序执行了!
上面是GCD和队列的一个简单应用,下面看下一个多线程的一个经典应用:卖票!
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,assign) int ticketCount;
@end
@implementationViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 30;
[self sellingTickets];
}
//卖一张票
-(void)sellingTicket{
intoldTicketCount =self.ticketCount;
// 休眠更能体现线程安全问题
sleep(0.3);
oldTicketCount --;
self.ticketCount= oldTicketCount;
NSLog(@"还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);
}
//三个窗口同时卖票,每个窗口都卖掉10张票
-(void)sellingTickets{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for(inti =0; i <10; i ++) {
[selfsellingTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for(inti =0; i <10; i ++) {
[selfsellingTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for(inti =0; i <10; i ++) {
[selfsellingTicket];
}
});
}
再看下代码执行的打印结果
可以看出数据是错乱的,而且每次运行结果都会有点不一样,这个时候就要进行线程同步了,我们在开发过程中最常用的就是加锁,iOS加锁的方式有很多,找了一个网上的性能对比图
1、OSSpinLock(自旋锁),顾名思义,就是自己在那里旋转,只要发现这个锁还没有被解开,一直占用CPU资源,处于忙等状态,直到锁被解开,下面会证明这个问题
先看下如何使用,需要导入头文件#import <libkern/OSAtomic.h>
//卖一张票
-(void)sellingTicket{
static OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
intoldTicketCount =self.ticketCount;
// 休眠更能体现线程安全问题
sleep(0.3);
oldTicketCount --;
self.ticketCount= oldTicketCount;
NSLog(@"还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);
OSSpinLockUnlock(&lock);
}
使用起来非常简单,OS_SPINLOCK_INIT就是0,所以这里可以直接使用static变量,如果不是一个具体的值就不能这样初始化了,可以写成属性,效果一样,不过OSSpinLock这个锁在iOS10之后就被弃用了,有可能会出现优先级反转问题,这里简单说下出现优先级反转问题的原因:
由于OSSpinLock这个自旋锁有个优先级的概念,就是优先级比较高的线程会被多分配一些CPU资源,如果有两个线程A、B,A的优先级较低,B的优先级较高,如果较低的线程A先进入方法进行加锁,当线程B进来时会等待线程A进行解锁才能继续执行,处于忙等状态,由于线程B是优先级较高的线程,系统会多分配一些资源给线程B进行自旋的操作,有可能导致线程B占用的资源过多,导致线程A分配的资源不足无法继续执行下面的代码,这样就造成了线程A的锁永远都无法被解锁!
下面通过汇编(这里是真机模式,ARM64架构)验证OSSpinLock自旋锁在等待锁被放开期间是一直处于循环忙等状态,先说下思路:在加锁的地方打上断点,过掉第一条线程,查看第二条线程在等待过程中的汇编干了些啥
第二条线程进入后直接进入汇编模式,Debug-》Debug Workflow-》勾上Always show Disassembly,然后一路敲si,直到出现下面的汇编为止
你会方向进入这个汇编指令后一直敲si,会在图片中阴影部位一直循环跳转,也就是进入了一个while循环,这也就证明了自旋锁是一直处于循环忙等状态
2、os_unfair_lock(自旋锁),OSSpinLock锁被废弃后,苹果推荐使用os_unfair_lock这个锁,不过有的文章说这是互斥锁,但本人自己测试过,看到的情景就是符合自旋锁,如有不正确,还请指出,万分感谢!
先看下如何使用,需要导入头文件#import <os/lock.h>
用法也很简单,需要注意的是,加锁解锁需要传入一个指针变量os_unfair_lock_t,可以看下这个定义
所以将os_unfair_lock这个结构体的地址传进去就可以了,按照同样的方法,进入第二条线程的汇编
可以看出一直在图片标明的部位循环执行,这也证明了os_unfair_lock是自旋锁
3、pthread_mutex_t(互斥锁),互斥锁意思就是在等待锁被放开期间该线程处于休眠状态,汇编证明可以自己尝试下,这里就不再累赘
先看下简单使用
pthread_mutex_t的用法有很多,这只是普通用法,假如有这样的一个场景,当票卖光了,就不能继续卖了,而是等待有人退票,退票成功后再回到卖票线程中继续卖票,类似生产者消费者模式,说白了就是消费者想买东西,必须依靠生产者生产出东西才能买到东西,直接看代码
#import "ViewController.h"
#import
@interface ViewController ()
@property(nonatomic,assign) int ticketCount;
@property(nonatomic,assign)pthread_mutex_t lock;
@property(nonatomic,assign)pthread_cond_t cond;
@end
@implementationViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 0;
pthread_mutex_t lockTmp = PTHREAD_MUTEX_INITIALIZER;
self.lock= lockTmp;
pthread_cond_t condTmp = PTHREAD_COND_INITIALIZER;
self.cond= condTmp;
[self sellingTickets];
}
-(void)sellingTickets{
[[[NSThread alloc] initWithTarget:self selector:@selector(sellingTicket) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(refundTicket) object:nil] start];
}
//退一张票
-(void)refundTicket{
NSLog(@"进入退票--refundTicket");
pthread_mutex_lock(&_lock);
sleep(2);
intoldTicketCount =self.ticketCount;
// 休眠更能体现线程安全问题
oldTicketCount ++;
self.ticketCount= oldTicketCount;
NSLog(@"退票,还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_lock);
}
//卖一张票
-(void)sellingTicket{
NSLog(@"进入卖票--sellingTicket");
pthread_mutex_lock(&_lock);
if(self.ticketCount==0) {
pthread_cond_wait(&_cond, &_lock);
}
intoldTicketCount =self.ticketCount;
oldTicketCount --;
self.ticketCount= oldTicketCount;
NSLog(@"卖票,还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);
pthread_mutex_unlock(&_lock);
}
上面的代码中创建了两个线程,一个执行卖票一个执行退票,而且卖票先执行,并且初始化的票数为0,再说下这句代码的作用
if(self.ticketCount==0) {
pthread_cond_wait(&_cond, &_lock);
}
如果票数为0,则解开当前的锁,并一直处于等待状态,这时退票任务就可以进行加锁,执行退票任务了,如果被再次唤醒而且锁已经被解开,则会再次加锁继续执行下面的任务。
pthread_cond_signal(&_cond); 这句代码意思是唤醒之前等待的地方,但只是被唤醒,退票的锁还没有解开前,卖票任务并不会执行,当退票任务中的锁被解开时,卖票任务才会继续执行
需要注意的是pthread_mutex_t创建的锁和一些条件参数在不使用的时候需要释放掉
-(void)dealloc{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
后面陆续补充pthread_mutex_t中递归锁以及其他类型的锁的用法!
最后再看一下IO操作相关的读写锁:pthread_rwlock_t
先看下普通锁的一个情况
self.lock1= [[NSLockalloc]init];
for(inti =0; i <5; i ++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(read1) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(write1) object:nil] start];
}
-(void)read1{
[self.lock1lock];
NSLog(@"----read----");
sleep(1);
[self.lock1unlock];
}
-(void)write1{
[self.lock1lock];
NSLog(@"----write----");
sleep(2);
[self.lock1unlock];
}
2019-12-02 17:11:27.015666+0800 GCD[58077:16338699] ----read----
2019-12-02 17:11:28.021159+0800 GCD[58077:16338700] ----write----
2019-12-02 17:11:30.023676+0800 GCD[58077:16338701] ----read----
2019-12-02 17:11:31.028279+0800 GCD[58077:16338702] ----write----
2019-12-02 17:11:33.031558+0800 GCD[58077:16338703] ----read----
2019-12-02 17:11:34.032123+0800 GCD[58077:16338704] ----write----
2019-12-02 17:11:36.035910+0800 GCD[58077:16338705] ----read----
2019-12-02 17:11:37.040473+0800 GCD[58077:16338707] ----read----
2019-12-02 17:11:38.045163+0800 GCD[58077:16338708] ----write----
2019-12-02 17:11:40.049023+0800 GCD[58077:16338706] ----write----
可以看出普通锁对读和写进行加锁后,读和写同一时间只能有一条线程进行操作,但对于文件IO操作,我们需要做到的是:
1、同一时间只能有一条写的线程对文件进行操作
2、同一时间可以有多条线程进行读的操作,但没有写的操作
我们再看下读写锁pthread_rwlock_t
- (void)viewDidLoad {
[super viewDidLoad];
pthread_rwlock_init(&_rwlock, NULL);
for(inti =0; i <5; i ++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(read) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(write) object:nil] start];
}
}
-(void)read{
pthread_rwlock_rdlock(&_rwlock);
NSLog(@"----read----");
sleep(1);
pthread_rwlock_unlock(&_rwlock);
}
-(void)write{
pthread_rwlock_wrlock(&_rwlock);
NSLog(@"----write----");
sleep(2);
pthread_rwlock_unlock(&_rwlock);
}
2019-12-02 17:19:24.244420+0800 GCD[58106:16339881] ----write----
2019-12-02 17:19:26.250256+0800 GCD[58106:16339880] ----read----
2019-12-02 17:19:26.250586+0800 GCD[58106:16339882] ----read----
2019-12-02 17:19:27.256933+0800 GCD[58106:16339883] ----write----
2019-12-02 17:19:29.258925+0800 GCD[58106:16339884] ----read----
2019-12-02 17:19:30.264998+0800 GCD[58106:16339885] ----write----
2019-12-02 17:19:32.270677+0800 GCD[58106:16339886] ----read----
2019-12-02 17:19:33.275315+0800 GCD[58106:16339887] ----write----
2019-12-02 17:19:35.279796+0800 GCD[58106:16339888] ----read----
2019-12-02 17:19:36.283005+0800 GCD[58106:16339889] ----write----
可以看出同一时间可以有多条读的线程进行操作,但同一时间只能有一条写的线程进行操作,需要注意的是如果将读写锁代码加入到队列中,则必须加入到自己创建的并发队列中,如果放到全局队列是达不到多读单写的需求!