Skip to content

Latest commit

 

History

History
103 lines (74 loc) · 5.01 KB

36.md

File metadata and controls

103 lines (74 loc) · 5.01 KB

同步,第 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()将参与屏障的线程数作为参数。 这是一个例子。

现在让我们实现自己的障碍,并使用它来保持所有线程在大型计算中同步。

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)

螺纹功能有四个主要部分 -

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的值。

#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);

注意,我们永远不会将此指针值取消引用作为实际的内存位置 - 我们只是将其直接转换回整数:

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':

// 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中初始化它们......

//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

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