使用 objc_sync 进行原子操作

什么叫原子操作

对于一个资源,在写入或读取时,只允许在一个时刻一个角色进行操作,则为原子操作。

你可以简单粗暴地这么理解,我的银行帐号里面有100块钱,假如两个人同时在不同的ATM机上操作,他们的操作都是取100块钱,那ATM会不会都吐出100块钱出来呢?

假如是,那么,取钱这个操作就是非原子性的。
假如不是,那么,取钱这个操作就是原子性的。

Swift

对于 let 声明的资源,永远是原子性的。
对于 var 声明的资源,是非原子性的,对其进行读写时,必须使用一定的手段,确保其值的正确性。

那么,在什么情况下,需要用这些手段去保证原子操作。

  • 如果你对资源的一致性要求不高,则不需要
  • 如果资源可能在多个线程中被读取或者写入
  • 如果资源的访问或写入需要被有序地执行

一个栗子

假设我们需要一个 ID 发生器,它的发生机制是从 0 ~ Int.max,它生成的 ID 不能是重复的,它生成的ID必须是正序的。

我们在 Playground 下输入以下代码,并查看结果。

import Foundation

struct TaskIDGenerater {
    
    static var value: Int = 0
    
    static func generate() -> Int {
        TaskIDGenerater.value++
        return TaskIDGenerater.value
    }
    
}

for _ in 0...10 {
    print(TaskIDGenerater.generate())
}

NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 5))

可以看到,控制台输出

1
2
3
4
5
6
7
8
9
10
11

现在,我们分开三个线程进行 ID 生成操作。

import Foundation

struct TaskIDGenerater {
    
    static var value: Int = 0
    
    static func generate() -> Int {
        TaskIDGenerater.value++
        return TaskIDGenerater.value
    }
    
}

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
    for _ in 0...10 {
        print(TaskIDGenerater.generate())
    }
}

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
    for _ in 0...10 {
        print(TaskIDGenerater.generate())
    }
}

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
    for _ in 0...10 {
        print(TaskIDGenerater.generate())
    }
}

NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 5))

现在,输出变得奇怪无比了!

2
2
2
5
5
5
8
8
8
11
11
11
14
14
14
17
17
17
19
20
20
23
22
23
26
26
26
29
29
29
31
32
32

解决方案

在 Objective-C 中, 我们可以使用以下方法解决问题。

@synchronized(<#token#>) {
    <#statements#>
}

在 Swift 中,我们使用类似的方法。

objc_sync_enter(lock)
TaskIDGenerater.value++
objc_sync_exit(lock)

使用 objc_sync_enter 和 objc_sync_exit 包裹的代码会被有序、同步地执行。
同时,你需要给这两个方法指定一个参考变量,这个参考变量可以是任意 var 类型的值。

但是,需要谨记,一旦 sync_enter 以后,整个应用就会被锁定,直至 sync_exit。
所以,在 lock 的代码区域中,要多加留意,看是否存在死锁的现象。

修改后的代码如下

import Foundation

struct TaskIDGenerater {
    
    static var value: Int = 0
    
    static var lock: Int = 0
    
    static func generate() -> Int {
        objc_sync_enter(lock)
        TaskIDGenerater.value++
        let value = TaskIDGenerater.value
        objc_sync_exit(lock)
        return value
    }
    
}

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
    for _ in 0...10 {
        print(TaskIDGenerater.generate())
    }
}

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
    for _ in 0...10 {
        print(TaskIDGenerater.generate())
    }
}

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
    for _ in 0...10 {
        print(TaskIDGenerater.generate())
    }
}

NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 5))

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
25
24
26
27
28
29
30
31
32
33

结语

在 Objective-C 的时代,我们使用 Copy() 以及非可变类型保证线程安全。
在 Swift 的世界,因为let的存在,可变性的线程安全问题经常被开发者忽略,如果哪一天,你的应用出现了难以重现的问题,不妨从原子操作问题查起。

你可能感兴趣的:(使用 objc_sync 进行原子操作)