001@多用派發佇列,少用同步鎖

Zack_Go發表於2018-12-21

多用派發佇列,少用同步鎖

《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
            }
        }
    }
}
複製程式碼

相關文章