go語言中如何實現同步操作呢

来源:https://www.cnblogs.com/chenjiazhan/archive/2023/05/27/17436995.html
-Advertisement-
Play Games

## IO流(input/output) ​ 數據運輸的載體或者中間鍵 ### 位元組流 #### 輸入位元組流(FileInputStream) ​ 以位元組為最小單元,讀取任何類型的文件,但是要註意字元集類型的轉換。 ```Java public static void testFileInputSt ...


1. 簡介

本文探討了併發編程中的同步操作,講述了為何需要同步以及兩種常見的實現方式:sync.Cond和通道。通過比較它們的適用場景,讀者可以更好地瞭解何時選擇使用不同的同步方式。本文旨在幫助讀者理解同步操作的重要性以及選擇合適的同步機制來確保多個協程之間的正確協調和數據共用的一致性。

2. 為什麼需要同步操作

2.1 為什麼需要同步操作

這裡舉一個簡單的圖像處理場景來說明。任務A負責載入圖像,任務B負責對已載入的圖像進行處理。這兩個任務將在兩個併發協程中同時啟動,實現並行執行。然而,這兩個任務之間存在一種依賴關係:只有當圖像載入完成後,任務B才能安全地執行圖像處理操作。

在這種情況下,我們需要對這兩個任務進行協調和同步。任務B需要確保在處理已載入的圖像之前,任務A已經完成了圖像載入操作。通過使用適當的同步機制來確保任務B在圖像準備就緒後再進行處理,從而避免數據不一致性和併發訪問錯誤的問題。

事實上,在我們的開發過程中,經常會遇到這種需要同步的場景,所以瞭解同步操作的實現方式是必不可少的,下麵我們來仔細介紹。

2.2 如何實現同步操作呢

通過上面的例子,我們知道當多協程任務存在依賴關係時,同步操作是必不可免的,那如何實現同步操作呢?這裡的一個簡單想法,便是採用一個簡單的條件變數,不斷採用輪詢的方式來檢查事件是否已經發生或條件是否滿足,此時便可實現簡單的同步操作。代碼示例如下:

package main

import (
        "fmt"
        "time"
)

var condition bool

func waitForCondition() {
       for !condition {
             // 輪詢條件是否滿足
             time.Sleep(time.Millisecond * 100)
       }
       fmt.Println("Condition is satisfied")
}

func main() {
        go waitForCondition()

        time.Sleep(time.Second)
        condition = true // 修改條件

        time.Sleep(time.Second)
}

在上述代碼中,waitForCondition 函數通過輪詢方式檢查條件是否滿足。當條件滿足時,才繼續執行下去。

但是這種輪訓的方式其實存在一些缺點,首先是資源浪費,輪詢會消耗大量的 CPU 資源,因為協程需要不斷地執行迴圈來檢查條件。這會導致 CPU 使用率升高,浪費系統資源,其次是延遲,輪詢方式無法及時響應條件的變化。如果條件在迴圈的某個時間點滿足,但輪詢檢查的時機未到,則會延遲對條件的響應。最後輪詢方式可能導致協程的執行效率降低。因為協程需要在迴圈中不斷檢查條件,無法進行其他有意義的工作。

既然通過輪訓一個條件變數來實現同步操作存在這些問題。那go語言中,是否存在更好的實現方式,可以避免輪詢方式帶來的問題,提供更高效、及時響應的同步機制。其實是有的,sync.Condchannel便是兩個可以實現同步操作的原語。

3.實現方式

3.1 sync.Cond實現同步操作

使用sync.Cond實現同步操作的方法,可以參考sync.Cond 這篇文章,也可以按照可以按照以下步驟進行:

  1. 創建一個條件變數:使用sync.NewCond函數創建一個sync.Cond類型的條件變數,並傳入一個互斥鎖作為參數。
  2. 在等待條件滿足的代碼塊中使用Wait方法:在需要等待條件滿足的代碼塊中,調用條件變數的Wait方法,這會使當前協程進入等待狀態,並釋放之前獲取的互斥鎖。
  3. 在滿足條件的代碼塊中使用SignalBroadcast方法:在滿足條件的代碼塊中,可以使用Signal方法來喚醒一個等待的協程,或者使用Broadcast方法來喚醒所有等待的協程。

