底层七-多线程

410 阅读10分钟
- (void)viewDidLoad {

    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    

    dispatch_async(queue, ^{

        sleep(5);

        NSLog(@"1");

    });

    

    dispatch_async(queue, ^{

        sleep(4);

        NSLog(@"4");

    });

    

    dispatch_sync(queue, ^{

        sleep(3);

        NSLog(@"2");

    });

    

    sleep(1);

    NSLog(@"3");

}
  • 在主线程执行, 输出顺序是 1 4 2 3 ,因为在输出2时,是dispatch_sync ,需要立即执行,而所在队列是串行的,将优先执行排在队列前面的任务。

  • 如果将队列换成并发队列,输出顺序为 2 3 4 1 ,因为并发队列任务可以随意出队,这时候sync任务可以提前出队,同时等进程任务执行完,再执行异步任务,异步任务执行也是并发执行,执行时间短的先结束

GNU源码 : GNUStep

iOS中的常见多线程方案

GCD

各种队列的执行效果

队列组的使用

思考:如何用gcd实现以下功能

  • 异步并发执行任务1、任务2
  • 等任务1、任务2都执行完毕后,再回到主线程执行任务3

队列组的可用方案

- (void)groupSync
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        sleep(5);
        NSLog(@"任务一完成");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        sleep(8);
        NSLog(@"任务二完成");
        dispatch_group_leave(group);
    });
    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"任务完成");
    });
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 创建队列组
    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, queue, ^{
        dispatch_async(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(@"任务3-%@", [NSThread currentThread]);
//        }
//    });
    
//    *************** *************** ***************
    
//    dispatch_group_notify(group, queue, ^{
//        for (int i = 0; i < 5; i++) {
//            NSLog(@"任务3-%@", [NSThread currentThread]);
//        }
//    });
//
//    dispatch_group_notify(group, queue, ^{
//        for (int i = 0; i < 5; i++) {
//            NSLog(@"任务4-%@", [NSThread currentThread]);
//        }
//    });
}

死锁

主队列执行主线程任务-- 死锁

- (void)interview
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
    NSLog(@"执行任务1");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"执行任务2");
    });
    
    NSLog(@"执行任务3");
    
    // dispatch_sync立马在当前线程同步执行任务
}

死锁原因

截屏2021-04-10 上午10.23.32.png

  • 主线程正在执行的任务,需要等sync()任务执行完才能执行,sync()任务需要等主线程空闲才能执行

- (void)interview03
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
    NSLog(@"执行任务1");

    //如果创建的是并发队列,不会死锁
	//dispatch_queue_t queue = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);    
	
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2,%@",[NSThread currentThread]);
        
        dispatch_sync(queue, ^{ // 1
            NSLog(@"执行任务3,%@",[NSThread currentThread]);
        });
    
        NSLog(@"执行任务4,%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5");
}

死锁原因

屏幕快照 2018-09-30 上午10.05.42.png

产生死锁条件: 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

多线程题解

one

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:self afterDelay:.0];
        
        // 如果跑起来子线程的runLoop,可以打印
        
        // runLoop在处理定时器是在唤醒的时候, 在runLoop睡觉之前之前肯定要先去处理点击事件,所以打印顺序会改变( 1 3 Test )
        
        // [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
        
        NSLog(@"2");
    });
}

- (void)test {
    NSLog(@"%s",__func__);
}

/*
打印结果是 :
2018-10-17 13:19:06.213307+0800 多线程[4284:708545] 1
2018-10-17 13:19:06.213499+0800 多线程[4284:708545] 2
*/

原因:

  • performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器
  • 子线程默认没有启动Runloop

参考GNUStep提供的源码(路径 Source -> Foundation -> NSRunLoop.m )

