Go併發:訪問共享資料
竟險
竟險(競爭條件、Race Condition)是指多個協程(goroutine)同時訪問共享資料,其結果取決於指令執行順序的情況。
考慮如下售票程式。該程式模擬兩個售票視窗,一個執行購票,一個執行退票。
package main
import (
"fmt"
"time"
)
var tickCount = 200 // 總票數
// 購票
func buy() {
tickCount = tickCount - 1 // A
}
// 退票
func refund() {
tickCount = tickCount + 1 // B
}
func main() {
go buy() // 購票協程
go refund() // 退票協程
time.Sleep(time.Second * 1) // 為了簡單,這裡睡眠1秒,等待上面兩個協程結束
fmt.Println("tick count:", tickCount) // 輸出結果是什麼?
}
考慮到一共200張票,買了一張,賣了一張,應該還是剩餘200張票。事實卻不總是這樣,多次執行程式發現有如下輸出:
tick count: 201
和如下輸出:
tick count: 199
多的一張和少的一張到哪裡去了?無論是先執行買票(語句A)還是賣票(語句B),結果都應該是200才對啊!
NO!!!
在計算機看來語句A和語句B並不是一條不可分割的語句,而是兩條語句:
A1: … = tickCount - 1
A2: tickCount = …
B1: … = tickCount + 1
B2: tickCount = …
它們的實際執行順序有如下四種可能:
- A1->A2->B1->B2 結果為200
- B1->B2->A1->A2 結果為200
- B1->A1->A2->B2 結果為201
- A1->B1->B2->A2 結果為199
可見第三種和第四種執行順序產生了意想不到的結果。原因在於兩個協程同時訪問並修改了共享變數(tickCount),而語句之間的順序無法保證,導致意外的情況發生,這便是竟險。
竟險顯然不是我們想要的結果。那麼如何規避竟險呢?有三種方式:1. 禁止修改共享變數。2. 限制在同一個協程中訪問共享變數。3. 利用互斥。下面分別來看看這三種方式。
禁止修改共享變數
可以通過禁止修改共享變數來達到規避竟險的目的。
考慮如下程式:
package main
var config = map[string]string{}
func loadConfig(key string) string { /*...*/ }
// 惰性載入
func getConfig(key string) string {
value, ok := config[key]
if !ok {
value = loadConfig(key)
config[key] = value
}
return value
}
func main() {
go func() {
user := getConfig("userName") // A 修改共享變數的值,發生竟險
// ...
}()
go func() {
address := getConfig("address") // B 修改共享變數的值,發生竟險
// ...
}()
// ...
}
注意該例中getConfig()為惰性載入,也就是在需要載入時再載入,這樣便在語句A和語句B中發生了竟險,兩條語句同時修改了共享變數config。如果修改為提前載入所有配置,則可規避竟險:
package main
// 提前載入所有配置
var config = map[string]string{
"userName": loadConfig("userName"),
"address": loadConfig("address"),
}
func loadConfig(key string) string { /*...*/ }
func getConfig(key string) string {
return config[key]
}
func main() {
go func() {
user := getConfig("userName") // 訪問共享變數,但不修改其值,不發生竟險
// ...
}()
go func() {
address := getConfig("address") // 訪問共享變數,但不修改其值,不發生竟險
// ...
}()
// ...
}
這種方式僅僅可以用於協程不需要修改共享變數的情況。這顯然滿足不了我們的所有需求。在很多情況下協程必須修改共享變數。
限制在同一個協程中訪問共享變數
將共享變數的訪問限制在一個協程中,就避免了竟險。 不過這種方式略微有些複雜,必須要建立一個監聽執行緒,來專門處理共享變數的修改。
package main
import (
"fmt"
"sync"
)
var tickCount = 200 // 總票數
var ch = make(chan int, 10) // 用來控制tickCount的同步,10表示模擬10個售/退票視窗
var n sync.WaitGroup // 用來等待購票和售票動作完成
var done = make(chan struct{}) // 用來等待監聽協程退出
// 購票
func buy() {
ch <- -1
}
// 退票
func refund() {
ch <- 1
}
func main() {
// 監聽協程
go func() {
for amount := range ch {
tickCount += amount
n.Done() // 每次呼叫Done(),n的計數減1
}
done <- struct{}{} // 監聽執行緒結束,傳送訊息
}()
n.Add(2) // 因為要執行兩個動作,所以使n的計數加2
go buy() // 購票協程
go refund() // 退票協程
n.Wait() // 等待購票和退票動作完成
// Wait()會一直等待,直到n的計數為0
close(ch) // 關閉管道
<-done // 等待監聽執行緒結束
fmt.Println("tick count:", tickCount)
}
這種方式類似於在Windows的執行緒中處理訊息迴圈,用PostThreadMessage來發送訊息。可以對比一下:
#include <Windows.h>
#include <iostream>
#define WM_AMOUNT (WM_USER + 1)
#define WM_DONE (WM_USER + 2)
int tickCount = 200; // 總票數200
DWORD dwThreadId = 0; // 監聽執行緒ID
HANDLE hEventState = NULL; // 用於同步監聽執行緒的開始和結束
// 監聽執行緒
DWORD WINAPI Monitor(LPVOID lpParameter) {
// 確保建立訊息迴圈
MSG msg = {};
PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
// 通知主執行緒訊息迴圈建立完成
SetEvent(hEventState);
// 處理訊息
BOOL bRet;
bool done = false;
while (!done && (bRet = GetMessage(&msg, NULL, 0, 0)) != 0) {
if (bRet == -1) {
break;
} else {
switch (msg.message)
{
case WM_AMOUNT:
tickCount += (int)msg.wParam;
break;
case WM_DONE:
done = true;
break;
default:
break;
}
}
}
SetEvent(hEventState);
return 0;
}
// 購票
void buy() {
PostThreadMessage(dwThreadId, WM_AMOUNT, (WPARAM)-1, NULL);
}
// 退票
void refund() {
PostThreadMessage(dwThreadId, WM_AMOUNT, (WPARAM)1, NULL);
}
int main() {
hEventState = CreateEvent(NULL, FALSE, FALSE, NULL); // 建立同步監聽執行緒的事件
// 開啟監聽執行緒
HANDLE hThread = CreateThread(NULL, 0, Monitor, NULL, 0, &dwThreadId);
CloseHandle(hThread);
WaitForSingleObject(hEventState, INFINITE); // 確保監聽執行緒已開啟
buy(); // 購票
refund(); // 退票
PostThreadMessage(dwThreadId, WM_DONE, NULL, NULL); // 退出監聽執行緒
WaitForSingleObject(hEventState, INFINITE); // 等待監聽執行緒退出
CloseHandle(hEventState); // 關閉同步監聽執行緒的事件
std::cout << "tick count: " << tickCount << std::endl;
}
可見C++的版本複雜了一些,不過也差不了多少。
利用互斥
第三種方式是使用sync包中提供的互斥鎖sync.Mutex。sync.Mutex是一個結構體,提供了Lock和Unlock兩個方法,Lock用來鎖定,Unlock用來解鎖。 利用互斥鎖,上面的程式變得更簡單了:
package main
import (
"fmt"
"sync"
)
var (
tickCount = 200 // 總票數
mu sync.Mutex // 互斥鎖
n sync.WaitGroup
)
// 購票
func buy() {
defer n.Done() // 計數減1
mu.Lock()
defer mu.Unlock() // 用defer保證函式返回時解鎖
tickCount += 1
}
// 退票
func refund() {
defer n.Done() // 計數減1
mu.Lock()
defer mu.Unlock() // 用defer保證函式返回時解鎖
tickCount -= 1
}
func main() {
n.Add(2) // 有兩個動作,所以計數加2
go buy() // 購票協程
go refund() // 退票協程
n.Wait() // 等待購票和退票動作完成
// Wait一直阻塞,直到n的計數為0返回
fmt.Println("tick count:", tickCount)
}
總結
- 多個協程同時訪問共享資料時會造成竟險。
- 可以有三種方式規避竟險:禁止修改共享變數、限制在同一個協程中訪問共享變數、 利用互斥物件。
相關推薦
Go併發:訪問共享資料
竟險 竟險(競爭條件、Race Condition)是指多個協程(goroutine)同時訪問共享資料,其結果取決於指令執行順序的情況。 考慮如下售票程式。該程式模擬兩個售票視窗,一個執行購票,一個執行退票。 package main import
windows清除訪問共享資料夾的登陸資訊
https://jingyan.baidu.com/article/c843ea0b70797e77931e4a96.html 當在命令提示視窗輸入net use命令時,會顯示本機快取的共享登入資訊,如果你想切換使用者,則可以刪除那條快取的記錄即可。 舉個例子,如上圖顯示,
windows使用ipv6地址訪問共享資料夾的方法
在IPV4的網路中,通常我們都是在開始-執行裡面輸入\\ip地址的方式來訪問檔案共享,但是這種方法在ipv6中的網路是行不通的,那麼ipv6如何訪問我們的windows共享呢?方法如下: 在開始-執行,輸入\\xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx.ipv6-lite
Windows7無法訪問共享資料夾(0x800704cf,0x80070035)解決方法
Windows7系統突然無法訪問Linux的samba伺服器,出現0x800704cf或者0x80070035錯誤,也不能訪問其他windows機器的共享資料夾,解決方案如下兩張圖,配置與下面兩張圖為
多執行緒訪問共享資料(1)
多執行緒訪問共享資料解決方案: 一,什麼是多執行緒 執行緒是程式中一個單一的順序控制流程.在單個程式中同時執行多個執行緒完成不同的工作,稱為多執行緒. 所有的執行緒雖然在微觀上是序列執行的,但是在巨集觀上你完全可以認為它們在並行執行 二,多執行緒訪問共享資料解決方
切換使用者賬戶訪問共享資料夾
問題描述 如題,區域網內訪問一臺機器的共享檔案時,第一次會彈提示框輸入使用者名稱和密碼,再次訪問這臺機器的時候共享直接就打開了;如果想用另外一個帳號重新訪問這個共享資料夾的話,該怎麼辦。
如何更換使用者名稱登入訪問共享資料夾
windows共享資料夾設定了局域網資料夾共享,我設定了2個使用者專門用來共享訪問,一個使用者只擁有對資料夾的只讀訪問許可權,另外一個使用者則對資料夾擁有完全許可權。 但是用其中一個使用者登陸之後,要用另外一個使用者名稱來進行登陸更換不了。 對於檔案更換使用者名稱進行登
Ubuntu16.04:nfs共享資料夾
背景 nfs服務是實現Linux和Linux之間的檔案共享,nfs服務的搭建比較簡單。現在介紹如何在ubuntu16.04系統中搭建nfs服務。 1、安裝nfs服務 sudo apt install nfs-common 2、修改配置檔案
tomcat不能訪問共享資料夾
今同事測試一個程式,原先很正常的程卻不能用了,出問題的地方是,程式中有一個地方需要訪問網路共離的資料夾,先前是可以訪問,後來居然死活不能訪問到檔案,在進行檔案訪問的時候程式一直報檔案不存在的異常,磁碟上檔案明明是存在的
如何不切換windows登陸使用者,更換使用者名稱訪問共享資料夾
訪問共享檔案時,第一次會提示輸入使用者名稱和密碼,以後再訪問時就不需要了。 當我們需要更換使用者名稱登陸時,怎麼辦呢?登出也許是一種辦法,那麼有沒有不登出就可以的簡單些的辦法呢? 使用以下命令可以做到: 1.刪除預設使用者名稱登陸: net use \\ip或計算
VirtualBox中的Ubuntu沒有許可權訪問共享資料夾/media/sf_bak
之前已經搞定可以自動共享檔案夾了,但是現在發現無法去訪問,非root使用者下,使用“ls /media/sf_bak”提示沒有許可權,當然如果切換到root,是可以的。 【解決過程】 1、把普通使用者名稱加入到vboxsf之中。因為你的使用者名稱不在vboxsf這個使用者組
win10不能訪問共享資料夾
最近訪問公司的共享資料夾一直受到限制無法訪問,總是彈出“指定的登入會話不存在 可能已被終止”提示框,不給我訪問,然後就想什麼原因造成的:別的電腦win7的都能訪問,就我的不行,排除共享電腦的共享許可權設定問題;上網搜說PIN碼造成的,但是我根本沒設定PIN碼,排除;然後搜
Go併發:利用sync.WaitGroup實現協程同步
經常看到有人會問如何等待主協程中建立的協程執行完畢之後再結束主協程,例如如下程式碼: package main import ( "fmt" ) func main() { go func() { fmt.Println
Java併發庫(五、六、七):執行緒範圍內共享資料、ThreadLocal、共享資料的三種方法
深切懷念傳智播客張孝祥老師,特將其代表作——Java併發庫視訊研讀兩遍,受益頗豐,記以後閱 05. 執行緒範圍內共享變數的概念與作用 執行緒範圍內共享資料圖解: 程式碼演示: class ThreadScopeShareData { 三個模組共享資料,主執
併發衝突控制與資料共享[原文發表時間:2005年3月19日]
Ada95和Java在這方面已經取得了一定進展;同樣,一些研究性語言,比如µC++,還有OpenMP(我覺得它很象工廠裡用的傳送帶,雖然結實,但實在醜陋)也是如此——但無一例外地,它們都只是做了一些改進工作而已,基本的東西並沒有變化,特別是仍然依賴於資料共享和資源鎖定。另外一些研究型語言(如Polyphoni
Java多執行緒/併發05、synchronized應用例項:執行緒間操作共享資料
電商平臺中最重要的一點就是賣東西。同個商品不能無限制的賣下去的,因為商品有庫存量,超過庫存就不能賣了。 這裡,約定一個規則,下單使庫存減n,取消訂單使庫存加m。庫存數量不可以小於0。 假設平臺上同時有很多使用者在操作,在不考慮效率的情況下,我們用同步方法來模
vsftpd實例:匿名訪問共享+系統用戶訪問控制
vsftp 匿名訪問 ftp server 實名訪問 FTP環境實例: 某公司由於業務發展需求,現需要在公司內部搭建一臺FTP服務器!該公司有數個部門(IT FD HR)和N名員工(fus1 fus2 fus3 fus4 fus5 fus6 fus7 fus8 fus9)使用該服務器!為了保障
多線程一共就倆問題:1.線程安全(訪問共享數據) 2.線程通信(wait(),notify())
class 共享 問題 無法 not 安全 pos 三方 gpo 多線程一共就倆問題:1.線程安全(訪問共享數據) 2.線程通信(wait(),notify()) 1.線程安全,無非就是加鎖,訪問共享資源時,synchronized 2.線程通信,就是控制各個線程之間的
Samba共享服務:匿名共享、身份驗證、賬戶映射、訪問控制
water mdb linux png ado adf sta 備份 bios 實驗項目:Samba服務匿名共享;Samba服務身份驗證共享;Samba服務賬戶映射。Samba服務訪問控制 實驗環境:VMware虛擬機Linux系統(我這裏是Redhat6.5)Win7(這
Go併發模式:管道和取消
WHY? Go的併發原語可以輕鬆構建流資料流水線,從而有效利用I/O和多個CPU。 WHAT? 管道是一種資料結構,傳送方可以以字元流形式將資料送入該結構,接收方可以從該結構接收資料。 HOW? Go中沒有正式的管道定義;但它是眾多併發程式中的一種,是通過通