# 同步,第 1 部分:互斥鎖
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-1%3A-Mutex-Locks>
## 解決關鍵部分
## 什么是關鍵部分?
關鍵部分是一段代碼,如果程序要正常運行,一次只能由一個線程執行。如果兩個線程(或進程)同時在臨界區內執行代碼,則程序可能不再具有正確的行為。
## 只是將變量遞增一個關鍵部分?
有可能。遞增變量(`i++`)分三個步驟執行:將存儲器內容復制到 CPU 寄存器。增加 CPU 中的值。將新值存儲在內存中。如果只能通過一個線程訪問存儲器位置(例如下面的自動變量`i`),則不存在競爭條件和沒有與`i`相關的臨界區的可能性。但是,`sum`變量是一個全局變量,可由兩個線程訪問。兩個線程可能會嘗試同時遞增變量。
```c
#include <stdio.h>
#include <pthread.h>
// Compile with -pthread
int sum = 0; //shared
void *countgold(void *param) {
int i; //local to each thread
for (i = 0; i < 10000000; i++) {
sum += 1;
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, countgold, NULL);
pthread_create(&tid2, NULL, countgold, NULL);
//Wait for both threads to finish:
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("ARRRRG sum is %d\n", sum);
return 0;
}
```
上述代碼的典型輸出為`ARGGGH sum is 8140268`每次運行程序時都會打印一個不同的總和,因為存在競爭條件;代碼不會阻止兩個線程同時讀寫`sum`。例如,兩個線程都將 sum 的當前值復制到運行每個線程的 CPU 中(讓我們選擇 123)。兩個線程都將一個增加到它們自己的副本兩個線程都回寫值(124)。如果線程在不同時間訪問了總和,則計數將為 125。
## 如何確保一次只有一個線程可以訪問全局變量?
你的意思是,“幫助 - 我需要一個互斥體!”如果一個線程當前在一個臨界區內,我們希望另一個線程等到第一個線程完成。為此,我們可以使用互斥(互斥的簡稱)。
對于簡單的示例,我們需要添加的最少量代碼只有三行:
```c
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; // global variable
pthread_mutex_lock(&m); // start of Critical Section
pthread_mutex_unlock(&m); //end of Critical Section
```
一旦我們完成了互斥鎖,我們也應該調用`pthread_mutex_destroy(&m)`。請注意,您只能銷毀未鎖定的互斥鎖。在被破壞的鎖上調用 destroy,初始化已初始化的鎖,鎖定已鎖定的鎖,解鎖未鎖定的鎖等都不受支持(至少對于默認的互斥鎖)并且通常會導致未定義的行為。
## 如果我鎖定互斥鎖,是否會阻止所有其他線程?
不,其他線程將繼續。只有在線程試圖鎖定已經鎖定的互斥鎖時,線程才會等待。一旦原始線程解鎖互斥鎖,第二個(等待)線程將獲得鎖定并能夠繼續。
## 還有其他方法可以創建互斥鎖嗎?
是。您只能將宏 PTHREAD_MUTEX_INITIALIZER 用于全局('靜態')變量。 m = PTHREAD_MUTEX_INITIALIZER 等同于更通用的`pthread_mutex_init(&m,NULL)`。 init 版本包括用于交換性能的選項,以進行其他錯誤檢查和高級共享選項。
```c
pthread_mutex_t *lock = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(lock, NULL);
//later
pthread_mutex_destroy(lock);
free(lock);
```
關于`init`和`destroy`需要注意的事項:
* 多線程 init / destroy 具有未定義的行為
* 銷毀鎖定的互斥鎖具有未定義的行為
* 基本上嘗試保持一個線程初始化互斥鎖的模式以及一個且只有一個初始化互斥鎖的線程。
## Mutex Gotchas
## `So pthread_mutex_lock`在讀取同一個變量時會停止其他線程嗎?
沒有。互斥體不是那么聰明 - 它適用于代碼(線程),而不是數據。只有當另一個線程在鎖定的互斥鎖上調用`lock`時,第二個線程才需要等到互斥鎖被解鎖。
考慮
```c
int a;
pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER,
m2 = = PTHREAD_MUTEX_INITIALIZER;
// later
// Thread 1
pthread_mutex_lock(&m1);
a++;
pthread_mutex_unlock(&m1);
// Thread 2
pthread_mutex_lock(&m2);
a++;
pthread_mutex_unlock(&m2);
```
仍會造成競爭條件。
## 我可以在分叉之前創建互斥嗎?
是 - 但是子進程和父進程不共享虛擬內存,并且每個進程都將具有獨立于另一個的互斥鎖。
(高級注釋:使用共享內存的高級選項允許子級和父級共享互斥鎖,如果它使用正確的選項創建并使用共享內存段。請參閱 [stackoverflow 示例](http://stackoverflow.com/questions/19172541/procs-fork-and-mutexes))
## 如果一個線程鎖定互斥鎖,另一個線程可以解鎖嗎?
不可以。同一個線程必須解鎖它。
## 我可以使用兩個或更多互斥鎖嗎?
是!事實上,每個數據結構都需要更新一個鎖。
如果你只有一個鎖,那么它們可能是兩個不必要的線程之間鎖定的重要爭用。例如,如果兩個線程正在更新兩個不同的計數器,則可能沒有必要使用相同的鎖。
然而,簡單地創建許多鎖是不夠的:能夠推斷關鍵部分是很重要的,例如:重要的是,一個線程在更新時暫時處于不一致狀態時無法讀取兩個數據結構。
## 調用鎖定和解鎖是否有任何開銷?
調用`pthread_mutex_lock`和`_unlock`會產生少量開銷;但這是你為正確運行的程序付出的代價!
## 最簡單的完整例子?
完整的示例如下所示
```c
#include <stdio.h>
#include <pthread.h>
// Compile with -pthread
// Create a mutex this ready to be locked!
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
int sum = 0;
void *countgold(void *param) {
int i;
//Same thread that locks the mutex must unlock it
//Critical section is just 'sum += 1'
//However locking and unlocking a million times
//has significant overhead in this simple answer
pthread_mutex_lock(&m);
// Other threads that call lock will have to wait until we call unlock
for (i = 0; i < 10000000; i++) {
sum += 1;
}
pthread_mutex_unlock(&m);
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, countgold, NULL);
pthread_create(&tid2, NULL, countgold, NULL);
//Wait for both threads to finish:
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("ARRRRG sum is %d\n", sum);
return 0;
}
```
在上面的代碼中,線程在進入之前獲得對計數室的鎖定。關鍵部分只是`sum+=1`,因此以下版本也正確但速度較慢 -
```c
for (i = 0; i < 10000000; i++) {
pthread_mutex_lock(&m);
sum += 1;
pthread_mutex_unlock(&m);
}
return NULL;
}
```
這個過程運行得慢,因為我們鎖定和解鎖互斥鎖一百萬次,這是昂貴的 - 至少與增加一個變量相比。 (在這個簡單的例子中,我們并不真正需要線程 - 我們本來可以加兩次!)一個更快的多線程示例是使用自動(本地)變量添加一百萬,然后將其添加到共享總數計算循環結束后:
```c
int local = 0;
for (i = 0; i < 10000000; i++) {
local += 1;
}
pthread_mutex_lock(&m);
sum += local;
pthread_mutex_unlock(&m);
return NULL;
}
```
## 如果我忘記解鎖會怎么樣?
僵局!我們稍后會討論死鎖,但是如果被多個線程調用,這個循環會出現什么問題。
```c
while(not_stop){
//stdin may not be thread safe
pthread_mutex_lock(&m);
char *line = getline(...);
if(rand() % 2) { /* randomly skip lines */
continue;
}
pthread_mutex_unlock(&m);
process_line(line);
}
```
## 我什么時候可以銷毀互斥鎖???
您只能銷毀未鎖定的互斥鎖
## 我可以將 pthread_mutex_t 復制到新的內存位置嗎?
不,將互斥鎖的字節復制到新的存儲位置然后使用副本是 _ 而不是 _ 支持。
## 互斥體的簡單實現是什么樣的?
一個簡單(但不正確!)的建議如下所示。 `unlock`功能只是解鎖互斥鎖并返回。鎖定功能首先檢查鎖定是否已被鎖定。如果它當前被鎖定,它將繼續檢查,直到另一個線程解鎖了互斥鎖。
```c
// Version 1 (Incorrect!)
void lock(mutex_t *m) {
while(m->locked) { /*Locked? Nevermind - just loop and check again!*/ }
m->locked = 1;
}
void unlock(mutex_t *m) {
m->locked = 0;
}
```
版本 1 使用“忙等待”(不必要地浪費 CPU 資源)但是存在更嚴重的問題:我們有競爭條件!
如果兩個線程同時調用`lock`,則兩個線程都可能將'm_locked'讀為零。因此,兩個線程都會相信它們具有對鎖的獨占訪問權,并且兩個線程都將繼續。哎呀!
我們可能會嘗試通過在循環中調用`pthread_yield()`來稍微減少 CPU 開銷 - pthread_yield 建議操作系統線程不會短時間使用 CPU,因此 CPU 可能被分配給等待的線程跑。但不能解決競爭條件。我們需要一個更好的實施 - 你能工作如何防止競爭條件?
## 我怎么知道更多?
[玩!](http://cs-education.github.io/sys) 閱讀手冊頁!
* [pthread_mutex_lock 手冊頁](http://linux.die.net/man/3/pthread_mutex_lock)
* [pthread_mutex_unlock 手冊頁](http://linux.die.net/man/3/pthread_mutex_unlock)
* [pthread_mutex_init 手冊頁](http://linux.die.net/man/3/pthread_mutex_init)
* [pthread_mutex_destroy 手冊頁](http://linux.die.net/man/3/pthread_mutex_destroy)
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話