下麵是一個簡單的例子,演示如何使用sync.Cond實現同步操作:

package main

import (
        "fmt"
        "sync"
        "time"
)

func main() {
        var cond = sync.NewCond(&sync.Mutex{})
        var ready bool

        // 等待條件滿足的協程
        go func() {
                fmt.Println("等待條件滿足...")
                cond.L.Lock()
                for !ready {
                        cond.Wait()
                }
                fmt.Println("條件已滿足")
                cond.L.Unlock()
        }()

        // 模擬一段耗時的操作
        time.Sleep(time.Second)

        // 改變條件並通知等待的協程
        cond.L.Lock()
        ready = true
        cond.Signal()
        cond.L.Unlock()

        // 等待一段時間,以便觀察結果
        time.Sleep(time.Second)
}

在上面的例子中,我們創建了一個條件變數cond,並定義了一個布爾型變數ready作為條件。在等待條件滿足的協程中,通過調用Wait方法等待條件的滿足。在主協程中,通過改變條件並調用Signal方法來通知等待的協程條件已滿足。在等待協程被喚醒後,輸出"條件已滿足"的消息。

通過使用sync.Cond,我們實現了一個簡單的同步操作,確保等待的協程在條件滿足時才會繼續執行。這樣可以避免了不必要的輪詢和資源浪費,提高了程式的效率。

3.2 channel實現同步操作

當使用通道(channel)實現同步操作時,可以利用通道的阻塞特性來實現協程之間的同步。下麵是一個簡單的例子,演示如何使用通道實現同步操作:

package main

import (
        "fmt"
        "time"
)

func main() {
        // 創建一個用於同步的通道
        done := make(chan bool)

        // 在協程中執行需要同步的操作
        go func() {
                fmt.Println("執行一些操作...")
                time.Sleep(time.Second)
                fmt.Println("操作完成")

                // 向通道發送信號,表示操作已完成
                done <- true
        }()

        fmt.Println("等待操作完成...")
        // 阻塞等待通道接收到信號
        <-done
        fmt.Println("操作已完成")
}

在上面的例子中,我們創建了一個通道done,用於同步操作。在執行需要同步的操作的協程中,首先執行一些操作,然後通過向通道發送數據done <- true來表示操作已完成。在主協程中,我們使用<-done來阻塞等待通道接收到信號,表示操作已完成。

通過使用通道實現同步操作,我們利用了通道的阻塞特性,確保在操作完成之前,主協程會一直等待。一旦操作完成並向通道發送了信號,主協程才會繼續執行後續的代碼。基於此實現了同步操作。

3.3 實現方式回顧

從上面的介紹來看,sync.Cond或者channel都可以用來實現同步操作。

但由於它們是不同的併發原語,因此在代碼編寫和理解上可能會有一些差異。條件變數是一種在併發編程中常用的同步機制,而通道則是一種更通用的併發原語,可用於實現更廣泛的通信和同步模式。

在選擇併發原語時,我們應該考慮到代碼的可讀性、可維護性和性能等因素。有時,使用條件變數可能是更合適和直觀的選擇,而在其他情況下,通道可能更適用。瞭解不同併發原語的優勢和限制,並根據具體需求做出適當的選擇,是編寫高質量併發代碼的關鍵。

4. channel適用場景說明

事實上,channel並不是被專門用來實現同步操作,而是基於channel中阻塞等待的特性,從而來實現一些簡單的同步操作。雖然sync.Cond是專門設計來實現同步操作的,但是在某些場景下,使用通道比使用 sync.Cond更為合適。

其中一個最典型的例子,便是任務的有序執行,使用channel,能夠使得任務的同步和順序執行變得更加直觀和可管理。下麵通過一個示例代碼,展示如何使用通道實現任務的有序執行:

package main

import "fmt"

