我的對GCD信號量的一些誤解...

之前認爲信號量的初始值就是線程的最大併發數,不可更改的,其實並否則。swift

平時開發通常都使用GCD信號量(DispatchSemaphore)來解決線程安全問題:當多個線程訪問同一塊資源時,很容易引起數據錯亂和數據安全問題。安全

經典的多線程安全隱患示例 - 賣票

  • 使用信號量以前:
var ticketTotal = 15
let group: DispatchGroup = DispatchGroup()
    
// 賣票操做
func __saleTicket(_ saleCount: Int) {
    DispatchQueue.global().async(group: group, qos: .default, flags: []) {
        for _ in 0..<saleCount {
            // 加個延時能夠大機率讓多條線程同時進行到這一步
            sleep(1) 
            // 賣一張
            self.ticketTotal -= 1 
        }
    }
}

// 開始賣票
func startSaleTicket() {
    print("\(Date()) 一開始總共有\(ticketTotal)張")

    print("\(Date()) 第一次賣5張票")
    __saleTicket(5)

    print("\(Date()) 第二次賣5張票")
    __saleTicket(5)

    print("\(Date()) 第三次賣5張票")
    __saleTicket(5)

    group.notify(queue: .main) {
        print("\(Date()) 理論上所有賣完了,實際上剩\(self.ticketTotal)張")
    }
}
複製代碼

打印結果:多線程

明顯結果是錯的,15張賣了15次卻還剩4張,這是多線程操做引起的數據錯亂問題。

  • 使用信號量對賣票的操做進行加解🔐:
func __saleTicket(_ saleCount: Int) {
    DispatchQueue.global().async(group: group, qos: .default, flags: []) {
        for _ in 0..<saleCount {
            // 加個延時能夠大機率讓多條線程同時進行到這一步
            sleep(1) 
            // 賣一張
            self.semaphore.wait() // 加🔐
            self.ticketTotal -= 1 
            self.semaphore.signal() // 解🔐
        }
    }
}
複製代碼

打印結果:併發

結果正確,多線程操做使用信號量就能夠實現線程同步以保證數據安全了。

對信號量的誤解

之前認爲信號量的初始值是指線程的最大併發數,不可更改的,直到看到其餘文章介紹的一個信號量用法:異步

let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)

func semaphoreTest() {
    DispatchQueue.global().async {

        DispatchQueue.main.async {
            // 從主隊列中獲取一些信息
            ...
            // 發送信號
            self.semaphore.signal()     
        }

        // 開始等待
        self.semaphore.wait() 
        // 等待結束,線程繼續
    }
}
複製代碼

看到這個用法就開始以爲奇怪了,明明初始化爲0,不就是線程最大併發數爲0嗎?不就是不能有線程能夠工做嗎?按道理應該會一直阻塞住這個子線程纔對,那這種用法有什麼意義呢?async

對信號量的從新認識

衆所周知,semaphore.wait()是減1操做,不過這個減1操做的前提是信號量是否大於0:函數

  1. 若是大於0,線程能夠繼續往下跑,而後緊接在semaphore.wait()這句事後,纔會真正對信號量減1;
  2. 若是等於0,就會讓線程休眠,加入到一個都等待這個信號的線程隊列當中,當信號量大於0時,就會喚醒這個等待隊列中靠前的線程,繼續線程後面代碼且對信號量減1,也就確保了信號量大於0才減1,因此不存在信號量小於0的狀況(除非在初始化時設置爲負數,不過這樣作的話當使用時程序就會崩潰)。

semaphore.signal()是對信號量的加1操做,後來通過測試發現,經過semaphore.signal()能夠任意添加信號量,因此初始化的信號量並不是不可更改的,是能夠隨意更改的測試

  • 驗證的🌰:
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // 0

