iOS中多线程的观察
什么是多线程?
我们的应用一般是对应一个进程的,在进程中有多个线程共同存在,每个线程执行各自的任务,相互之间不会干扰,一个线程可以创建或者撤销其他的线程。让一个进程同时执行很多任务,这就减少了等待时间,提高了进程的运行效率,但是多线程会耗费资源,并且有时候也会由于一个线程死掉引起整个进程死掉,多线程也容易造成安全性问题。
多线程中的一些概念
同步:在当前线程中执⾏任务,不具备开启新线程的能⼒。 异步:在新的线程中执⾏任务,具备开启新线程的能⼒。 串行:⼀个任务执⾏完毕后,再执⾏下⼀个任务。 并发:多个任务并发(同时)执⾏。
同步和异步主要影响:能不能开启新的线程。 串行和并发主要影响:任务的执行方式。
iOS中常见的多线程方案
GCD 一般应用:简单的线程同步,多读单写 NSOperation 一般的应用:方便控制任务状态,比如 AFN/SDWebImage中的应用 NSThread 一般的应用:常驻线程
GCD
GCD在我们开发中是经常使用到的多线程方案,简单介绍一下使用方式
dispatch_queue_t main = dispatch_get_main_queue(); // 主队列
dispatch_queue_t global = dispatch_get_global_queue(0, 0); // 全局并发队列
dispatch_queue_t mySerial = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL); //自创串行队列
dispatch_queue_t myConcurrent = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT); //自创并发队列
dispatch_sync(mySerial, ^{
NSLog(@"向串行队列中添加同步任务");
});
dispatch_async(myConcurrent, ^{
NSLog(@"向并发队列中添加异步任务");
});
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("my_queue", 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, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3 ---- %@", [NSThread currentThread]);
}
});
//任务三与任务四交替执行
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4 ---- %@", [NSThread currentThread]);
}
});
NSOperation
NSOperation 是基于GCD更高一层的封装,面向对象。相对于GCD而言使用更加的简单、代码更具可读性。当然NSOperation需要和NSOperationQueue配合使用。
NSBlockOperation 和 NSInvocationOperation 是 NSOperation 的子类,介绍一下简单的使用
/// NSInvocationOperation 单独使用时,并没有开启新的线程,任务都是在当前线程中执行的。
NSInvocationOperation *invocationoperation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation) object:nil];
[invocationoperation start];
/// NSBlockOperation单独使用时,并未开启新的线程,任务的执行都是在当前线程中执行的。
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"%d--%@",i,[NSThread currentThread]);
}
}];
/// 调用了addExecutionBlock添加多个任务后,开启新线程,任务是并发执行的,blockOperationWithBlock中的任务也不在当前的线程执行。
[blockOperation addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"executionBlock1--%@", [NSThread currentThread]);
}
}];
[blockOperation addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"executionBlock2--%@", [NSThread currentThread]);
}
}];
[blockOperation start];
如果NSInvocationOperation、NSBlockOperation不能满足需求,我们可以自定义子类。定一个继承自NSOperation的类,重写它的main或者start方法。
如果只重写 main 方法,底层控制变更任务执行完成状态以及任务的退出。 如果重写 start 方法,则需要自行控制状态。
@implementation MyOperation
- (void)main {
if(!self.isCancelled){
for (int i = 0; i < 4; i++) {
NSLog(@"%d--%@",i,[NSThread currentThread]);
}
}
}
@end
MyOperation *myOperation1 = [[MyOperation alloc] init];
NSOperation 结合 NSOperationQueue 使用
MyOperation *myOperation1 = [[MyOperation alloc] init];
// myOperation1 的优先级最高
myOperation1.queuePriority = NSOperationQueuePriorityVeryHigh;
MyOperation *myOperation2 = [[MyOperation alloc] init];
MyOperation *myOperation3 = [[MyOperation alloc] init];
// 添加依赖
[myOperation3 addDependency:myOperation1];
NSOperationQueue * mainQueue = [NSOperationQueue mainQueue]; //添加到主队列中的任务都会放到主线程中执行
NSOperationQueue * customQueue =[[NSOperationQueue alloc] init]; //添加到自定义队列中的任务会放到子线程中执行
// 一个队列中能够同时执行的任务的数量
customQueue.maxConcurrentOperationCount = 1;//串行队列
//customQueue.maxConcurrentOperationCount = 4;//并发队列
//开启新线程,任务是并发执行的。如果将 customQueue 换成是 mainQueue,那么任务将会在主线程中同步执行。
[customQueue addOperation:myOperation1];
[customQueue addOperation:myOperation2];
[customQueue addOperation:myOperation3];
//[customQueue addOperations:@[myOperation1, myOperation2, myOperation3] waitUntilFinished:NO]; // 如果设置为
YES 则会阻塞
// 使用block的形式添加 operation
[customQueue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"operation1--%@", [NSThread currentThread]);
}
}];
NSOperation 用状态机来描述每一个操作的执行,包含以下状态: isReady: 返回YES表示操作已经准备好被执行, 如果返回NO则说明还有其他没有先前的相关步骤没有完成。 isExecuting: 返回YES表示操作正在执行,反之则没在执行。 isFinished : 返回YES表示操作执行成功或者被取消了,NSOperationQueue只有当它管理的所有操作的isFinished属性全标为YES以后操作才停止出列,也就是队列停止运行。 isCancelled : 代表任务取消了。
NSOperation 对象在 Finished 之后怎样从 Queue 中移除掉? 通过 KVO 通知 NSOperationQueue 对 NSOperation 进行移除。
NSThread
NSThread是pthread的封装(见gnustep ./source/NSThread.m),面向对象技术,基于thread封装,添加面向对象概念,性能较差,偏向底层,相对于GCD和NSOperation来说是较轻量级的线程开发,使用比较简单,但是需要手动管理创建线程的生命周期、同步、异步、加锁等问题
- (void)threadTest {
[[[NSThread alloc] initWithTarget:self selector:@selector(currentThread) object:nil] start];
[[[NSThread alloc] initWithBlock:^{
NSLog(@"---block---%@------", [NSThread currentThread]);
}] start];
}
- (void)currentThread {
NSLog(@"------%@------%s", [NSThread currentThread], __func__);
}
线程间的通信
iOS中线程之间的通信方式有很多,这里简单介绍一下
管道 由于iOS沙盒机制的限制,命名管道文件路径需要指定在应用内部,如临时/tmp目录,这也限制了进程间通信方式,但不妨碍其线程间通信,不过其为“半双工”通信,使用也存在许多限制,如原子性写入数据长度受限于缓存大小、消息封装及边界、进程异常消息丢失等问题。 注意;管道阻塞模式打开会阻塞等待另一方读或者写打开,若在主线程打开需要保证管道已经在其他线程读/写打开,否则可能会打开失败,且管道为半双工通信,只能读或者写打开;
套接字 常用的本机通信为“域套接字”,多用于进程间通信,但也可以用于线程间通信,一般使用较少;对于指定socket域套接字文件路径也存在沙盒机制限制,并且需要处理消息边界;
共享内存 本身线程间共享进程的内存空间,因此不需要专门使用进程间“共享内存”方式,可直接使用进程空间(如全局变量)来传递数据,并通过线程间同步方式来控制数据同步,如互斥锁、信号量、同步锁、读写锁等;
共享存储 如通过临时文件、plist文件、持久化存储(如NSUserDefault)等共享存储的方式也可以传递数据,通过线程锁来解决数据一致性问题。
Mach Port
mach消息是Mach IPC的核心基础,消息可以在在两个port端口(或称为端点)之间传递,端口可以是单主机也可以是远程机器,并解决了消息参数串行化、对齐、填充和字节顺序问题。
对于底层的machapi使用较少,并且需要深刻理解内核mach对象框架,不过Core Foundation和Foundation为Mach Port提供了高级API,在内核基础上封装的 CFMachPort / NSMachPort
作为 runloop 源结合 runloop 实现异步通信;其中苹果官方给出的 NSNotificaiton 通知转发到指定线程处理,就是利用 Mach Port 加入到转发线程runloop中来处理。
mach相关的头文件定义在<mach/message.h>
相关的头文件中,CFMachPort定义在<CFMachPort.h>
中
其优势在于结合runloop来实现异步通信,并且更接近于内核态对象更为高效;
NSObject对象提供的api
//主线程
- (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;
//指定线程
- (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;
GCD 主要是向主队列、全局队列或者自定义队列添加任务,利用block会传递上下文来携带通信参数,并通过dispatch_sync、dispatch_group dispatch_barrier等方式同步任务。
NSOperationQueue 主要是向操作队列添加任务,通过添加依赖关系来控制同步,若需要传递数据可通过block来截获上下文数据,但注意对象的生命周期避免循环引用问题。
看几个问题
第一个问题:以下的打印结果
- (void)testDelay {
NSLog(@"2");
}
- (void)test {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(testDelay) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
只会打印 1 和 3,performSelector:withObject:afterDelay:
底层使用了定时器,子线程默认没有开启runloop,需要开启runloop才能执行
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
第二个问题:以下的打印结果
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"全局队列添加同步任务");
});
结果是直接 Crash,因为向当前串行队列中添加同步任务,引起了死锁。
多线程的隐患
1块资源可能会被多个线程共享,也就是多个线程可能会访问同⼀块资源,⽐如多个线程访问同⼀个对象、同⼀个变量、同⼀个⽂件,当多个线程访问同⼀块资源时,很容易引发数据错乱和数据安全问题,举个例子
- (void)saleTickets {
self.ticketCount = 30;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for(int i = 0; i < 5; i ++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for(int i = 0; i < 5; i ++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for(int i = 0; i < 5; i ++) {
[self saleTicket];
}
});
}
- (void)saleTicket {
int count = self.ticketCount;
sleep(0.2);
count--;
self.ticketCount = count;
NSLog(@"当前线程是--- %@, 还剩 %d 张", [NSThread currentThread], count);
}
同时多个线程访问同一份数据,导致我们访问到的数据有可能是错误的
这并不是我们想要的结果,这里就引入了一个概念:线程同步技术,常见的线程同步技术是加锁,在修改的时候添加上锁,暂时不让别的线程去操作就可以了
- (void)saleTicket {
os_unfair_lock_lock(&_lock);
int count = self.ticketCount;
sleep(0.2);
count--;
self.ticketCount = count;
os_unfair_lock_unlock(&_lock);
NSLog(@"当前线程是--- %@, 还剩 %d 张", [NSThread currentThread], count);
}
iOS中的锁
常见的锁有一下几种,加锁解锁一定要成对出现,不然就出现一直等待最终死锁
OSSpinLock
“⾃旋锁”,等待锁的线程会处于忙等(busy-wait)状态,⼀直占⽤着CPU资源⽬前已经不再安全,可能会出现优先级反转问题,如果等待锁的线程优先级较⾼,它会⼀直占⽤着CPU资源,优先级低的线程就⽆法释放锁,需要导⼊头⽂件 #import <libkern/OSAtomic.h>
OSSpinLock lock = OS_SPINLOCK_INIT; //初始化
bool success = OSSpinLockTry(&lock); //尝试加锁
OSSpinLockLock(&lock); //加锁
OSSpinLockUnlock(&lock); //解锁
os_unfair_lock
os_unfair_lock⽤于取代不安全的OSSpinLock ,从iOS10开始才⽀持,从底层调⽤看,等待os_unfair_lock锁的线程会处于休眠状态,并⾮忙等,需要导⼊头⽂件#import <os/lock.h>
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_trylock(&lock);
os_unfair_lock_lock(&lock);
os_unfair_lock_unlock(&lock);
pthread_mutex
mutex叫做“互斥锁”,等待锁的线程会处于休眠状态,需要导⼊头⽂件#import <pthread.h>
//初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); //普通锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //递归锁
//初始化锁
pthread_mutex_init(lock, &attr);
//销毁属性
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
条件
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_cond_init(&cond, NULL);
pthread_mutex_lock(&lock);
pthread_cond_signal(&cond); //发送信号唤醒
//pthread_cond_broadcast(&cond); //发广播,适用于多条线程在等待
pthread_mutex_unlock(&lock);
pthread_mutex_lock(&lock);
pthread_cond_wait(&cond, &lock); //先执行到这里的话先放开锁,睡眠等待条件达成,收到条件达成信号就唤醒,唤醒之后重新加上锁往下执行。
pthread_mutex_unlock(&lock);
pthread_cond_destroy(&cond);
NSLock NSLock是对mutex普通锁的封装
NSRecursiveLock NSRecursiveLock是对pthread_mutex递归锁的封装,递归锁是可以递归加锁
@interface NSRecursiveLockDemo()
@property(nonatomic, strong) NSRecursiveLock *lock;
@end
@implementation NSRecursiveLockDemo
- (instancetype)init {
if(self = [super init]) {
self.lock = [[NSRecursiveLock alloc] init];
}
return self;
}
- (void)otherTest {
[self.lock lock];
NSLog(@"-----------> OtherTest");
[self otherTest2];
[self.lock unlock];
}
- (void)otherTest2 {
[self.lock lock];
NSLog(@"==========> OtherTest2");
[self.lock unlock];
}
@end
NSCondition NSCondition是对mutex和cond的封装
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
[condition signal]; //发送信号唤醒
[condition unlock];
[condition wait]; //先执行到这里的话先放开锁,睡眠等待条件达成,收到条件达成信号就唤醒,唤醒之后重新加上锁往下执行。
NSConditionLock NSConditionLock 是对 NSCondition的进一步封装,可以设置具体的条件值
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1]; // 初始化的时候添加了一个条件
- (void)__one {
[self.conditionLock lockWhenCondition:1]; //加锁成功继续执行,如果加锁不成功,就一直等这个条件
NSLog(@"======> __one");
[self.conditionLock unlockWithCondition:2];
}
- (void)__two {
[self.conditionLock lockWhenCondition:2];
NSLog(@"======> __two");
[self.conditionLock unlock];
}
SerialQueue 本质是不让多条线程在同一时间占用同一份资源
- (instancetype)init {
if(self = [super init]) {
self.serialQueue = dispatch_queue_create("myquque", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)saleTicket {
dispatch_sync(self.serialQueue, ^{
[super saleTicket];
});
}
Semaphore 信号量,有初始值,可以⽤来控制线程并发访问的最⼤数量,信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
- (instancetype)init {
if(self = [super init]) {
self.semaphore = dispatch_semaphore_create(5); // 最大并发量是5
}
return self;
}
- (void)otherTest {
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(otherTest2) object:nil] start];
}
}
- (void)otherTest2 {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"--- 当前线程为 ----> %@", [NSThread currentThread]);
dispatch_semaphore_signal(self.semaphore);
}
@synchronized 是对mutex递归锁的封装,内部会⽣成obj对应的递归锁,然后进⾏加锁、解锁操作,不建议使用,因为用了hash表的结构,传进去的参数作为key,性能较差
- (void)saleTicket {
static NSObject *lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSObject alloc] init];
});
@synchronized (lock) {
[super saleTicket];
}
}
性能从⾼到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
自旋锁、互斥锁比较
什么情况使⽤⾃旋锁⽐较划算? 预计线程等待锁的时间很短,加锁的代码(临界区)经常被调⽤,但竞争情况很少发⽣,CPU资源不紧张,多核处理器 什么情况使⽤互斥锁⽐较划算? 预计线程等待锁的时间较⻓,单核处理器,临界区有IO操作,临界区代码复杂或者循环量⼤,临界区竞争⾮常激烈
atomic
原子性,属性的修饰字符,⽤于保证属性setter、getter的原⼦性操作,相当于在getter和setter内部加了线程同步的锁,使用的是自旋锁,但是并不能保证使⽤属性的过程是线程安全的,比如一个可变数组属性,用 atomic 修饰,向数组添加删除元素的时候并不是线程安全的,特别是比较损耗性能,所以移动端几乎不用。
iOS中读写安全的方案
怎么保证读写安全?保证下面几条,也就是典型的“多读单写”,经常⽤于⽂件等数据的读写操作
同⼀时间,只能有1个线程进⾏写的操作; 同⼀时间,允许有多个线程进⾏读的操作; 同⼀时间,不允许既有写的操作,⼜有读的操作;
iOS中的实现⽅案有 pthread_rwlock 读写锁,等待锁的线程会进⼊休眠
- (void)test1 {
pthread_rwlock_init(&_rwlock, NULL);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); // 注意队列的创建
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
[self read];
});
dispatch_async(queue, ^{
[self write];
});
}
}
- (void)read {
pthread_rwlock_rdlock(&_rwlock);
NSLog(@"------> %s", __func__);
pthread_rwlock_unlock(&_rwlock);
}
- (void)write {
pthread_rwlock_wrlock(&_rwlock);
NSLog(@"------> %s", __func__);
pthread_rwlock_unlock(&_rwlock);
}
- (void)dealloc {
pthread_rwlock_destroy(&_rwlock);
}
dispatch_barrier_async 异步栅栏调⽤,这个函数传⼊的并发队列必须是⾃⼰通过dispatch_queue_cretate创建的,如果传⼊的是⼀个串⾏或是⼀个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
- (void)test2 {
self.myQueue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);;
for (int i = 0; i < 10; i++) {
[self read1];
[self write1];
}
}
- (void)read1 {
/// 读可以同时
dispatch_async(self.myQueue, ^{
sleep(1);
NSLog(@"------> %s", __func__);
});
}
- (void)write1 {
// 写只能一个一个来
dispatch_barrier_async(self.myQueue, ^{
sleep(1);
NSLog(@"------> %s", __func__);
});
}
把写操作放在dispatch_barrier_async中可以把其他的操作隔离开