- (void) performSelector: (SEL)aSelector
	      withObject: (id)argument
	      afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop		*loop = [NSRunLoop currentRunLoop];
  // GSTimedPerformer 封装了NSTimer
  GSTimedPerformer	*item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
					     target: self
					   argument: argument
					      delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  // 向runLoop中添加timer
  // 虽然添加了timer,但是runLoop需要调用run,才能执行
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

two

- (void)test
{
    NSLog(@"2");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
// 向runLoop中添加任务可以进行保活操作,注意runLoop中必须要port、source、timer、observer才能进行run操作        
//        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];
    
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

// 结果 : 打印 1 之后崩溃,

原因 test函数也要在线程thread中执行,但是threadNSLog完之后,就会进行释放操作,子线程没有runLoop无法保活,子线程销毁,再次调用造成崩溃

线程组的使用

多线程安全隐患

  1. 资源共享
    • 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    • 比如多个线程访问同一个对象、同一个变量、同一个文件
  2. 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

iOS中的线程同步方案

OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized

OSSpinLock

  1. OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源(自旋锁)
  2. 目前已经不再安全,可能会出现优先级反转问题
    • 可能thred1级别高,但是进入的比thred2晚,一直忙等(while 1),占用了CPU资源,造成死锁
  3. 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁.
  4. 需要导入头文件#import <libkern/OSAtomic.h> 屏幕快照 2018-11-03 23.28.12.png

os_unfair_lock

  1. os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
  2. 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等 (互斥锁
  3. 需要导入头文件#import <os/lock.h>

屏幕快照 2018-11-03 23.30.35.png

pthread_mutex (pthread 一般都是跨平台的)

  1. mutex叫做 互斥锁,等待锁的线程会处于休眠状态
  2. 需要导入头文件#import <pthread.h>

屏幕快照 2018-11-03 23.31.37.png

pthread_mutex – 递归锁

下面代码会造成死锁,因为otherTest2的加锁,会休眠等待otherTest的锁解锁,但是otherTest要等otherTest2结束才会解锁,可以换一把锁,otherTest2使用mutex2

- (void)otherTest
{
    pthread_mutex_lock(&_mutex);
    
    [self otherTest2];
    
    pthread_mutex_unlock(&_mutex);
}

- (void)otherTest2
{
    pthread_mutex_lock(&_mutex);

    NSLog(@"%s", __func__);

    pthread_mutex_unlock(&_mutex);
}

但是递归的情况,只能使用递归锁

递归锁: 允许同一个线程对一把锁重复加锁(不同线程不行)


/**
 线程1:otherTest(+-)
        otherTest(+-)
         otherTest(+-)
 
 线程2:otherTest(等待)
 */


- (void)otherTest
{
    pthread_mutex_lock(&_mutex);
    
    [self otherTest];
    
    pthread_mutex_unlock(&_mutex);
}

屏幕快照 2018-11-03 23.32.27.png

pthread_mutex – 条件

生产者、消费者模式

// 模拟删除、添加在不同线程
- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 生产者-消费者模式

// 线程1
// 删除数组中的元素
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) { // 有数据才删除
        NSLog(@"唤醒");
        // 带有条件(_cond)的等待,同时放开锁mutex,等待信号过来,
        // 这时候等待添加数据解锁,又会再次加锁
        // 所以这里是 ----- 解锁 ------加锁
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    pthread_mutex_unlock(&_mutex);
    
    NSLog(@"删除结束");
}

// 线程2
// 往数组中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信号 (会唤醒刚刚带有条件的等待pthread_cond_wait)
    pthread_cond_signal(&_cond);
    // 广播 (唤醒所有的等待线程)
//    pthread_cond_broadcast(&_cond);
    sleep(3);
    pthread_mutex_unlock(&_mutex);
    
    NSLog(@"添加结束");
}

打印

