iOS底层之多线程

1,124 阅读8分钟

iOS 常见的多线程方案

后三种方案都基于第一种方案的封装

GCD 的用法

重要概念区别

1,确定是同步(sync)还是异步(async) 同步有能力开启新的线程,异步不能够开启新线程 2,确定是并发队列还是串行队列 队列一共分两种串行(主队列是串行的一种)和并行。 常见的用法, 开辟一个同步的并发线程。 回到主线程刷新UI

GCD 造成的死循环(死循环的原因在于sync同步要求立即在当前线程执行,而串行队列的特点是先进先出,并发队列一般不造成死锁)

简单死循环

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

}

1,任务2是要添加的主线程执行的。dispatch_sync 执行完本任务才能往下走。 2,队列的特点:FIFO first in first out 3,任务2是放在队列里面的,需要等队列里的任务执行完。主队列已经有任务了,就是任务1,3 。 4,死锁原因,任务2在等任务3,因为队列。任务3在等任务2,因为dispathc_sync 。

复杂死循环

    dispatch_async(queue, ^{//blcok0
        NSLog(@"执行任务2");
        dispatch_sync(queue, ^{//blcok1
            NSLog(@"执行任务3");

        });
        NSLog(@"执行任务4");

    });


1,任务3要求立即执行,但是队列中还有任务4没有执行。 2,blcok0 先进入队列还没执行完,blcok1就要求立即执行。 3,任务3要等任务4,任务4要等任务3.

GCD 中的队列组(多个队列顺序执行)

    // 创建队列组
    
    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]);
                
            }

        });
        
    });

GUstep

GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍

源码地址:www.gnustep.org/resources/d…

虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值

多线程的安全隐患

1,单线程为什么么没有安全隐患,因为主线程是串行执行的,数据时安全的。 2,多线程,是异步并发处理的,有可能存在同时操作一组数据的情况。(特别是有些异步操作时耗时的,拿到数据后要操作一段时间,此时别的线程拿到的又是老数据。) 例子:1000张票同时在2个窗口买,2个窗口有可能卖同一张票。

如何解决线程安全隐患(使用线程同步技术)常见的技术:加锁

OSSpinLock (已不安全)

1,创建一个全局的锁

2,对异步操作的代码加锁

3,OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源

目前已经不再安全,可能会出现优先级反转问题

如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。低优先级的任务先进入锁住线程,高优先级的任务进来后,会让线程进入忙等状态,阻塞了优先级低的任务的执行。 需要导入头文件#import <libkern/OSAtomic.h>

os_unfair_lock

1,引入头文件 #import <os/lock.h> 2,加锁,解锁,销毁。

pthread_mutex_t (互斥锁”,等待锁的线程会处于休眠状态)

1,引入头文件#import <pthread.h> 2,加锁,解锁,销毁。

pthread_mutex_t 递归锁

递归锁的概念:允许同一个线程对一把锁进行重复加锁

pthread_mutex – 条件

让线程处于休眠状态,激活一个等待的线程,激活所有等待该条件的线程

NSLock、NSRecursiveLock (更加符合OC习惯)

NSLock是对mutex普通锁的封装 NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致

NSCondition (符合OC习惯,对mutex和条件的封装)

wait 唤醒的条件。

1,signal 条件触发,

2,wait休眠的锁已经被解开。

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

dispatch_semaphore()

1,semaphore叫做”信号量” 2,信号量的初始值,可以用来控制线程并发访问的最大数量 3,信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步(相当于加锁,和加锁的本质是一样的)

dispatch_queue

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

@synchronized (最简单的方法,但是性能并不高)

1,@synchronized是对mutex递归锁的封装

2,源码查看:objc4中的objc-sync.mm文件

3,@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

@synchronized(obj){//任务}

锁的 性能从高到低排序

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

自旋锁、互斥锁比较

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

预计线程等待锁的时间很短 加锁的代码(临界区)经常被调用,但竞争情况很少发生 CPU资源不紧张 多核处理器

  1. 什么情况使用互斥锁比较划算?

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

多线程读写安全

atomic

可以参考源码objc4的objc-accessors.mm

1,atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁

2,它并不能保证使用属性的过程是线程安全的

比如创建一个数组,数组的set和get方法是线程安全的,但是往数组中添加和移除元素时线程不安全的。

3,在手机上使用是极其消耗性能的.

iOS 中的读写安全方案

多读单写

同一时间,只能有1个线程进行写的操作

同一时间,允许有多个线程进行读的操作

同一时间,不允许既有写的操作,又有读的操作

经常用于文件等数据的读写操作,iOS中的实现方案有

pthread_rwlock:读写锁 等待锁的线程会进入休眠

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
//    [self barrier];
    
    // 初始化锁
    pthread_rwlock_init(&_lock, 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(&_lock);
    
    sleep(1);
    NSLog(@"%s", __func__);
    
    pthread_rwlock_unlock(&_lock);
}

- (void)write
{
    pthread_rwlock_wrlock(&_lock);
    
    sleep(1);
    NSLog(@"%s", __func__);
    
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc
{
    pthread_rwlock_destroy(&_lock);
}

dispatch_barrier_async:异步栅栏调用 1,这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的

2,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

(读和写在同一个传建的队列里,读取是异步操作,写入加了栅栏。此时读取的时候是加了锁的)

面试题

 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"1");
        // 这句代码的本质是往Runloop中添加定时器
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
        

打印结果是:1、3 原因 performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器 子线程默认没有启动Runloop

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
    }];
    [thread start];
    
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

程序会崩溃,因为thread 执行完线程就会退出。已经没有执行任务的能力了(thread并没有被立即销毁。)

解决方法,在线程中添加runloop,

 NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
        
        [[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];

小结

GCD的常见用法

各种队列的执行效果

2个重要概念,并发和串行,异步和同步。

1,只有异步的并发队列和手动串行队列才能开启新线程。

2,只有异步的并发队列,才能并发的执行任务。

gcd 造成的死锁的原因

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

  1. sync 要求在本任务立即执行。
  2. 串行队列要求先执行sync任务前的任务。

串行队列的特点和同步执行的特点导致的。

performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器

子线程默认没有启动Runloop。

CGD的队列组

多线程的安全隐患

多个线程同时访问一组数据。

解决多线程安全的方法(加锁)

自旋锁和互斥锁

底层锁和高级锁,及OC锁

iOS中的读写安全

多读单写(写和读也不能同时)

pthread_rwlock:读写锁

dispatch_barrier_async:异步栅栏调用