iOS多线程

iOS多线程

iOS中常见的多线程方案

  1. pthread是一套通用的多线程API,适用于Unix/Linux/Windows等系统,跨平台/可移植,使用难度比较大,生命周期由程序员管理
  2. NSThread 是一个面向对象的线程类,简单易用可以直接操作线程对象,生命周期由程序员管理
  3. GCD是为了替代NSThread等线程技术,充分利用设备的多核,生命周期由系统自动管理
  4. NSOperation是对GCD的封装,使用起来更加面向对象,生命周期由系统自动管理

本文主要以GCD为例来阐述。

GCD的常用函数

同步的方式执行任务

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

queue:队列;
block:任务;

用异步的方式执行任务

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

GCD的队列

GCD的队列可以分为两大类型

  1. 并发队列 - Concurrent Dispatch Queue
  • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
  • 并发功能只有在异步dispatch_async函数下才有效
  1. 串行队列 - Serial Dispatch Queue
  • 让任务一个接一个的执行(一个任务执行完毕后,再执行下一个任务)

同步、异步、并发、串行

  1. 同步和异步主要影响:能不能开启新的线程
  • 同步:在当前线程中执行任务,不具备开启新线程的能力
  • 异步:在新的线程中执行任务,具备开启新线程的能力
  1. 并发和串行主要影响:任务的执行方式
  • 并发:多个任务并发(同时)执行
  • 串行:一个任务完毕后,再执行下一个任务

各种队列的执行效果

image-20210906235429846

  • 使用sync 函数往当前串行队列中添加任务,会卡主当前的串行队列(产生死锁)
1
2
3
4
5
6
7
-(void)viewDidLoad{
NSLog(@"执行任务一");
dispatch_sync(dispatch_get_main_queue(),^{
NSLog(@"执行任务二");
});
NSLog(@"执行任务三");
}

上面这段代码会有产生上述问题,导致死锁。

多线程安全隐患

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

那么该如何解决上述问题吗?
答案是:线程同步,使用线程同步技术,协同步调,按预定的先后次序进行;
常见的线程同步技术是:加锁

image-20210915154125563

iOS中的线程同步方案

  1. OSSpinLock
  2. os_unfair_lock
  3. pthread_mutex
  4. dispatch_semaphore
  5. dispatch_queue(DISPATCH_QUEUE_SERIAL)
  6. NSLock
  7. NSRecursiveLock
  8. NSCondation
  9. NSCondationLock
  10. @synchronized

接下来我们一一介绍

  1. OSSpinLock叫做自旋锁,等待锁的线程会处于“忙等(busy-wait)”状态,一直占用着CPU资源
    注意:这种锁目前已经不安全,可能会出现优先级反转问题
  • 如果等待说的优先级比较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
  • 需要导入头问题件 #import<libkern/OSAtomic.h>

使用范例:

1
2
3
4
5
6
7
8
// 初始化
OSSPinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁(如果等待就不加锁,直接返回false;如果不等待就加锁返回true)
bool result = OSSpinLockTry(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnLock(&lock);
  1. os_unfair_lock用于取代不安全的OSSpinLock,从iOS10才支持

    • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
    • 需要导入头文件#import<os/lock.h>

使用范例:

1
2
3
4
5
6
7
8
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);
  1. pthread_mutex
    • mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
    • 需要导入头文件#import"<pthread.h>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,&attr);
// 尝试加锁
pthread_mutex_trylock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁相关资源
pthread_mutexattr_destroy(&mutex);
pthread_mutex_destroy(&mutex);

/*
属性类型
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
*/

pthread_mutex - 递归锁

1
2
3
4
5
6
7
// 初始化所得属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,&attr);

pthread_mutex - 条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化锁
pthread_mutex_t mutex;
// NULL代表使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&condition, &mutex);
// 激活一个等待该条件的线程
pthread_cond_signal(&condition);
// 激活所有等待条件的线程
pthread_cond_broadcast(&condition);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);

系统封装起来的锁:NSLock、NSRecursiveLock、NSCondition、NSConditionLock

  • NSLock是对 mutex普通锁的封装
  • NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
  • NSConditon是对mutexcond的封装
  • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
  1. dispatch_semaphore
  • semaphore叫做信号量,信号量的初始值可以用来控制线程并发访问的最大数量,信号量初始值为1,代表同时只允许1条线程访问资源,保证线程同步。
1
2
3
4
5
6
7
8
9
// 信号量初始值
int value= 1;
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信号量的值小于等于0,当前线程就会进入休眠状态(直到信号量大于0)
// 如果信号量的值大于0,就减1,然后往下执行后面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值加1
dispatch_semaphore_signal(semaphore);
  1. dispatch_queue

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

1
2
3
4
dispatch_queue_t queue = dispatch_queue_create("lcok_queue",DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue,^{
//task
});
  1. @synchronized 是对mutex递归锁的封装,@synchronized(obj)内部会生成对obj对应的递归锁,然后进行加锁、解锁操作

    1
    2
    3
    @synchronized(obj){
    //task
    }

iOS线程同步方案性能比较

性能从高到低排序:

1
2
3
4
5
6
7
8
9
10
11
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthred_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

自旋锁、互斥锁比较

  • 什么情况使用自旋锁比较划算?
  1. 预计线程等待锁的时间很短
  2. 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  3. CPU资源不紧张
  4. 多核处理器
  • 什么情况使用互斥锁比较划算?
  1. 预计线程等待锁的时间较长
  2. 单核处理器
  3. 临界区有IO操作
  4. 临界区代码复杂或者循环量大
  5. 临界区竞争很激烈

atomic

atomic用于保证属性setter、getter的原子操作,相当于在getter和setter内部家里线程同步的锁,它并不能保证属性的使用过程是线程安全的

iOS中的读写安全方案

同一时间,只允许一个线程进行写操作,允许有多个线程进行读操作,并且不允许既有写的操作又有读的操作。这种场景就是典型的”多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:
pthread_rwlock:读写锁
dispatch_barrier_async:异步栅栏调用

谢谢您的支持!