2021-04-13 17:12:53.685170+0800 Interview04-线程同步[6627:375249] __remove - begin
2021-04-13 17:12:53.685282+0800 Interview04-线程同步[6627:375249] 唤醒
2021-04-13 17:12:54.688471+0800 Interview04-线程同步[6627:375250] 添加了元素
2021-04-13 17:12:57.694049+0800 Interview04-线程同步[6627:375250] 添加结束
2021-04-13 17:12:57.694099+0800 Interview04-线程同步[6627:375249] 删除了元素
2021-04-13 17:12:57.694416+0800 Interview04-线程同步[6627:375249] 删除结束

屏幕快照 2018-11-03 23.33.13.png

NSLock、NSRecursiveLock

NSLock是对mutex普通锁的封装

屏幕快照 2018-11-03 23.33.55.png

NSRecursiveLock也是对mutex递归锁的封装,APINSLock基本一致

NSCondition

NSCondition是对pthread_mutexcond(mutex条件)的封装

生产者-消费者模式

屏幕快照 2018-11-03 23.35.09.png

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

NSConditionLock.png

可以根据条件值,使得子线程依赖执行

- (instancetype)init
{
    if (self = [super init]) {
        // 条件值是1
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one
{
    // 不管条件值,直接加锁
    [self.conditionLock lock];
    
    NSLog(@"__one");
    sleep(1);
    
    // 设置条件值为2,并解锁
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
    // 条件值是2才能加锁
    [self.conditionLock lockWhenCondition:2];
    
    NSLog(@"__two");
    sleep(1);
    
    // 设置条件值为3,并解锁
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three
{
    // 条件值是3才能加锁
    [self.conditionLock lockWhenCondition:3];
    
    NSLog(@"__three");
    
    // 不管条件值,直接解锁
    [self.conditionLock unlock];
}

// 打印
2021-04-14 09:43:59.040652+0800 -线程同步[1667:43984] __one
2021-04-14 09:44:00.045292+0800 -线程同步[1667:43985] __two
2021-04-14 09:44:01.048857+0800 -线程同步[1667:43986] __three

dispatch_semaphore

  1. semaphore叫做”信号量”
  2. 信号量的初始值,可以用来控制线程并发访问的最大数量
  3. 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_semaphore.png

宏定义信号量


#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);



- (void)test1
{
    SemaphoreBegin;
    
    // .....
    
    SemaphoreEnd;
}
控制最大线程数
// 线程10、7、6、9、8

self.semaphore = dispatch_semaphore_create(5);

- (void)test
{
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

dispatch_queue

直接使用GCD的串行队列,也是可以实现线程同步的

dispatch_queue.png

@synchronized

并没有代码提示,性能较差

  1. @synchronized是对mutex递归锁的封装
  2. 源码查看:objc4中的objc-sync.mm文件
  3. @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

synchronized.png

iOS线程同步方案性能比较

iOS线程同步方案性能比较.png

自旋锁、互斥锁比较

  1. 什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
  2. 什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作(比较占用CPU资源)
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

atomic

  • atomic用于保证属性settergetter的原子性操作,相当于在gettersetter内部加了线程同步的锁
  • 可以参考源码objc4的objc-accessors.mm
// set 方法
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) { 
        // [self.array mutableCopy]
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

// get 方法
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

spinlock_t的源码,可以看出atomic内部是os_unfair_lock

using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
}
  • 它并不能保证使用属性的过程是线程安全的 (比如创建数组添加元素)

创建一个类 Person,有一个属性 NSMutableArray *array, 在ViewController中添加元素 [p.array addobject: @"xxx"],相当于 [[p array] addObject @"xxx"] , 但是只有前半部分[p array]是线程安全的,后半部分不是

iOS中的读写安全方案

思考如何实现以下场景

同一时间,只能有1个线程进行写的操作 同一时间,允许有多个线程进行读的操作 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有

pthread_rwlock:读写锁

dispatch_barrier_async:异步栅栏调用

pthread_rwlock

pthread_rwlock.png

dispatch_barrier_async

  1. 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
  2. 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

dispatch_barrier_async1111.png

实现效果类似:

dispatch_barrier_async2222.png