问题: 简述一下多线程的相关内容.


回答目录:

  • 线程与进程.
  • 多线程的方案.
  • 同步、异步、串行、并发.
  • 死锁.
  • GCD队列组.
  • iOS相关的锁.
  • 实现线程同步的方案.
  • atomic.
  • IO 操作的读写安全
  • GCD问题实操.


进程与线程:

  1. 本质: 进程是一个静态的容器,可以理解为正在执行的应用程序实例,它里面容纳很多线程; 线程是一系列方法的 线性 执行路径(CPU维度).

  2. 资源共享与同步: 进程拥有独立的资源空间(资源分配基本单位),共享起来比较复杂,常使用IPC进行同步,同步简单; 线程之间共享所属的进程空间,共享起来比较简单,但是同步较为麻烦,常使用加锁的方式进行同步.

  3. 崩溃: 进程崩溃不会影响到其他进程; 一个线程的崩溃会导致整个进程的崩溃.



iOS的多线程主要有一下几种方案.

技术方案简介语言线程声明周期使用频率
pthread一套通用的多线程API; 适用于Unix/Linux/Windows; 跨平台\可移植; 使用难度较大C程序员管理几乎不用
NSThead使用更加面向对象; 简单易用,可以直接操作线程对象OC程序员管理偶尔使用
GCD旨在替换NSThread等多线程技术; 可以充分利用设备的多核C自我管理经常使用
NSOperation底层基于GCD; 比GDD多了一些更简单使用的功能; 使用更加面向对象0C程序员管理经常使用


知识补充点: 同步、异步、串行、并发

同步、异步主要影响: 能不能开启新的线程

  • 同步: 在当前线程执行任务,不具备开启新的线程的能力.
  • 异步: 在新的线程执行任务,具备开启新线程的能力.

串行、并发主要影响: 任务的执行方式

  • 串行: 一个任务执行完成之后,再去执行另外一个任务.
  • 并发: 多个任务同时执行.
并发队列手动创建的串行队列主队列
同步dispatch_sync不开启新线程 串行执行不开启新线程 串行执行不开启新线程 串行执行
异步dispatch_async开启新线程 并发执行开启新线程 串行执行不开启新线程 串行执行


** 问题: 什么叫死锁? **

线程的各个任务相关等待则会导致死锁问题.

使用 sync 往当前串行队列中添加任务就会造成死锁问题.

示例:

- (void)viewDidLoad {
    NSLog(@"Log1");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^(){
        NSLog(@"Log2");
    });

    NSLog(@"Log3");
}

分析如下所示.

  • viewDidLoad 整个方法作为任务添加到主队列中.并且在主线程中执行.

  • 在打印完成Log1之后.立即添加任务(打印 Log2)到主队列中,并且要求立即执行(sync的缘故).

  • 由于是串行队列,当前队列中还有任务(ViewDidLoad),故 任务2 需要等待 任务1 完成.

  • 想要完成ViewDidLoad任务(任务1),那么立刻打印Log2就必须要执行,也就是任务2必须要完成.

  • 最终两个任务相互等待,造成死锁.


GCD_Group 队列组

通过下述的函数可以实现执行完一定量的任务之后,再执行别的任务.

// 创建任务组
dispatch_group_t group = dispatch_group_create();
// (在group完成之后再去执行别的任务)
// 激活任务组
dipatch_group_notify(group, queue, ^(){
    // 执行某些任务
});


锁相关内容:

常应用于 线程同步 问题的解决.



OSSpinLock

OSSpinLock叫做自旋锁,等待锁的线程会处于忙等的状态,一直占用CPU资源.

目前现在已经不安全,可能出现优先级反转问题.如果等待锁的线程优先级比较高,它会一直占用CPU资源,导致优先级较低的线程无法释放锁.

OSSpinLock需要导入 <libkern/OSAtomic.h>

OSSpinLock 主要的函数有:

  • SpinLock初始化

     OSSpinLock lock = OS_SPINLOCK_INIT;
    
  • SpinLock加锁

    OSSpinLockLock(&lock);
    
  • SpinLock解锁

    OSSpinLockUnlock(&lock);
    


os_unfair_lock

os_unfair_lock 用来取代不安全的OSSpinLock,在iOS10之后支持.

os_unfair_lock 不再是自旋锁类型,加锁等待的线程会处于休眠状态,并非忙等.

os_unfair_lock 需要导入 <os/lock.h>

os_unfair_lock 相关函数有:

  • 初始化锁

    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