func taskA(waitCh chan<- string, resultCh chan<- string) {
        // 等待開始執行
        <- waitCh
        
        // 執行任務A的邏輯
        // ...
        // 將任務A的結果發送到通道
        resultCh <- "任務A完成"
}

func taskB(waitCh <-chan string, resultCh chan<- string) {
        // 等待開始執行
        resultA := <-waitCh

        // 根據任務A的結果執行任務B的邏輯
        // ...

        // 將任務B的結果發送到通道
        resultCh <- "任務B完成"
}

func taskC(waitCh <-chan string, resultCh chan<- string) {
        // 等待任務B的結果
        resultB := <-waitCh

        // 根據任務B的結果執行任務C的邏輯
        // ...
        resultCh <- "任務C完成"
}

func main() {
        // 創建用於任務之間通信的通道
        beginChannel := make(chan string)
        channelA := make(chan string)
        channelB := make(chan string)
        channelC := make(chan string)
        
        beginChannel <- "begin"
        // 啟動任務A
        go taskA(beginChannel, channelA)

        // 啟動任務B
        go taskB(channelA, channelB)

        // 啟動任務C
        go taskC(channelB,channelC)

        // 阻塞主線程,等待任務C完成
        select {}

        // 註意:上述代碼只是示例,實際情況中可能需要適當地添加同步操作或關閉通道的邏輯
}

在這個例子中,我們啟動了三個任務,並通過通道進行它們之間的通信來保證執行順序。任務A等待beginChannel通道的信號,一旦接收到信號,任務A開始執行並將結果發送到channelA通道。其他任務,比如任務B,等待任務A完成的信號,一旦接收到channelA通道的數據,任務B開始執行。同樣地,任務C等待任務B完成的信號,一旦接收到channelB通道的數據,任務C開始執行。通過這種方式,我們實現了任務之間的有序執行。

相對於使用sync.Cond的實現方式來看,通過使用通道,在任務之間進行有序執行時,代碼通常更加簡潔和易於理解。比如上面的例子,我們可以很清楚得識別出來,任務的執行順序為 任務A ---> 任務B --> 任務C。

其次通道可以輕鬆地添加或刪除任務,並調整它們之間的順序,而無需修改大量的同步代碼。這種靈活性使得代碼更易於維護和演進。也是以上面的代碼例子為例,假如現在需要修改任務的執行順序,將其執行順序修改為 任務A ---> 任務C ---> 任務B,只需要簡單調整下順序即可,具體如下:

func main() {
        // 創建用於任務之間通信的通道
        beginChannel := make(chan string)
        channelA := make(chan string)
        channelB := make(chan string)
        channelC := make(chan string)
        
        beginChannel <- "begin"
        // 啟動任務A
        go taskA(beginChannel, channelA)

        // 啟動任務B
        go taskB(channelC, channelB)

        // 啟動任務C
        go taskC(channelA,channelC)

        // 阻塞主線程,等待任務C完成
        select {}

        // 註意:上述代碼只是示例,實際情況中可能需要適當地添加同步操作或關閉通道的邏輯
}

和之前的唯一區別,只在於任務B傳入的waitCh參數為channelC,任務C傳入的waitCh參數為channelA,做了這麼一個小小的變動,便實現了任務執行順序的調整,非常靈活。

最後,相對於sync.Cond,通道提供了一種安全的機制來實現任務的有序執行。由於通道在發送和接收數據時會進行隱式的同步,因此不會出現數據競爭和併發訪問的問題。這可以避免潛在的錯誤和 bug,並提供更可靠的同步操作。

總的來說,如果是任務之間的簡單協調,比如任務執行順序的協調同步,通過通道來實現是非常合適的。通道提供了簡潔、可靠的機制,使得任務的有序執行變得靈活和易於維護。

5. sync.Cond適用場景說明

在任務之間的簡單協調場景下,使用channel的同步實現,相對於sync.Cond的實現是更為簡潔和易於維護的,但是並非意味著sync.Cond就無用武之地了。在一些相對複雜的同步場景下,sync.Cond相對於channel來說,表達能力是更強的,而且是更為容易理解的。因此,在這些場景下,雖然使用channel也能夠起到同樣的效果,使用sync.Cond可能相對來說也是更為合適的,即使sync.Cond使用起來更為複雜。下麵我們來簡單講述下這些場景。

