# 同步,第 6 部分:实现障碍
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-6%3A-Implementing-a-barrier>
## 在继续下一步之前,如何等待 N 个线程达到某个点?
假设我们想要执行具有两个阶段的多线程计算,但我们不希望在第一阶段完成之前进入第二阶段。
我们可以使用称为 **barrier** 的同步方法。当一个线程到达一个屏障时,它将在屏障处等待,直到所有线程都到达屏障,然后它们将一起进行。
把它想象成和一些朋友一起去远足。你同意在每个山顶上等待对方(并且你会记下你的小组中有多少人)。说你是第一个到达第一座山顶的人。你会在那里等你的朋友。一个接一个,他们将到达顶部,但没有人会继续,直到你的小组中的最后一个人到达。一旦他们这样做,你们都会继续前进。
Pthreads 有一个实现它的函数`pthread_barrier_wait()`。你需要声明一个`pthread_barrier_t`变量并用`pthread_barrier_init()`初始化它。 `pthread_barrier_init()`将参与屏障的线程数作为参数。 [这是一个例子。](https://github.com/angrave/SystemProgramming/wiki/Sample-program-using-pthread-barriers)
现在让我们实现自己的障碍,并使用它来保持所有线程在大型计算中同步。
```c
double data[256][8192]
1 Threads do first calculation (use and change values in data)
2 Barrier! Wait for all threads to finish first calculation before continuing
3 Threads do second calculation (use and change values in data)
```
螺纹功能有四个主要部分 -
```c
void *calc(void *arg) {
/* Do my part of the first calculation */
/* Am I the last thread to finish? If so wake up all the other threads! */
/* Otherwise wait until the other threads has finished part one */
/* Do my part of the second calculation */
}
```
我们的主线程将创建 16 个线程,我们将每个计算分成 16 个单独的部分。每个线程都将被赋予一个唯一值(0,1,2,... 15),因此它可以在自己的块上工作。由于(void *)类型可以包含小整数,我们将通过将它转换为 void 指针来传递`i`的值。
```c
#define N (16)
double data[256][8192] ;
int main() {
pthread_t ids[N];
for(int i = 0; i < N; i++)
pthread_create(&ids[i], NULL, calc, (void *) i);
```
注意,我们永远不会将此指针值取消引用作为实际的内存位置 - 我们只是将其直接转换回整数:
```c
void *calc(void *ptr) {
// Thread 0 will work on rows 0..15, thread 1 on rows 16..31
int x, y, start = N * (int) ptr;
int end = start + N;
for(x = start; x < end; x++) for (y = 0; y < 8192; y++) { /* do calc #1 */ }
```
计算 1 完成后,我们需要等待较慢的线程(除非我们是最后一个线程!)。因此,请跟踪到达我们屏障的线程数量'checkpoint':
```c
// Global:
int remain = N;
// After calc #1 code:
remain--; // We finished
if (remain ==0) {/*I'm last! - Time for everyone to wake up! */ }
else {
while (remain != 0) { /* spin spin spin*/ }
}
```
但是上面的代码有一个竞争条件(两个线程可能会尝试减少`remain`)并且循环是一个繁忙的循环。我们可以做得更好!让我们使用一个条件变量然后我们将使用广播/信号函数来唤醒睡眠线程。
提醒一下,条件变量类似于房子!线程去那里睡觉(`pthread_cond_wait`)。您可以选择唤醒一个线程(`pthread_cond_signal`)或所有线程(`pthread_cond_broadcast`)。如果当前没有线程正在等待,则这两个调用无效。
条件变量版本通常非常类似于忙循环不正确的解决方案 - 我们将在下面展示。首先,让我们添加一个互斥和条件全局变量,不要忘记在`main`中初始化它们......
```c
//global variables
pthread_mutex_t m;
pthread_cond_t cv;
main() {
pthread_mutex_init(&m, NULL);
pthread_cond_init(&cv, NULL);
```
我们将使用互斥锁来确保一次只有一个线程修改`remain`。最后到达的线程需要唤醒 _ 所有 _ 休眠线程 - 所以我们将使用`pthread_cond_broadcast(&cv)`而不是`pthread_cond_signal`
```c
pthread_mutex_lock(&m);
remain--;
if (remain ==0) { pthread_cond_broadcast(&cv); }
else {
while(remain != 0) { pthread_cond_wait(&cv, &m); }
}
pthread_mutex_unlock(&m);
```
当一个线程进入`pthread_cond_wait`时,它会释放互斥锁并休眠。在将来的某个时刻,它会被唤醒。一旦我们从睡眠中恢复一个线程,在返回之前它必须等到它可以锁定互斥锁。请注意,即使睡眠线程提前醒来,它也会检查 while 循环条件并在必要时重新进入等待状态。
**上面的障碍是不可重用的**意味着如果我们将它粘贴到任何旧的计算循环中,那么代码很可能会遇到障碍要么死锁,要么线程在一次迭代中前进更快的情况。考虑如何使上述障碍可重用,这意味着如果多个线程在循环中调用`barrier_wait`,那么可以保证它们在同一个迭代中。
- 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 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话