【问题标题】:Executing threads taking turns轮流执行线程
【发布时间】:2021-12-23 08:14:03
【问题描述】:

我有一个函数,它只打印调用它的线程 ID。我想要 2 个线程调用这个函数 轮流 n 次。我已经用条件变量在 pthreads 中实现了这个功能,但它太冗长了。我希望程序打印如下:

id: 0
id: 1
id: 0
id: 1
id: 0
id: 1
...

最后,“id:0”和“id:1”应该打印n次。

执行此 OpenMP 的惯用方式是什么?

【问题讨论】:

  • 一个有趣的思考问题,但需要注意的是,像这样对线程进行排序是对线程的毫无意义的使用。听起来你的条件变量技巧是一个很好的技巧。也许您应该将其展示给我们,以便我们为您找到简化它的方法。也有可能您认为“过于冗长”的内容已达到预期效果。
  • 其实这个问题对我来说不是很清楚,minimal reproducible example 会很好
  • 虽然这是一个非常有趣的谜题,但它确实有点像 X/Y 问题。交替线程听起来像是您选择解决一些我们不知道的同步挑战的方式,但可能有更好的方式来解决这个潜在目标。可以分享更多上下文吗?
  • 我目前正在构建一个基准,用于测量从 2 个不同线程修改变量时的延迟,每个线程轮流,总共 n 次。在基准测试的每次运行中,线程都绑定到不同的核心对,我正在比较所有运行的延迟。
  • @AydinÖzcan 谢谢你。如果我正确理解你想要做什么,我的直觉是同步机制很容易主导基准测试,这可能是少数几个疯狂的自旋锁会给出更准确结果的情况之一。也许像gcc.godbolt.org/z/3Pjhhd3e8

标签: c++ c parallel-processing openmp


【解决方案1】:

您可以根据您的迭代计数检查线程数并使用屏障实现切换。

#include <omp.h>

#include <cstdio>


int main()
{
  /* note that it is not pragma omp parallel for, just a parallel block */
# pragma omp parallel num_threads(2)
  for(int i = 0; i < 10; ++i) {
    if((i & 1) == (omp_get_thread_num() & 1))
      std::printf("%d: thread %d\n", i, omp_get_thread_num());
#   pragma omp barrier
  }
}

【讨论】:

  • 我对 OpenMP 不是很精通,所以我可能有点离谱,但是:我对 barrier 的理解是,它所做的只是让两个线程在它们相互等待时相互等待达到这一点,但这并不能阻止它们同时运行,这正是 OP 想要的。我在这里错过了什么?
  • @Frank 因为有线程号检查,所以总是有一个线程直接去barrier
【解决方案2】:

既然 OP 已经提到这是为了对跨核延迟进行基准测试,那么至少应该考虑一个纯粹基于 std::atomic 的解决方案。

使用C++20的wait(),我们可以做到以下几点:

#include <thread>
#include <atomic>
#include <iostream>

std::atomic<int> target(1);

void worker(int wid, int n) {
  int v = wid;

  for (int i = 0; i < n; ++i) {    
    target.wait(v);       // wait until target has a different value than v

    // Do stuff...    
    // std::cout << wid;

    v = target.load() + 1;
    target.store(v);      // increment target
    target.notify_one();  // Wakeup the other thread.
  }
}

int main() {
  int n = 100;

  std::jthread t0(worker, 0, n);
  std::jthread t1(worker, 1, n);
}

诚然,在引导过程中,这有点随心所欲。 这是发生了什么:

  • t0 的第一个 wait() 将立即通过,因为 0 != 1,并且没有什么可以提前更改 target
  • t1 的第一个 wait(),直到 t0 移动 target 远离其初始值:1。
  • 如果t0t1 到达它的wait() 之前执行它的notify_one(),那么t1 的等待不会阻塞,因为到那时target 已经是2。
  • 此后,两个线程始终wait() 使值从它们设置的值更改,这将导致它们交替。

显然,这只适用于 2 个线程。

【讨论】:

  • 如果我选择使用自旋锁而不是 std::atomic 构造来实现基准测试,性能会如何变化?你有什么猜测吗?
  • @AydinÖzcan 自旋锁仍然需要使用原子结构(尽管略有不同)。它使性能有多少不同将取决于很多。通常,自旋锁会浪费 CPU 以获得更低的延迟,因此您可以可能获得更快的跨线程反应时间,但会降低程序范围的性能。这与std::atomic::wait() 相比如何高度依赖于实现。实现者很可能会选择做一个自旋锁,在一个小的超时后衰减成一个操作系统锁。 (我当然会这样做)
  • 我会尝试 pthread_spinlock(3) 来观察差异。顺便说一句,这有点离题,但是,标记我在基准“内联”中调用的函数以避免所有可能的抖动是否重要?我的目标是丢弃所有可能的抖动,自旋锁定肯定是朝着正确方向迈出的一步,我正在寻找我。我们也可以在这种情况下使用 std::atomic_wait() 吗?它会有什么不同,你有经验吗?我也可以考虑seqlock。谢谢!
  • @AydinÖzcan 要消除抖动,您必须: a) 使用实时操作系统 b) 不使用任何系统调用,这通常会排除使用大多数操作系统提供的锁。不过,使用std::atomic 编写自己的自旋锁很容易。
猜你喜欢
  • 2013-12-14
  • 1970-01-01
  • 2020-07-26
  • 1970-01-01
  • 2015-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多