# 同步，第 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`，那么可以保证它们在同一个迭代中。