多用派发队列,少用同步锁
《Effective Objective-C》中第41条: 多用派发队列,少用同步锁。多个线程执行同一份代码时,很可能会造成数据不同步,这是推荐采用GCD来为代码加锁解决问题。
锁与线程安全
快速回顾一下锁
锁(lock) 或者 互斥锁(mutex) 是一种结构,用来保证一段代码在同一时刻只有一个线程执行。它们通常被用来保证多线程访问同一可变数据结构时的数据一致性。主要有下面几种锁:
- 阻塞锁(Blocking locks):常见的表现形式是当前线程会进入休眠,直到被其他线程释放。
- 自旋锁(Spinlocks):使用一个循环不断地检查锁是否被释放。如果等待情况很少话这种锁是非常高效的,相反,等待情况非常多的情况下会浪费 CPU 时间。
- (Reader/writer locks):允许多个读线程同时进入一段代码,但当写线程获取锁时,其他线程(包括读取器)只能等待。这是非常有用的,因为大多数数据结构读取时是线程安全的,但当其他线程边读边写时就不安全了。
- 递归锁(Recursive locks):允许单个线程多次获取相同的锁。非递归锁被同一线程重复获取时可能会导致死锁、崩溃或其他错误行为。
APIs
苹果提供了一系列不同的锁 API,下面列出了其中一些:
- pthread_mutex_t
- pthread_rwlock_t
- dispatch_queue_t
- NSOperationQueue 当配置为 serial 时
- NSLock
- OSSpinLock
除此之外,Objective-C 提供了 @synchronized
语法结构,它其实就是封装了 pthread_mutex_t
。与其他 API 不同的是,@synchronized 并未使用专门的锁对象,它可以将任意 Objective-C 对象视为锁。@synchronized(someObject) 区域会阻止其他 @synchronized(someObject) 区域访问同一对象指针。不同的 API 有不同的行为和能力:
- pthread_mutex_t 是一个可选择性地配置为递归锁的阻塞锁;
- pthread_rwlock_t 是一个阻塞读写锁;
- dispatch_queue_t 可以用作阻塞锁,也可以通过使用 barrier block 配置一个同步队列作为读写锁,还支持异步执行加锁代码;
- NSOperationQueue 可以用作阻塞锁。与 dispatch_queue_t 一样,支持异步执行加锁代码。
- NSLock 是 Objective-C 类的阻塞锁,它的同伴类 NSRecursiveLock 是递归锁。
- OSSpinLock 顾名思义,是一个自旋锁。 最后,@synchronized 是一个阻塞递归锁。
OC中的方法锁方法
互斥锁块@synchronized
// 1、创建一个锁,等待块执行完毕
-(vodi)synchronizedMethod {
@synchronized(self) {
//Safe
}
}
复制代码
NSLock对象
// 2、另外的一个方法时使用NSLock对象
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock]
// Safe
[_lock unlock]
}
复制代码
互斥锁pthread_mutex_lock
/// 3、互斥锁 YYCache
pthread_mutex_init(&_lock, NULL);
- (NSUInteger)totalCost {
pthread_mutex_lock(&_lock);
NSUInteger totalCost = _lru->_totalCost;
pthread_mutex_unlock(&_lock);
return totalCost;
}
- (void)dealloc {
...
//销毁互斥锁
pthread_mutex_destroy(&_lock);
}
复制代码
信号量
_lock = dispatch_semaphore_create(1);
复制代码
然后使用了宏来代替加锁解锁的代码:
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
复制代码
使用:
- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}
复制代码
滥用@synchronized
会减低代码效率,极端情况下回出现死锁(deadlock)现象。也可以使用NSRecursiveLock这种递归锁,这样线程多次持有该锁,而不会出现死锁。
虽然加了锁,但是没法保证绝对的线程安全。因为一个线程多次调用获取方法(getter),每次取到的结果未必相同。在两次操作之间,可能会有其他线程写入新的属性值。
队列
把设置操作与获取操作都安排在序列化的队列里面执行,这样针对属性的访问操作就都同步了。
串行同步队列
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//设置字符串
- (void)setSomeString:(NSString*)someString {
// 这里也可以优化成异步派发 dispatch_async
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
复制代码
将同步与异步派发派发结合起来,可以实现与普通加锁机制一样的同步行为,这么做却不会阻塞执行异步派发的的线程
并发队列
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//设置字符串
- (void)setSomeString:(NSString*)someString {
// 栅栏函数
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
复制代码
同步队列及栅栏块,可以零同步行文更加高效
Swift中的应用
同步锁
Swift中没有*@synchronized* ,但是可以使用objc_sync_enter
,这是@synchronized
的底层实现
func synced(_ lock: Any, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
// 使用
synced(self) {
println("This is a synchronized closure")
}
复制代码
也可以这样进行分钟:
// 也可以这样使用
{
objc_sync_enter(lock)
defer { objc_sync_exit(lock) }
//
// code of critical section goes here
//
} // <-- 退出此块时释放锁定
// 如果我们加上异常的话,stackoverflow中有一个我觉得写的比较好的方法:
extension NSObject {
func synchronized<T>(lockObj: AnyObject!, closure: () throws -> T) rethrows -> T
{
objc_sync_enter(lockObj)
defer {
objc_sync_exit(lockObj)
}
return try closure()
}
}
复制代码
使用效果如下:
class Foo: NSObject {
func test() {
print("1")
objc_sync_enter(self)
defer {
objc_sync_exit(self)
print("3")
}
print("2")
}
}
class Foo2: Foo {
override func test() {
super.test()
print("11")
objc_sync_enter(self)
defer {
print("33")
objc_sync_exit(self)
}
print("22")
}
}
let test = Foo2()
test.test()
复制代码
打印结果如下
1
2
3
11
22
33
复制代码
信号量
static private var syncSemaphores: [String: DispatchSemaphore] = [:]
static func synced(_ lock: String, closure: () -> ()) {
//get the semaphore or create it
var semaphore = syncSemaphores[lock]
if semaphore == nil {
semaphore = DispatchSemaphore(value: 1)
syncSemaphores[lock] = semaphore
}
//lock semaphore
semaphore!.wait()
//execute closure
closure()
//unlock semaphore
semaphore!.signal()
}
复制代码
并发队列
方法与OC中是一样的,在取值操作与赋值操作
public class UserData {
private let myPropertyQueue = dispatch_queue_create("com.example.mygreatapp.property", DISPATCH_QUEUE_CONCURRENT)
private var _myProperty = "" // Backing storage
public var myProperty: String {
get {
var result = ""
dispatch_sync(myPropertyQueue) {
result = self._myProperty
}
return result
}
set {
dispatch_barrier_async(myPropertyQueue) {
self._myProperty = newValue
}
}
}
}
复制代码