iOS中多线程的观察

标签: 编程学习 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中可以把其他的操作隔离开