pthread_mutex 是互斥锁,等待锁的线程会进入休眠状态

需要导入头文件 <pthread.h>

pthread_mutex 相关函数:

  • 初始化锁

    // 静态初始化
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    
    // 初始化方式2
    pthread_mutex_t lock;
    
    pthread_mutexattr_t attr;
    
    pthread_mutex_init(&lock, &attr);
    
  • 设置锁的类型

    #define PTHREAD_MUTEX_NORMAL		0 默认锁类型
    #define PTHREAD_MUTEX_ERRORCHECK	1 可捕获错误的锁类型
    #define PTHREAD_MUTEX_RECURSIVE		2 递归锁类型
    #define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL
    
    pthread_mutex_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    
  • 销毁属性

    pthread_mutex_destroy(&attr);
    
  • 加锁

    pthread_mutex_lock(&lock);
    
  • 解锁

    pthread_mutex_unlock(&lock);
    
  • 销毁锁

    pthread_mutex_destroy(&lock);
    

pthread_mutex 条件

  • 条件初始化

    pthread_cond_t cond;
    
    pthread_cond_init(&cond, NULL);
    
  • 条件阻塞

    • 进入休眠,放开锁
    • 得到通知之后,退出休眠,并且会再次加锁
    pthread_cond_wait(&_cond, &lock);
    
  • 条件信号

  • 通知一条wait所在线程.

    pthread_cond_signal(&_cond);
    
  • 条件通知

  • 通知所有的线程中所有的wait阻塞

    pthread_cond_broadcast(&_cond);
    


递归锁概念:

允许同一个线程对同一把锁进行多次加锁.



NSLockNSRecursiveLock

NSLock 就是对普通的pthread_mutex的封装,所以是互斥锁.

NSRecursiveLock 是对递归类型的 phread_mutex的封装.

NSLockNSRecursiveLock 相关方法:

  • 锁的初始化

    NSLock *lock = [[NSLock alloc] init];
    
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
  • 加锁

    [lock lock];
    
  • 解锁

    [lock unlock];
    


NSCondition

NSCondition 是对 pthread_mutex 和 pthread_cond 的 封装.

NSCondition 相关方法:

  • 初始化

    NSCondition *cond = [[NSCondition alloc] init];
    
  • 加锁

    [cond lock];
    
  • 解锁

    [cond unlock];
    
  • 等待

    • 等待过程, 会进行解锁
    • 重新进入的时候,会再次加锁.
    [cond wait];
    
  • 发送信号

    [cond signal];
    
  • 发送通知

    [cond broadcast];
    


NSConditionLock

NSConditionLock 是对 NSCondition 进一步的封装. 新增了内部条件值 condition.

作用: 可用于顺序调用某些事件.

  • 根据 condition 来进行加锁.

    - (void)lockWhenCondition:(NSInteger)condition;
    
  • 根据 condition 来进行解锁.

    - (void)unlockWhenCondition:(NSInteger)condition;
    


disptch_semaphore

disptch_semaphore 是 GCD API 中的信号量

信号的初始值,可以用来控制线程并发访问 的最大数量.

相关函数如下所示.

  • 初始化

    // 设置最大并发数为 3
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
    
  • 设置等待

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
  • 发送信号

    dispatch_semaphore_signal(semaphore);
    


@sychronized

@sychronized 是一个递归锁, 是对pthread_mutex的一个封装. 但是性能比较差,官方不推荐使用.

使用方式

@sychronized(...) {
    // do some thing
}


线程同步的方案

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • NSLock, NSrecursiveLock
  • NSCondition, NSConditionLock
  • 串行队列 Serial -> DESPATCH_QUEUE_SERAIL
  • 信号量 disptch_semaphore
  • @sychronized


atomic

atomic 保证了属性的 set 、 get 方法的原子性操作,相当于在set和get方法内部增加了线程同步的锁.

但 atomic并不能保证 属性的使用是安全的.

iOS不常用的原因:

  • 比较损耗性能.


IO 操作的读写安全

场景需求:

  • 同一时间只能有一个线程进行写的操作.
  • 同一时间可以有多个线程进行读的操作.
  • 同一时间不能既有写的操作,又有读的操作.

方案一: pthread_rwlock

pthread_rwlock 等待锁的线程会进入休眠.

