iOS 多线程安全

多线程知识的简单介绍

进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
  • 通过“活动监视器”可以查看mac系统中所开启的线程
  • MAC是多进程的,iOS是单进程的。
  • 进程中包含多个线程,进程负责任务的调度,线程负责任务的执行。

在iOS中并不支持多进程,所有程序都是单一进程运行,进程之间相互独立。

wecom-temp-a691821dca665276208b9c85be7a5ee8.png

线程

  • 程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程
  • 程序启动会默认开启一条线程,这条线程被称为主线程或者UI线程
线程生命周期.png

多线程

iOS中多线程同时执行的本质是CPU在多个任务之间快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片。所以多线程并不是真正的并发,而真正的并发必须建立在多核CPU的基础上。

多线程在iOS上的应用

在iOS开发中 ,关于多线程现有的框架有:pthread、NSTread、GCD、NSOperation

iOS多线程.jpeg
1. pthread(了解)
  • 创建phtread_create
  • create一次就会创建一个新的线程
  • 系统会自动在子线程中调用传入的函数
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg)
2. NSTread(了解/掌握)
Thread使用
  1. 第一种创建方式
//第一种创建方式
let thread = Thread.init(target: self, selector: #selector(run), object: nil)
// 设置线程的名称
thread.name = "线程A"
// 启动线程---线程一启动,就会在线程thread中执行self的run方法
thread.start()
  • 注意: 需要手动启动线程
  • 特点: 系统内部会retain当前线程
  • 只有线程中的方法执行完毕, 系统才会将其释放
  1. 第二种创建方式
 // 分离出一条子线程,自动启动线程,但无法获得线程对象
 Thread.detachNewThreadSelector(#selector(run), toTarget: self, with: nil)
// 开启一条后台线程,自动启动线程,但无法获得线程对象
 self.performSelector(inBackground: #selector(run), withObject: nil)
  • 不用手动调用start方法, 系统会自动启动
  • 没有返回值, 不能对线程进行更多的设置
  • 应用场景: 需要快速简便的执行线程
Thread线程状态

线程的状态:新建-就绪-运行-阻塞-死亡

//取消线程--cancel并不能exit线程,只是标记为canceled,但线程并没有死掉
Thread.current.cancel()
// 程的退出
Thread.exit()
// 线程的休眠1
Thread.sleep(forTimeInterval: 2.0)
// 线程的休眠2
 Thread.sleep(until: Date.init(timeIntervalSinceNow: 3.0))

一个NSThread对象就代表一条线程


Thread线程测试.png
3. GCD(了解/掌握)

GCD 是苹果公司为多核的并行运算提出的解决方案, GCD会自动利用更多的 CPU 内核(比如双核、四核)来开启线程执行任务,GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程),不需要我们程序员手动管理内存

GCD队列

  • 并发队列
    - 可以让多个任务并发(同时)执行,开启多个线程同时执行任务
    - 并发功能只有在异步async函数下才有效
  • 串行队列
    - 让任务一个接着一个地执行,遵循FIFO

GCD四种队列

  • 串行队列
//OC
dispatch_queue_create("queue.name", DISPATCH_QUEUE_SERIAL);
//swift
let queue = DispatchQueue(label: "queue.name")

所有任务按顺序依次执行,结束顺序固定,符合先进先出(FIFO)的基本原则,队列后面的任务必须等待前面的任务执行完毕后才出队列。但是,不要认为串行队列中的所有任务都在同一个线程中执行,串行队列中的异步任务,可能会开启新线程去执行

  • 并发队列
//OC
dispatch_queue_create("queue.name",DISPATCH_QUEUE_CONCURRENT);
//Swift
let queue = DispatchQueue(label: "queue.name", attributes: .concurrent)

所有任务可以同时执行,结束顺序不固定,只要有可用线程,则队列头部任务将持续出队列

  • 主队列
//OC
dispatch_get_main_queue()
//swift
DispatchQueue.main

主队列本质是一个特殊的串行队列,主队列的任务都在主线程来执行,专门负责调度主线程度的任务,无法开辟新的线程。所以,在主队列下的任务不管是异步任务还是同步任务都不会开辟线程,任务只会在主线程顺序执行。如果在主线程上已经有任务正在执行,主队列会等到主线程空闲后再调度任务

  • 全局队列
//OC
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//swift
 DispatchQueue.global(priority: 0)

全局队列本质是一个特殊的并发队列。可以设置“调度优先级” 参数

线程与队列的组合分析

  • 串行队列+异步:顺序执行,先进先出,可能会开启新线程
let queue = DispatchQueue(label: "queue.name")

for i in 0 ..< 5 {
    queue.async {
        print("这是第 %d 个任务;线程 %@", i, Thread.current)
        if ( i == 1 || i == 3 ){
            Thread.sleep(forTimeInterval: 2)
        }
    }
}
print("-----------------所有任务执行完毕")
====================================================
这是第 %d 个任务;线程 %@ 0 <NSThread: 0x6000005bc300>{number = 4, name = (null)}
-----------------所有任务执行完毕
这是第 %d 个任务;线程 %@ 1 <NSThread: 0x6000005bc300>{number = 4, name = (null)}
这是第 %d 个任务;线程 %@ 2 <NSThread: 0x6000005bc300>{number = 4, name = (null)}
这是第 %d 个任务;线程 %@ 3 <NSThread: 0x6000005bc300>{number = 4, name = (null)}
这是第 %d 个任务;线程 %@ 4 <NSThread: 0x6000005bc300>{number = 4, name = (null)}
  • 串行队列+同步:顺序执行,先进先出,不会开启新线程
let queue = DispatchQueue(label: "queue.name")

for i in 0 ..< 5 {
    queue.sync {
        print("这是第 %d 个任务;线程 %@", i, Thread.current)
        if ( i == 1 || i == 3 ){
            Thread.sleep(forTimeInterval: 2)
        }
    }
}
print("-----------------所有任务执行完毕")
====================================================
这是第 %d 个任务;线程 %@ 0 <_NSMainThread: 0x600003aa41c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 1 <_NSMainThread: 0x600003aa41c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 2 <_NSMainThread: 0x600003aa41c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 3 <_NSMainThread: 0x600003aa41c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 4 <_NSMainThread: 0x600003aa41c0>{number = 1, name = main}
-----------------所有任务执行完毕
  • 并发队列+异步:同时执行,每个任务的结束顺序不确定,会开启新线程
let queue = DispatchQueue(label: "queue.name", attributes: .concurrent)

for i in 0 ..< 5 {
    queue.async {
        print("这是第 %d 个任务;线程 %@", i, Thread.current)
        if ( i == 1 || i == 3 ){
            Thread.sleep(forTimeInterval: 2)
        }
    }
}

print("-----------------所有任务执行完毕")
====================================================
这是第 %d 个任务;线程 %@ 0 <NSThread: 0x600000f4fc40>{number = 8, name = (null)}
这是第 %d 个任务;线程 %@ 1 <NSThread: 0x600000f54dc0>{number = 7, name = (null)}
这是第 %d 个任务;线程 %@ 2 <NSThread: 0x600000f543c0>{number = 6, name = (null)}
这是第 %d 个任务;线程 %@ 3 <NSThread: 0x600000f54440>{number = 4, name = (null)}
这是第 %d 个任务;线程 %@ 4 <NSThread: 0x600000f4e100>{number = 3, name = (null)}
  • 并发队列+同步:顺序执行,先进先出,不会开启新线程
let queue = DispatchQueue(label: "queue.name", attributes: .concurrent)

for i in 0 ..< 5 {
    queue.sync {
        print("这是第 %d 个任务;线程 %@", i, Thread.current)
        if ( i == 1 || i == 3 ){
            Thread.sleep(forTimeInterval: 2)
        }
    }
}

print("-----------------所有任务执行完毕")
====================================================
这是第 %d 个任务;线程 %@ 0 <_NSMainThread: 0x6000039a0000>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 1 <_NSMainThread: 0x6000039a0000>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 2 <_NSMainThread: 0x6000039a0000>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 3 <_NSMainThread: 0x6000039a0000>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 4 <_NSMainThread: 0x6000039a0000>{number = 1, name = main}
-----------------所有任务执行完毕
  • 主队列+异步:不会立即执行,而是等待主队列中的所有其他除我们添加到主队列的任务都执行完毕之后,才会执行我们添加的任务
let queue = DispatchQueue.main

for i in 0 ..< 5 {
    queue.async {
        print("这是第 %d 个任务;线程 %@", i, Thread.current)
        if ( i == 1 || i == 3 ){
            Thread.sleep(forTimeInterval: 2)
        }
    }
}
print("-----------------所有任务执行完毕")
====================================================
-----------------所有任务执行完毕
这是第 %d 个任务;线程 %@ 0 <_NSMainThread: 0x60000197c5c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 1 <_NSMainThread: 0x60000197c5c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 2 <_NSMainThread: 0x60000197c5c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 3 <_NSMainThread: 0x60000197c5c0>{number = 1, name = main}
这是第 %d 个任务;线程 %@ 4 <_NSMainThread: 0x60000197c5c0>{number = 1, name = main}
  • 主队列+同步:会相互等待导致死锁
//同步任务+主线程 == 相互等待,死锁
let queue = DispatchQueue.main

for i in 0 ..< 5 {
    queue.sync {
        print("这是第 %d 个任务;线程 %@", i, Thread.current)
        if ( i == 1 || i == 3 ){
            Thread.sleep(forTimeInterval: 2)
        }
    }
}
print("-----------------所有任务执行完毕")
====================================================
运行崩溃报错,相互等待,死锁

综上所述列表总结一下吧:

并发队列 串行队列 主队列
同步 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务
异步 有开启新线程,并行执行任务 有开启新线程,串行执行任务 没有开启新线程,串行执行任务

其他操作

  • asyncAfter
    asyncAfter函数可以设置延迟一段时间后运行闭包,功能类似于定时器
  • DispatchGroup
4. NSOperation(了解/掌握)
  • 基于GCD封装的抽象类。
  • 如果不使用NSOperationQueue单独使用为同步操作,不会开启线程。异步执行需要配合NSOperationQueue使用。
  • 有两个子类分别为:NSInvocationOperation和NSBlockOperation。
  • 可以添加依赖控制所需要执行的任务。
  • 可以通过maxConcurrentOperationCount控制并发数量。
  • NSOperation支持KVO(Key-Value Observing),可以方便的监听任务的状态(完成、执行中、取消等等状态)
  • GCD是按FIFO顺序来执行的,而NSOperation能够方便地通过依赖关系设置操作执行顺序,可以控制任务在特定的任务执行完后才执行。
  • swfit 中 NSOperation和 NSOperationQueue 都去掉了前缀NS,直接叫 Operation 和OperationQueue。
  • 苹果认为 NSInvocationOperation 不是类型安全或者不是 ARC 安全的,在 Swift中 取消了与之相关的 API。

NSOperation(任务)

NSOperation是个抽象类,实际运用需要使用他的子类。

  • 3种创建方式:
  1. NSInvocationOperation(swift不支持): 此类调用选择器方法(selector), 在方法里面编写任务代码.

  2. NSBlockOperation (swift对应BlockOperation): 此类采用block方式, 在block中编写任务代码.

let operation = BlockOperation.init {
        print("\(#function), thread:\(Thread.current)")
}
  1. 定义继承自NSOperation (swift对应Operation)的子类,通过实现内部相应的方法来封装任务。
class CustomOperation: Operation {
    override func main() {
        print("\(#function), thread:\(Thread.current)")
    }
}
 let operation = CustomOperation.init()
  • 2种执行方式:
  1. 不添加到队列, 手动调用operation的start方法.不开启新线程在主线程执行

  2. 添加到队列, 系统自动调用start方法.

NSOperationQueue(队列)

队列 (Operation Queue)有两种: 主队列和非主队列 (自定义队列).

主队列通过mainQueue获得, 主队列里的任务都是放到主线程执行 (不包括使用addExecutionBlock:添加的额外操作, 因其可能在其他线程执行).
非主队列 (自定义队列) 即一般 alloc init 出来的队列, 默认在子线程中异步执行. 通多设置最大并发数(maxConcurrentOperationCount)来控制队列是串行还是并发.

  • 添加操作(任务)到队列有四种方式:
  1. addOperation:
    添加一个现有的Operation (或者其子类).
  2. addOperations:waitUntilFinished:
    可添加多个现有的Operation (或者其子类), 可设置等待所有操作完成后方可继续往下执行.
  3. addOperationWithBlock:
    直接添加一个block
  4. addBarrierBlock:
    添加栅栏, 顺带一个任务. 等栅栏前的所有任务都执行完, 再执行本栅栏的任务, 起到隔离同步等待的目的.

串行 & 并行

主队列是串行队列. 自定义队列默认是并发队列, 但可通多设置最大并发数(maxConcurrentOperationCount)来控制队列是串行还是并发.

  • maxConcurrentOperationCount
    -1, 默认值, 并发队列
    =0, 不执行任何操作
    =1, 串行队列
    <0, 除-1默认值外, 其他负值均报错
    >1, 并发队列, 如果数值过大, 最终并发数由系统决定.

其他操作

  • 取消队列NSOperationQueue的所有操作,NSOperationQueue对象方法
    cancelAllOperations()
  • 取消NSOperation的某个操作,NSOperation对象方法
    cancel()
  • 使队列暂停或继续
    queue.isSuspended = true
  • 判断队列是否暂停
    isSuspended
    暂停和取消不是立刻取消当前操作,而是等当前的操作执行完之后不再进行新的操作。
  • NSOperation的操作依赖
let queue = OperationQueue.init()

let operation1 = BlockOperation.init {
    for _ in 0 ..< 3 {
        print("operation1, thread:\(Thread.current)")
    }
}
let operation2 = BlockOperation.init {
    print("****operation2依赖于operation1,只有当operation1执行完毕,operation2才会执行****")
    for _ in 0 ..< 3 {
        print("operation2, thread:\(Thread.current)")
    }
}

operation2.addDependency(operation1)

queue.addOperation(operation1)
queue.addOperation(operation2)
====================================================
operation1, thread:<NSThread: 0x600003625000>{number = 7, name = (null)}
operation1, thread:<NSThread: 0x600003625000>{number = 7, name = (null)}
operation1, thread:<NSThread: 0x600003625000>{number = 7, name = (null)}
****operation2依赖于operation1,只有当operation1执行完毕,operation2才会执行****
operation2, thread:<NSThread: 0x6000036385c0>{number = 5, name = (null)}
operation2, thread:<NSThread: 0x6000036385c0>{number = 5, name = (null)}
operation2, thread:<NSThread: 0x6000036385c0>{number = 5, name = (null)}

多线程的优点

  1. 能适当提高程序的执行效率
  2. 能适当提高资源的利用率,如CPU、内存
  3. 线程上的任务执行完成后,线程会自动销毁

多线程的缺点

  1. 创建线程是有开销的,iOS下主要成本包括:内核数据结构(1KB)、栈空间(子线程512KB 、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间
  2. 如果开启大量线程,会降低程序的性能
  3. 线程越多,CPU在调度线程上的开销就越大
  4. 多个线程同时访问同一块资源时,很容易引发资源抢夺,造成数据错乱和数据安全问题
造成UI主线程的卡顿原因:
1. 把一些耗时的任务,放在了UI主线程中
2. 开启的子线程太多了,导致UI主线程被CPU调度的频率下降了
3. 内存这些配置太低了,就算不做耗时操作,频繁的操作UI界面,都让CPU够呛了

多线程的安全隐患

  • 资源共享

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

多个线程同时访问同一块资源时,很容易引发资源抢夺,造成数据错乱和数据安全问题如何避免?

—— 对资源进行加锁,以保证线程安全

接下来一起探索下iOS中锁的实现和使用

线程锁

锁是什么

锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

为什么要有锁?

前面说到了,锁是用来保护线程安全的工具。

可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。

当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。

所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)。

锁的分类讲解

基本的锁就包括三类:⾃旋锁、互斥锁、读写锁。其他的⽐如条件锁、递归锁、信号量都是上层的封装和实现

⾃旋锁

线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

  • OSSpinLock
  • os_unfair_lock

互斥锁

是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。

  • NSLock
  • pthread_mutex
  • @synchronized

条件锁

就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

  • NSCondition
  • NSConditionLock

递归锁

同一个线程可以加锁N次而不会引发死锁

  • NSRecursiveLock
  • pthread_mutex(recursive)

信号量

一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

  • dispatch_semaphore

读写锁

一种特殊的自旋锁。它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

  • pthread_rwlock
  • dispatch_barrier_async

锁的性能对比:


1212147-ef64a6b791be6074.png

性能从高到低排序:OSSpinLock(自旋锁)> os_unfair_lock(自旋锁)> NSCondition(条件锁)> pthread_mutex(互斥锁)> NSLock(互斥锁)> dispatch_semaphore_t(信号量)> pthread_mutex_t(recursive)(递归锁)> NSRecursiveLock(递归锁)> @synchronized(互斥锁)> NSConditionLock(条件锁)

测试方案

循环十万次,进行加锁解锁操作,计算耗时

int kc_runTimes = 100000;

<!-- OSSpinLock -->
{
    OSSpinLock kc_spinlock = OS_SPINLOCK_INIT;
    
    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        OSSpinLockLock(&kc_spinlock);          //解锁
        OSSpinLockUnlock(&kc_spinlock);
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"OSSpinLock: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- dispatch_semaphore_t -->
{
    dispatch_semaphore_t kc_sem = dispatch_semaphore_create(1);

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        dispatch_semaphore_wait(kc_sem, DISPATCH_TIME_FOREVER);
        dispatch_semaphore_signal(kc_sem);
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"dispatch_semaphore_t: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- os_unfair_lock_lock -->
{
    os_unfair_lock kc_unfairlock = OS_UNFAIR_LOCK_INIT;

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        os_unfair_lock_lock(&kc_unfairlock);
        os_unfair_lock_unlock(&kc_unfairlock);
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"os_unfair_lock_lock: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- pthread_mutex_t -->
{
    pthread_mutex_t kc_metext = PTHREAD_MUTEX_INITIALIZER;

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        pthread_mutex_lock(&kc_metext);
        pthread_mutex_unlock(&kc_metext);
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"pthread_mutex_t: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- NSLock -->
{
    NSLock *kc_lock = [NSLock new];

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        [kc_lock lock];
        [kc_lock unlock];
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"NSlock: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- NSCondition -->
{
    NSCondition *kc_condition = [NSCondition new];

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        [kc_condition lock];
        [kc_condition unlock];
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"NSCondition: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- pthread_mutex_t -->
{
    pthread_mutex_t kc_metext_recurive;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init (&attr);
    pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init (&kc_metext_recurive, &attr);

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        pthread_mutex_lock(&kc_metext_recurive);
        pthread_mutex_unlock(&kc_metext_recurive);
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- NSRecursiveLock -->
{
    NSRecursiveLock *kc_recursiveLock = [NSRecursiveLock new];

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        [kc_recursiveLock lock];
        [kc_recursiveLock unlock];
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"NSRecursiveLock: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- NSConditionLock -->
{
    NSConditionLock *kc_conditionLock = [NSConditionLock new];

    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kc_runTimes; i++) {
        [kc_conditionLock lock];
        [kc_conditionLock unlock];
    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"NSConditionLock: %f ms",(kc_endTime - kc_beginTime)*1000);
}

<!-- @synchronized -->
{
    double_t kc_beginTime = CFAbsoluteTimeGetCurrent();

    for (int i=0 ; i < kc_runTimes; i++) {
        @synchronized(self) {}

    }
    double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
    KCLog(@"@synchronized: %f ms",(kc_endTime - kc_beginTime)*1000);
}

输出结果:
<循环1000次>
2022-12-07 16:37:59.843100+0800 AKLockTest[19581:5626571] OSSpinLock: 0.033975 ms
2022-12-07 16:37:59.843228+0800 AKLockTest[19581:5626571] dispatch_semaphore_t: 0.018954 ms
2022-12-07 16:37:59.843410+0800 AKLockTest[19581:5626571] os_unfair_lock_lock: 0.044942 ms
2022-12-07 16:37:59.843596+0800 AKLockTest[19581:5626571] pthread_mutex_t: 0.064015 ms
2022-12-07 16:37:59.843744+0800 AKLockTest[19581:5626571] NSlock: 0.036001 ms
2022-12-07 16:37:59.843904+0800 AKLockTest[19581:5626571] NSCondition: 0.036001 ms
2022-12-07 16:37:59.844136+0800 AKLockTest[19581:5626571] PTHREAD_MUTEX_RECURSIVE: 0.045896 ms
2022-12-07 16:37:59.844321+0800 AKLockTest[19581:5626571] NSRecursiveLock: 0.058055 ms
2022-12-07 16:37:59.844558+0800 AKLockTest[19581:5626571] NSConditionLock: 0.116944 ms
2022-12-07 16:37:59.844850+0800 AKLockTest[19581:5626571] @synchronized: 0.167966 ms
<循环100000次>
2022-12-07 16:15:22.587874+0800 AKLockTest[19247:5602920] OSSpinLock: 0.756979 ms
2022-12-07 16:15:22.589268+0800 AKLockTest[19247:5602920] dispatch_semaphore_t: 1.297951 ms
2022-12-07 16:15:22.590893+0800 AKLockTest[19247:5602920] os_unfair_lock_lock: 1.462936 ms
2022-12-07 16:15:22.593120+0800 AKLockTest[19247:5602920] pthread_mutex_t: 2.094984 ms
2022-12-07 16:15:22.596076+0800 AKLockTest[19247:5602920] NSlock: 2.822995 ms
2022-12-07 16:15:22.599409+0800 AKLockTest[19247:5602920] NSCondition: 3.178000 ms
2022-12-07 16:15:22.604141+0800 AKLockTest[19247:5602920] PTHREAD_MUTEX_RECURSIVE: 4.518986 ms
2022-12-07 16:15:22.609652+0800 AKLockTest[19247:5602920] NSRecursiveLock: 5.347967 ms
2022-12-07 16:15:22.617816+0800 AKLockTest[19247:5602920] NSConditionLock: 8.015990 ms
2022-12-07 16:15:22.630761+0800 AKLockTest[19247:5602920] @synchronized: 12.816072 ms
自旋锁和互斥锁的区别和关系
  • 互斥锁 = 互斥 + 同步
    互斥保证线程安全,当一条线程执行时,其他线程休眠。同步保证执行顺序,多线程串行执行
  • 自旋锁 = 互斥 + 忙等,例如do...while循环。
    自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。而缺点是当等待时会消耗大量CPU资源,所以自旋锁不适用较长时间的任务

自旋锁,也是互斥锁的一种实现,而 spin lock和 mutex 两者都是为了解决某项资源的互斥使用,在任何时刻只能有一个保持者。但spin lock和 mutex 调度机制上有所不同。

iOS开发常用的锁

通过上面的内容,我们对锁有了一定的了解,接下来,让我们一起看看iOS中常用锁的实现吧

atomic(自旋锁)

atomic 本身就有一把锁(自旋锁) 单写多读:单个线程写入,多个线程可以读取

源码分析:

搜索objc_setProperty的方法实现,源码如下

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

进入reallySetProperty方法查看源码

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) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    // atomic修饰,增加了spinlock_t的锁操作;
    // 所以atomic是标示,自身并不是锁。而atomic所谓的自旋锁,由底层代码实现。
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    objc_release(oldValue);
}

pthread_mutex(互斥锁)

This check detects anytime pthread_mutex_lock(:) or pthread_mutex_unlock(:) is called with a pthread_mutex_t variable that wasn't initialized. Attempting to use an uninitialized mutex results in an error, and removes any guarantees about ordering that would exist while a mutex is locked.

使用pMutex之前一定要初始化,否则不生效

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
/加锁
pthread_mutex_lock(&_mutex);
//解锁
pthread_mutex_unlock(&_mutex);
  • pthread_mutexattr_settype可以设置锁的类型(PTHREAD_MUTEX_NORMAL代表它是一把普通的锁,PTHREAD_MUTEX_RECURSIVE代表递归锁)
  • pthread_mutex_lock`给互斥量加锁。当另外一个线程来获取这个锁的时候,发现这个锁已经加锁,那么这个线程就会进入休眠状态,直到这个互斥量被解锁,线程才会从新被唤醒。
  • pthread_mutex_trylock当互斥量已经被锁住时调用该函数将返回错误代码EBUSY,若是当前互斥量没有被锁,则会执行和pthread_mutex_lock同样的效果。
  • pthread_mutex_unlock对互斥量进行解锁

NSLock(非递归互斥锁)

NSLock非递归 互斥锁,不能多次调用 lock方法,会造成死锁。NSLock内部封装了pthread_mutex
遵循 NSLocking 协议

public protocol NSLocking {
    func lock()
    func unlock()
}

open class NSLock : NSObject, NSLocking {
    open func `try`() -> Bool
    open func lock(before limit: Date) -> Bool
    @available(iOS 2.0, *)
    open var name: String?
}
  1. lock和-unlock必须在相同的线程调用,也就是说,他们必须在同一个线程中成对调用
  2. 除 lock 和 unlock 方法外,nslock 还提供了 trylock 和 lockbeforedate:两个方法。
  3. trylock 并不会阻塞线程,trylock能加锁返回 yes,不能加锁返回 no,然后都会执行后续代码。
  4. trylock 和 lock 使用场景:当前线程锁失败,也可以继续其它任务,用 trylock 合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock。
  5. lockbeforedate: 方法会在所指定 date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 no,指定时间之前能加锁,则返回 yes。
  6. 由于是互斥锁,当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。但是如果连续锁定两次,则会造成死锁问题。

NSLock原理分析

private typealias _MutexPointer = UnsafeMutablePointer<pthread_mutex_t>
private typealias _RecursiveMutexPointer = UnsafeMutablePointer<pthread_mutex_t>
private typealias _ConditionVariablePointer = UnsafeMutablePointer<pthread_cond_t>

open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
    public override init() {
        //初始化互斥锁,没有递归类型的属性
        pthread_mutex_init(mutex, nil)
    }
    open func lock() {
         //使用mutex加锁
        pthread_mutex_lock(mutex)
    }
    open func unlock() {
         //使用mutex解锁
        pthread_mutex_unlock(mutex)
    }
}
open func `try`() -> Bool {
    return pthread_mutex_trylock(mutex) == 0
}

open func lock(before limit: Date) -> Bool {
    if pthread_mutex_trylock(mutex) == 0 {
        return true
    }
    
    return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
}

private func timedLock(mutex: _MutexPointer, endTime: Date,
                       using timeoutCond: _ConditionVariablePointer,
                       with timeoutMutex: _MutexPointer) -> Bool {
    var timeSpec = timeSpecFrom(date: endTime)
    while var ts = timeSpec {
        let lockval = pthread_mutex_lock(timeoutMutex)
        precondition(lockval == 0)
        let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)
        precondition(waitval == 0 || waitval == ETIMEDOUT)
        let unlockval = pthread_mutex_unlock(timeoutMutex)
        precondition(unlockval == 0)
        
        if waitval == ETIMEDOUT {
            return false
        }
        let tryval = pthread_mutex_trylock(mutex)
        precondition(tryval == 0 || tryval == EBUSY)
        if tryval == 0 { // The lock was obtained.
            return true
        }
        // pthread_cond_timedwait didn't timeout so wait some more.
        timeSpec = timeSpecFrom(date: endTime)
    }
    return false
}

通过源码可知验证 NSLock 是对 pthread 中互斥锁 的封装。
其他都好理解,这里列一下 timedLock() 的实现流程:
1、设定超时时间,进入while循环。
2、pthread_cond_timedwait()在本次循环中计时等待,线程进入休眠
3、等待超时,直接返回 false;
4、如果等待没有超时,期间锁被释放,线程会被唤醒,再次尝试获取锁 pthread_mutex_trylock(),如果获取成功返回true
5、即没有超时,被唤醒后也没有成功获取到锁(被其他线程抢先获得锁),重新计算超时时间进入下一次while循环

NSLock局限性

let lock = NSLock()
DispatchQueue.global().async {
    func test(value: Int) {
        lock.lock()
        if value > 0 {
           print("current value = \(value)")
           test(value: value - 1)
        }
        lock.unlock()
    }
    test(value: 10)
}
====================================================
current value = 10

递归调用 test 输出,正常应该是10 9 8 .....,但是实际只输出了10.
原因分析:因为在if 之前进行锁了之后,在if 里面有递归调用了 test 方法,又一次进来有锁了,这样被锁了多次都没去执行解锁一直处理阻塞。
这里应该换成递归锁.
例如:NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];

NSRecursiveLock(递归锁)

NSRecursiveLock内部封装了pthread_mutex

let lock = NSRecursiveLock()
DispatchQueue.global().async {
    func test(value: Int) {
        lock.lock()
        if value > 0 {
           print("current value = \(value)")
           test(value: value - 1)
        }
        lock.unlock()
    }
    test(value: 10)
}
====================================================
current value = 10
current value = 9
current value = 8
current value = 7
current value = 6
current value = 5
current value = 4
current value = 3
current value = 2
current value = 1

那为什么NSRecursiveLock就可以支持可递归加锁呢?why ?为什么呢!请继续往下看:

public override init() {
    super.init()
    var attrib = pthread_mutexattr_t()
    withUnsafeMutablePointer(to: &attrib) { attrs in
        pthread_mutexattr_init(attrs)
        // 设置为递归属性
        pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
        pthread_mutex_init(mutex, attrs)
    }
    pthread_cond_init(timeoutCond, nil)
    pthread_mutex_init(timeoutMutex, nil)
}
    
deinit {
    pthread_mutex_destroy(mutex)
    mutex.deinitialize(count: 1)
    mutex.deallocate()
    deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
}
    
open func lock() {
    pthread_mutex_lock(mutex)
}
    
open func unlock() {
    pthread_mutex_unlock(mutex)
    // Wakeup any threads waiting in lock(before:)
    pthread_mutex_lock(timeoutMutex)
    pthread_cond_broadcast(timeoutCond)
    pthread_mutex_unlock(timeoutMutex)
}
    
open func `try`() -> Bool {
    return pthread_mutex_trylock(mutex) == 0
}
    
open func lock(before limit: Date) -> Bool {
    if pthread_mutex_trylock(mutex) == 0 {
        return true
    }
        
    return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
}

#define PTHREAD_MUTEX_NORMAL        0  //普通非递归锁
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE     2   //递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL   
  1. NSRecursiveLock是对pthread_mutex的封装
  2. NSRecursiveLock支持可递归加锁,在init方法中,通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))进行了递归的设置

NSRecursiveLock局限性

如果在递归的基础上加上多线程,使用NSRecursiveLock加锁,结果会怎么样?

let lock = NSRecursiveLock()
let queue = DispatchQueue(label: "queue.name", attributes: .concurrent)
for i in 0 ..< 10 {
    queue.async {
        func test(value: Int) {
            lock.lock()
            if value > 0 {
               print("current value = \(value)")
               test(value:value - 1)
            }
            lock.unlock()
        }
        test(value: 10)
    }
}
====================================================
current value = 10

多线程访问的递归操作。此时运行程序,我们会发现,程序会崩溃在[lock unlock]这个方法,这是因为发生了死锁。多线程加锁解锁的时候,会出现互相等待解锁的情况,比如当线程2调用lock方法,在还没unlock的时候,线程3也调用了lock方法,这时候线程2已经lock,所以线程3需要等待线程2来unlock,线程2要等到线程3来unlock
这里应该换成多线程递归锁:
例如:@synchonized

@synchonized(互斥递归锁)

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        @synchronized (appDelegateClassName) {
            
        }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

在使用过程中你是否也有这样几个疑问?

  • @synchronized (参数),这里的参数该传什么?一般会传self,那么传self的意义是什么?
  • @synchronized (参数),参数可以传nil吗?传nil的话会有什么结果?
  • @synchronized (参数)可以起到加锁的效果,并且能够起到递归可重入的效果,递归可重入是什么样的结构?简单理解递归可重入就是锁里面嵌套锁
源码解析
  • clang编译出C++代码
    xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        {
            id _rethrow = 0;
            id _sync_obj = (id)appDelegateClassName;
            objc_sync_enter(_sync_obj);
            try {
                struct _SYNC_EXIT {
                    _SYNC_EXIT(id arg) : sync_exit(arg) {}
                    ~_SYNC_EXIT() {
                        objc_sync_exit(sync_exit);
                    }
                    id sync_exit;
                }
                _sync_exit(_sync_obj);
                
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_qy__11tvxy50djf0ztzh2z1vg2m0000gn_T_main_d0f51c_mi_0);
            } catch (id e) {_rethrow = e;}
            { struct _FIN { _FIN(id reth) : rethrow(reth) {}
                ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
                id rethrow;
            } _fin_force_rethow(_rethrow);}
        }
        
    }

    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

通过分析main.cpp文件,发现@synchronized (appDelegateClassName){} 执行了objc_sync_enter与objc_sync_exit方法

  1. 调用_sync_exit传入_sync_obj,相当于调用结构体的构造函数和析构函数。构造函数中没有代码,而析构函数中调用objc_sync_exit函数,传入的sync_exit等同于_sync_obj
  2. 使用objc_sync_enter(_sync_obj)函数,进行加锁
  3. 使用objc_sync_exit(_sync_obj)函数,进行解锁
  4. 使用try...catch,说明锁的使用有可能出现异常
  • 查看objc_sync_enter, objc_sync_exit函数

#   define BREAKPOINT_FUNCTION(prototype)                             \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    return result;
}
  1. 如果obj存在,则调用id2data方法获取对应的SyncData,对threadCount、lockCount进行递减操作。
  2. 如果obj为nil,什么也不做。
    objc_sync_enter加锁时传入nil对象时执行 objc_sync_nil(),相当于将void objc_sync_nil(void)传入宏,等同于void objc_sync_nil(void) { }无效代码不进行加锁操作。这里就解答了上面传入nil的问题,@synchronized (参数)参数传nil,什么都不做does nothing
  3. 在objc_sync_enter中,对SyncData对象中的mutex,调用lock进行加锁;
  4. 在objc_sync_exit中,对SyncData对象中的mutex,调用tryUnlock进行解锁。

** synchronized数据结构**

typedef struct alignas(CacheLineSize) SyncData {
    // SyncData属于单项链表 
    struct SyncData* nextData;//指向下一条数据
    DisguisedPtr<objc_object> object;//伪装,用于封装类型
    int32_t threadCount;  // number of THREADS using this block 多线程访问,记录多线程操作数
    recursive_mutex_t mutex;//递归锁,可以递归使用
} SyncData;


static SyncData* id2data(id object, enum usage why)
{
    //1、传入object,从哈希表中获取数据 
    //传入object,从哈希表中获取lock,用于保证分配SyncData代码的线程安全
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    //传入object,从哈希表中获取SyncData的地址,等同于SyncList
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
    bool fastCacheOccupied = NO;
    //2、在当前线程的tls(线程局部存储)中寻找
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY); // 通过KVC方式对线程进行获取 线程绑定的data
    if (data) {
        fastCacheOccupied = YES;
        //SyncData中的对象和传入的对象相同
        if (data->object == object) {
            //可以进入到这里,应该是同一线程中对同一对象,进行嵌套@synchronized
            uintptr_t lockCount;
            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }
            switch(why) {
            case ACQUIRE: {
                //锁的次数+1
                lockCount++;
                //存储到tls中
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                //锁的次数-1
                lockCount--;
                //存储到tls中
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    //删除tls线程局部存储
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    //对SyncData对象的threadCount进行-1,因为当前线程中的对象已经解锁
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                break;
            }
            return result;
        }
    }
#endif
    //3、tls中未找到,在各自线程的缓存中查找
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int I;
        //遍历缓存
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[I];
            //item中的对象和传入的对象不一致,跳过
            if (item->data->object != object) continue;
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }                
            switch(why) {
            case ACQUIRE:
                //锁的次数+1
                item->lockCount++;
                break;
            case RELEASE:
                //锁的次数-1
                item->lockCount--;
                if (item->lockCount == 0) {
                    //从缓存中删除
                    cache->list[i] = cache->list[--cache->used];
                    //对SyncData对象的threadCount进行-1,因为当前线程中的对象已经解锁
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }
            return result;
        }
    }
    //加锁,保证下面分配SyncData代码的线程安全(第一次进来,所有缓存都找不到)
    lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //4、遍历SyncList,如果无法遍历,证明当前object第一次进入,需要分配新的SyncData
        for (p = *listp; p != NULL; p = p->nextData) {
            //遍历如果链表中存在SyncData的object和传入的object相等
            if ( p->object == object ) {
                //将p赋值给result
                result = p;
                //对threadCount进行+1
                OSAtomicIncrement32Barrier(&result->threadCount);
                //跳转至done
                goto done;
            }
            //找到一个未使用的SyncData
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
        //未找到与对象关联的SyncData,如果当前非ACQUIRE逻辑,直接进入done
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
        //从SyncList中找到未使用的SyncData,进行覆盖
        if ( firstUnused != NULL ) { //第一次进来,没有找到
            //赋值给result
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            //跳转至done
            goto done;
        }
    }
    //5、分配一个新的SyncData并添加到SyncList中,并且支持内存对齐
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    //使用单链表头插法,新增节点总是插在头部
    result->nextData = *listp;
    *listp = result;
 done:
    //解锁,保证上面分配SyncData代码的线程安全
    lockp->unlock();

    if (result) {
         //一些错误处理,应该只有ACQUIRE时,产生新SyncData时进入这里 
         //所有的RELEASE和CHECK和递归ACQUIRE,都应该由上面的线程缓存处理
        if (why == RELEASE) {
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");
//6、保存到tls线程或者缓存中
        if (!fastCacheOccupied) {
            //保存到当前线程的tls中
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            //tls还在占用,保存到缓存
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }
    return result;
}

主要步骤

  1. 传入object从sDataLists哈希表中获取数据, sDataLists是一个全局哈希表,存放的是SyncList,SyncList绑定了需要加锁的object,采用的是拉链法,拉链的是SyncData


    sDataLists.png
  2. 在当前线程的tls(线程局部存储)中寻找
  3. tls中未找到,在各自线程的缓存中查找
  4. 遍历SyncList找到SyncData,相当于在所有线程中寻址
  5. 分配一个新的SyncData并添加到SyncList中
  6. 将SyncData保存到tls线程或者缓存中

synchronized的注意事项

  • @synchronized为递归互斥锁,lockCount表示可递归使用,threadCount表示可在多线程中使用;
  • 使用@synchronized时,不能传入nil,使用nil锁的功能无法生效;
    在日常开发中,经常会传入self,它的好处可以保证生命周期同步,对象不会提前释放;
  • 不能使用非OC对象作为加锁对象,因为其object的参数为id类型;
  • 底层的缓存和链表都使用循环遍历查找,所以性能偏低。但开发中使用方便简单,并且不用解锁,所以使用频率较高。

OSSpinLock/os_unfair_lock(自旋锁)

OSSpinLock等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
OSSpinLock在iOS10之后弃用了,使用os_unfair_lock代替

OSSpinLock不再安全

系统使用基于优先级(priority)的抢占式调度算法时,high priority线程始终会在low priority线程前执行,可能会出现优先级反转的问题

举个🌰:

task1:high priority,要使用资源data
task2:low priority,要使用资源data
spin_lock:一把自旋锁

1.先执行task2,拿到spin_lock,由于任务比较耗时,未执行完,继续持有锁。
2.紧接着执行task1,等待锁释放。由于优先级高,得到系统分配更多的CPU。
3.task1得不到CPU时间,无法正常执行,因此形成短暂死锁。

os_unfair_lock

os_unfair_lock是一种底层锁,用于取代OSSpinLock,尝试获取已加锁的线程无需忙等,解锁时由内核唤醒。os_unfair_lock锁的线程会处于休眠状态,并非忙等

private var lock: os_unfair_lock = os_unfair_lock_s()
os_unfair_lock_lock(&lock)
//dosomthing
os_unfair_lock_unlock(&lock)
  1. 锁的实现依赖锁值和拥有锁进程的地址,因此线程、进程不能通过共享、映射内存地址获取os_unfair_lock
  2. 如果锁已经加锁,os_unfair_lock_lock()会休眠,解锁后由内核唤醒
  3. os_unfair_lock_trylock()遇到已经加锁的锁,会直接返回 false。不要在循环中调用os_unfair_lock_trylock()函数,os_unfair_lock_lock()函数已经实现了循环功能
  4. 必须在加锁的线程调用os_unfair_lock_unlock()解锁,从其他线程解锁会产生运行时错误

信号量dispatch_semaphore_t

dispatch_semaphore是GCD用来同步的一种方式,当信号量设置为1的时候相当于一把同步锁,与他相关的共有三个函数,分别是:

  • dispatch_semaphore_create(创建一个semaphore)
  • dispatch_semaphore_signal(发送一个信号)
  • dispatch_semaphore_wait(等待信号)
#define INIT(...) self = super.init; \
if (!self) return nil; \
__VA_ARGS__; \
if (!_arr) return nil; \
_lock = dispatch_semaphore_create(1); \
return self;


#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);

读写锁

多读单写,即,写入操作只能串行执行,且写入时,不能读取,而读取需支持多线程操作,且读取时,不能写入

实现方式这里就提供两种:pthread_rwlock、GCD的barrier来实现

** pthread_rwlock**

  • pthread_rwlock_init,初始化锁

  • pthread_rwlock_rdlock,阻断性的读锁定读写锁

  • pthread_rwlock_tryrdlock,非阻断性的读锁定读写锁

  • pthread_rwlock_wrlock,阻断性的写锁定读写锁

  • pthread_rwlock_trywrlock,非阻断性的写锁定读写锁

  • pthread_rwlock_unlock,解锁

  • pthread_rwlock_destroy,销毁锁释放

使用如下

//初始化pthread读写锁
- (void)setupPhreadRW {
    pthread_rwlock_init(&_lock, NULL);
    //使用完毕销毁读写锁
    //pthread_rwlock_destroy(&_lock);
}

#pragma mark --通过pthread读写锁来设置
- (void)setLock1:(NSString *)lock1 {
    pthread_rwlock_wrlock(&_lock);
    _lock1 = lock1;
    pthread_rwlock_unlock(&_lock);

}
- (NSString *)lock1 {
    NSString *lock1 = nil;
    pthread_rwlock_rdlock(&_lock);
    lock1 = [_lock1 copy]; //copy到新的地址,避免解锁后拿到旧值
    pthread_rwlock_unlock(&_lock);
    return lock1;
}

GCD的barrier读写锁

GCD的barrier栅栏功能在一个新创建的队列中,barrier功能可以保证,在他之前的异步队列执行完毕才指定barrier中间的内容,且还能保证barrier执行完毕后,才之后barrier之后的任务,且一个队列可以有多个barrier

因此此特性可以用于完成一个读写锁功能,即 barrier的代码块作为 写入操作模块

- (void)setupGCDRW {
    _queue = dispatch_queue_create("RWLockQueue", DISPATCH_QUEUE_CONCURRENT);
}

#pragma mark --通过GCD的barrier栅栏功能实现
//通过GCD的barrier栅栏功能实现,缺点是需要借助自定义队列实现,且get方法无法重写系统的,只能以回调的方式获取值
//barrier功能使用global队列会失效,全局队列是无法阻塞的,里面有系统的一些任务执行
- (void)setLock2:(NSString *)lock2 {
    dispatch_barrier_async(_queue, ^{
        self->_lock2 = lock2;
    });
}
- (void)getLock2WithBlock:(void(^)(NSString *))block {
    dispatch_async(_queue, ^{
        block(self->_lock2);
    });
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容