5.1 精細化條件控制

對於具有複雜的等待條件和需要精細化同步的場景,使用sync.Cond是一個合適的選擇。它提供了更高級別的同步原語,能夠滿足這種特定需求,並且可以確保線程安全和正確的同步行為。

下麵舉一個簡單的例子,有一個主協程負責累加計數器的值,而存在多個等待協程,每個協程都有自己獨特的等待條件。等待協程需要等待計數器達到特定的值才能繼續執行。

對於這種場景,使用sync.Cond來實現是更為合適的選擇。sync.Cond提供了一種基於條件的同步機制,可以方便地實現協程之間的等待和通知。使用sync.Cond,主協程可以通過調用Wait方法等待條件滿足,並通過調用BroadcastSignal方法來通知等待的協程。等待的協程可以在條件滿足時繼續執行任務。

相比之下,使用通道來實現可能會更加複雜和繁瑣。通道主要用於協程之間的通信,並不直接提供條件等待的機制。雖然可以通過在通道中傳遞特定的值來模擬條件等待,但這通常會引入額外的複雜性和可能的競爭條件。因此,在這種情況下,使用sync.Cond更為合適,可以更直接地表達協程之間的條件等待和通知,代碼也更易於理解和維護。下麵來簡單看下使用sync.Cond實現:

package main

import (
        "fmt"
        "sync"
)

var (
        counter int
        cond    *sync.Cond
)

func main() {
        cond = sync.NewCond(&sync.Mutex{})

        // 啟動等待協程
        for i := 0; i < 5; i++ {
                go waitForCondition(i)
        }

        // 模擬累加計數器
        for i := 1; i <= 10; i++ {
                // 加鎖,修改計數器
                cond.L.Lock()
                counter += i
                fmt.Println("Counter:", counter)
          
                cond.L.Unlock()
                
                cond.Broadcast()
        }
}

func waitForCondition(id int) {
        // 加鎖,等待條件滿足
        cond.L.Lock()
        defer cond.L.Unlock()

        // 等待條件滿足
        for counter < id*10 {
             cond.Wait()
        }

        // 執行任務
        fmt.Printf("Goroutine %d: Counter reached %d\n", id, id*10)
}

在上述代碼中,主協程使用sync.CondWait方法等待條件滿足時進行通知,而等待的協程通過檢查條件是否滿足來決定是否繼續執行任務。每個協程執行的計數器值條件都不同,它們會等待主協程累加的計數器值達到預期的條件。一旦條件滿足,等待的協程將執行自己的任務。

通過使用sync.Cond,我們可以實現多個協程之間的同步和條件等待,以滿足不同的執行條件。

因此,對於具有複雜的等待條件和需要精細化同步的場景,使用sync.Cond是一個合適的選擇。它提供了更高級別的同步原語,能夠滿足這種特定需求,並且可以確保線程安全和正確的同步行為。

5.2 需要反覆喚醒所有等待協程

這裡還是以上面的例子來簡單說明,主協程負責累加計數器的值,並且有多個等待協程,每個協程都有自己獨特的等待條件。這些等待協程需要等待計數器達到特定的值才能繼續執行。在這種情況下,每當主協程對計數器進行累加時,由於無法確定哪些協程滿足執行條件,需要喚醒所有等待的協程。這樣,所有的協程才能判斷是否滿足執行條件。如果只喚醒一個等待協程,那麼可能會導致另一個滿足執行條件的協程永遠不會被喚醒。

因此,在這種場景下,每當計數器累加一個值時,都需要喚醒所有等待的協程,以避免某個協程永遠不會被喚醒。這種需要重覆調用Broadcast的場景並不適合使用通道來實現,而是最適合使用sync.Cond來實現同步操作。

通過使用sync.Cond,我們可以創建一個條件變數,協程可以使用Wait方法等待特定的條件出現。當主協程累加計數器並滿足等待條件時,它可以調用Broadcast方法喚醒所有等待的協程。這樣,所有滿足條件的協程都有機會繼續執行。