相关函数:

  • 初始化锁.

    pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock, NULL);
    
  • 读加锁.

    pthread_rwlock_rdlock(&rwlock);
    
  • 读-尝试加锁.

    pthread_rwlock_tryrdlock(&rwlock);
    
  • 写加锁.

    pthread_rwlock_wrlock(&rwlock);
    
  • 写-尝试加锁.

    pthread_rwlock_trywrlock(&rwlock);
    
  • 解锁.

    pthread_rwlock_unlock(&rwlock);
    
  • 销毁锁.

    pthread_rwlock_destory(&rwlock);
    

方案二: dispatch_barrier_async 栅栏函数

栅栏函数实现读写锁的话,需要自己通过 dispatch_queue_create 创建一个并发队列.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    NSLog(@"0");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1");
    });
    NSLog(@"2");
}

打印 0,然后崩溃.原因是死锁造成.

主队列是一个串行队列, 往主队列中添加同步任务,导致任务互等,最终导致死锁.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i < 20; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d - %@", i, [NSThread currentThread]);
        });
    }
}

会顺序打印,并且当前在主线程,那么就会打印出主线程.

串行队列添加同步任务,先进先出. 串行执行先进先出,所以会依次打印 0 - 19 数字.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    NSLog(@"0");
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"1 - %@", [NSThread currentThread]);
    });
    NSLog(@"2 - %@", [NSThread currentThread]);
}

打印顺序为 0 → 2 → 1, 打印线程为同一线程.

串行队列添加异步任务,不会创建新的线程.所以当前是什么线程,那么block中代码执行所在的线程就是什么线程.

添加异步任务,虽然是串行队列,但是需要等待打印完成Log2才能打印Log1.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    for (int i = 0; i < 20; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d - %@", i, [NSThread currentThread]);
        });
    }
    NSLog(@"End");
}

先打印End,再随机打印 0 - 19; 打印的线程也不是同一个,是 20 个不同的线程.

添加异步任务,所以会执行完当前的任务,也就是打印End,再随机打印0 - 19.

全局队列(并发队列) 与 添加异步任务 的结合会导致新的线程开启.所以每一个任务都会在不同的线程进行执行.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 20; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d - %@", i, [NSThread currentThread]);
        });
    }
    NSLog(@"End");
}

由于是并发队列,添加异步任务. 所以与上一题的结果分析一致.这里就不过多叙述了.

先打印End,再随机打印 0 - 19; 打印的线程也不是同一个,是 20 个不同的线程.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    for (int i = 0; i < 20; i++) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d - %@", i, [NSThread currentThread]);
        });
    }
    NSLog(@"End");
}

先依次打印 0 - 19,再打印End; 不会创建新的线程,所以所有的线程都是当前的线程.

全局队列(并发队列) 与 同步添加任务,不会创建新的线程;并且在同一线程里面执行的话,那么就是顺序执行.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

大概率打印顺序为 1 → 5 → 2 → 3 → 4. 或者 1 → 2 → 3 → 4 → 5.

并发队列 + 添加异步任务 会导致创建新的线程. 当前线程与新线程的执行顺序未知, 所以大概率会先执行 1 → 5 .

在新的线程中,同步添加任务打印3,那么不会再次创建新的线程了,只会在当前线程串行执行. 也就是 2 → 3 → 4.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

并发队列 + 异步 嵌套 异步,那么会出现以下几种情况.

1 → 5 → 2 → 3 → 4

1 → 5 → 2 → 4 → 3

1 → 2 → 3 → 4 → 5

1 → 2 → 4 → 3 → 5

大概率为 1 → 5 → 2 → 4 → 3

并发队列 与 添加异步任务会创建新的线程, 新的线程就会导致顺序的不一致.
但是创建线程需要时间,所以大概率的打印顺序为 1 → 5 → 2 → 4 → 3


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 20; i++) {
        dispatch_async(queue, ^{
            NSLog(@"第一 %d - %@", i, [NSThread currentThread]);
            dispatch_barrier_async(queue, ^{
                NSLog(@"第二 %d - %@", i, [NSThread currentThread]);
            });
            NSLog(@"第三 %d - %@", i, [NSThread currentThread]);
        });
    }
}

混合打印所有的 "第一 ..." 和 "第三... ", 顺序随机, 然后打印所有的 "第二 ....", 顺序随机. 所有的 "第二 ...." 打印的线程都是在同一个线程中.

