# 同步,第 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 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话