因此,在這種需要重覆調用Broadcast的同步場景中,使用sync.Cond是最為合適的選擇。它提供了靈活的條件等待和喚醒機制,確保所有滿足條件的協程都能得到執行的機會,從而實現正確的同步操作。

6. 總結

同步操作在併發編程中起著關鍵的作用,用於確保協程之間的正確協調和共用數據的一致性。在選擇同步操作的實現方式時,我們有兩個常見選項:使用sync.Cond和通道。

使用sync.Cond和通道的方式提供了更高級、更靈活的同步機制。sync.Cond允許協程等待特定條件的出現,通過WaitSignalBroadcast方法的組合,可以實現複雜的同步需求。通道則提供了直接的通信機制,通過發送和接收操作進行隱式的同步,避免了數據競爭和併發訪問錯誤。

選擇適當的同步操作實現方式需要考慮具體的應用場景。對於簡單的同步需求,可以使用通道方式。對於複雜的同步需求,涉及共用數據的操作,使用sync.Cond和可以提供更好的靈活性和安全性。

通過瞭解不同實現方式的特點和適用場景,可以根據具體需求選擇最合適的同步機制,確保併發程式的正確性和性能。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 欄位類型 | 數據類型 | 位元組 | 範圍 | | | | | |TINYINT|1 位元組|-2^7 + 1 ~ 2^7 - 1| |SMALLINT|2 位元組|-2^15 + 1 ~ 2^15 - 1| |INT|4 位元組|-2^31 + 1 ~ 2^31 - 1| |BIGINT|8 位元組| ...
  • 摘要:條件表達式函數中出現結果集不一致問題,我們首先要考慮是否入參數據類型不一致導致出參不一致。 本文分享自華為雲社區《GaussDB(DWS)條件表達式函數返回錯誤結果集排查》,作者:yd_211369925 。 (一)案例背景 客戶使用greatest獲取並返回參數列表中值最大的表達式的值,子查 ...
  • 好久沒更新博客了,因為工作越來越忙,沒什麼時間去記錄一些問題,最近閑下來一點,由於某些原因不得不暫時在Windows下做開發,項目用到了node-canvas處理圖片什麼的,在安裝的時候各種報錯,確實讓人很抓狂,這裡簡單記錄下: 首先說明下,node-canvas的 官方git倉庫 https:// ...
  • # Web 前端常用正則校驗規則 作為 Web 前端開發,常用的正則校驗規則有很多。下麵是一些常見的示例: ## 1. 校驗手機號碼 手機號碼的正則表達式可以根據不同國家和地區的手機號碼格式進行調整。以下是中國大陸的手機號碼正則表達式: ```javascript const regex = /^1 ...
  • 如果你有 *n* 個緩存伺服器,一個常見的負載均衡方式是使用以下的哈希方法: *伺服器索引 = 哈希(鍵) % N*,其中 *N* 是伺服器池的大小。 讓我們通過一個例子來說明這是如何工作的。如表5-1所示,我們有4台伺服器和8個字元串鍵及其哈希值。 ![image-2023052022160981 ...
  • 設計一個業務改動信息時的自定義記錄,例如新增、修改、刪除數據等。並且記錄的規則可以通過配置的方式控制。大家需要根據各自業務場景參考,歡迎討論。偽代碼如下: 實體類: @TableName("tbl_user") User{ String id String name Integer age Stri ...
  • > 本文首發於公眾號:Hunter後端 > 原文鏈接:[Python連接es筆記一之連接與查詢es](https://mp.weixin.qq.com/s/smp3VvWD6ChuFVuotQ9_zg) 有幾種方式在 Python 中配置與 es 的連接,最簡單最有用的方法就是定義一個預設的連接,如 ...
  • 在Python軟體開發中,tkinter中command功能的作用是為按鈕、菜單等組件綁定回調函數,用戶操作該組件時會觸發相應的函數執行。 本文涵蓋了各種組件和功能: 1、為Button組件(按鈕)綁定回調函數 import tkinter as tk def say_hello(): print( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...