func semaphoreTest() {
    semaphore.signal() // 0 + 1 = 1
    semaphore.signal() // 1 + 1 = 2
    semaphore.signal() // 2 + 1 = 3

    semaphore.wait() // 3 - 1 = 2
    print("\(Date()) \(Thread.current) hello_1")

    semaphore.wait() // 2 - 1 = 1
    print("\(Date()) \(Thread.current) hello_2")

    semaphore.wait() // 1 - 1 = 0
    print("\(Date()) \(Thread.current) hello_2")
    
    // 延遲3秒去另外一個線程異步添加信號量
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
        print("\(Date()) \(Thread.current) 信號量+1")
        let result = self.semaphore.signal() // 0 + 1 = 1
        print("\(Date()) \(Thread.current) result: \(result)");
        /* * PS: signal() 會返回一個結果,文檔解釋爲: * This function returns non-zero if a thread is woken. Otherwise, zero is returned. 若是線程被喚醒,則此函數返回非零。不然,返回零。 * 這裏執行後會有一條線程被喚醒,因此返回1,前面的3次signal()返回的都是0,說明沒有線程被喚醒,不過信號量的確是有+1的。 */
    }

    semaphore.wait() // 等於0就」卡住「當前線程
    print("\(Date()) \(Thread.current) hello_4") // 1 - 1 = 0
}
複製代碼

打印結果:ui

即使信號量初始爲0,也能夠手動添加信號量,因此前3句立刻打印;而最後1句因爲沒有信號了,線程進入休眠沒法執行,而後3秒後在 另外一條線程添加了信號量,這條線程才被喚醒去打印最後一句。

證實了信號量是能夠本身維護的,只是「看不見」(沒有API獲取)。spa

  • 僞代碼解釋前面介紹的用法:
// 初始化信號量爲0,假設 semaphoreCount 是表明信號量的一個數字
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) // semaphoreCount = 0

func semaphoreTest() {
    DispatchQueue.global().async {
        //【1】開始執行任務1

        DispatchQueue.main.async {
            //【4】開始執行任務2
            ...
            //【5】任務2結束,信號量加1,發送信號,喚醒等待靠前的線程
            self.semaphore.signal() // semaphoreCount + 1 = 1 
        }

        //【2】任務1須要等待任務2執行完才繼續,判斷有無信號量
        self.semaphore.wait() //【3】判斷信號量,發現 semaphoreCount == 0,這裏」卡住「(休眠)
        
        //【6】能來到這裏,說明信號量至少爲1,喚醒了這條線程,同時對信號量減1
        // semaphoreCount - 1 = 0

        // 減1後若是等於0,那麼其餘還在等這個信號量的線程只能繼續等,而這條線程會繼續往下執行。 
        //【7】任務1繼續
    }
}
複製代碼

總結

  1. GCD信號量的初始值的確是線程的最大併發數,不過這個併發數不是不能修改的,能夠經過semaphore.signal()任意添加的,至關因而有個隱藏的semaphoreCount來控制能有多少條線程能同時工做;
  2. 這個semaphoreCount至少要有 1 才能夠執行代碼,只要是 0,semaphore.wait()就會讓線程休眠等着直到semaphoreCount大於 0 才喚醒;
  3. 因爲沒有API獲取這個semaphoreCount,因此必定要注意:用過多少次semaphore.wait()就記得也要用多少次semaphore.signal(),保證使用配對,否則線程會永遠休眠。

知道這些後,之後GCD信號量除了能夠加解🔐外,也能夠作到讓當前線程等待別的線程了,也就是說能夠控制線程的執行時機喔~

GCD其它一些須要本身維護配對次數的函數

這些函數也是沒有相應API獲取次數,須要本身維護:

  • 隊列組:group.enter()group.leave()
  • 定時器:timer.resume()timer.suspend()
    • PS:不要在暫停suspend狀態下執行cancel(),不然會崩潰,因此記得在cancel()前肯定 timer 是在運行resume狀態下。
相關文章
相關標籤/搜索