使用栅栏异步函数,并不会阻塞线程,再添加完成异步任务就会直接打印 "第三... ".


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1");
    });
    
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"2");
    });
    dispatch_barrier_sync(queue, ^{
        NSLog(@"----%@-----",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
}

打印顺序为 1 → 2 → (栅栏函数打印线程) → (3 4), 大概率为 1 → 2 → (栅栏函数打印线程) → 4 → 3

栅栏同步函数 + 并发队列 会真正导致线程的阻塞, 那么 打印2 操作内部有延时,也会等待完成之后再执行栅栏同步函数的Block, 打印3 会创建新的线程.有一定的延时性, 所以大概率的顺序是 4 → 3.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_async(globalQueue, ^{
        NSLog(@"1");
    });
    
    dispatch_async(globalQueue, ^{
        sleep(1);
        NSLog(@"2");
    });
    dispatch_barrier_sync(globalQueue, ^{
        NSLog(@"----%@-----",[NSThread currentThread]);
    });
    dispatch_async(globalQueue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
}

打印顺序为随机.

栅栏同步函数 + 全局并发队列,这时候栅栏函数相当于是dispatch_async,所以打印顺序为随机顺序.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(globalQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        sleep(1);
        NSLog(@"1");
    });
    
    dispatch_async(globalQueue, ^{
        dispatch_semaphore_signal(semaphore);
        NSLog(@"2");
    });
    dispatch_async(globalQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"3");
    });
    NSLog(@"4");
}

打印顺序大概率为 4 → 2 → 1, 3不会打印.

信号量 与 全局队列(并发队列),那么会 打印1打印3 都会进入等待.

执行完 打印2 之后,往信号量发送信号, 打印1打印3 会根据添加顺序进行执行,大概率打印1 先进先出先执行, 所以 打印3 没有机会执行.

添加异步任务,会在当前函数执行完再执行,所以会先 打印4.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(globalQueue, ^{
        sleep(10);
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"1");
    });
    
    dispatch_async(globalQueue, ^{
        NSLog(@"2");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(globalQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"3");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(globalQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"4");
        dispatch_semaphore_signal(semaphore);
    });
}

打印顺序大概率为 2 → (4 , 3 随机顺序) → 1.

信号量 与 全局队列(并发队列),打印2 会先进入,然后打印完成之后发送信息.

打印3 打印4 都有等待与发送信息逻辑代码, 大概率会是 打印3 先于 打印4 进入等待状态,所以一般为先 打印3打印4 .

打印1 则刚进入线程就休眠 10 秒, 10秒之后很大概率已经满足信号量条件所以会执行 打印1.


问题: 下面GCD代码会打印什么? 为什么?


- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
     dispatch_semaphore_t sem = dispatch_semaphore_create(0);
     dispatch_queue_t queue1 = dispatch_queue_create("queue1", NULL);

     dispatch_async(queue, ^{
         dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
         sleep(2);
         NSLog(@"1");
     });
     
     dispatch_async(queue1, ^{
         dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
         sleep(2);
         NSLog(@"2");
     });
     
     dispatch_async(queue1, ^{
         sleep(2);
         NSLog(@"3");
         dispatch_semaphore_signal(sem);
     });
     
     dispatch_async(queue, ^{
         dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
         sleep(2);
         NSLog(@"4");
         dispatch_semaphore_signal(sem);
     });
}

结果为 无输出信息

打印2 打印3 在串行队列中,所以先执行完打印2, 再执行打印3. 打印2 需要一直等待信息,信息的发送在 打印3 中.所以最终不能造成所有的流程都不能进行打印输出.


问题: 实现一个场景:图片异步加载,确保全部加载完成后,再使用,保证数据安全


- (void)download {
    dispatch_group_t group = dispatch_group_create();
    dispath_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

    __weak typeof(self) weakSelf = self;
    dispatch_group_async(group, queue, ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        //创建调度组
        NSString *logoStr1 = @"https://img1.baidu.com/it/u=1313595101,2257100959&fm=253&fmt=auto&app=120&f=GIF?w=450&h=300";
        NSData *data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr1]];
        UIImage *image1 = [UIImage imageWithData:data1];
        dispatch_barrier_async(queue, ^{
            [strongSelf.mArray addObject:image1];
        });
    });

    dispatch_group_async(group, queue, ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        //创建调度组
        NSString *logoStr2 = @"https://img1.baidu.com/it/u=1313595101,2257100959&fm=253&fmt=auto&app=120&f=GIF?w=450&h=300";
        NSData *data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr2]];
        UIImage *image2 = [UIImage imageWithData:data2];
        dispatch_barrier_async(queue, ^{
            [strongSelf.mArray addObject:image2];
        });
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{

    });
}

IT界无底坑洞栋主 欢迎加Q骚扰:676758285