【问题标题】:How to synchronize instances of a function running on different threads (in c++11)?如何同步在不同线程上运行的函数的实例(在 C++11 中)?
【发布时间】:2016-11-15 17:49:32
【问题描述】:

假设有许多线程由一个循环运行相同函数的实例组成,但每次迭代的开始都需要同步(因此首先完成的线程必须等待最后一个开始新的迭代)。在 c++11 中如何做到这一点?

...

帖子的其余部分只是我尝试过的以及失败的原因。

我正在使用计数器“sync”,最初设置为 3(线程数)。每个线程在函数结束时都会从这个计数器中减去 1 并开始等待。当计数器为 0 时,表示它们 3 已经完成了一轮,所以主线程会将计数器重置为 3,并通知线程唤醒它们。

这在大多数情况下都有效,但有时一两个线程无法唤醒。

所以这些是全局变量:

mutex syncMutex;
condition_variable syncCV;
int sync;

这是在线程中循环运行的函数的末尾:

unique_lock<mutex> lk(syncMutex);
cout << "Thread num: " << mFieldNum << " got sync value: " << sync;
sync --;
syncCV.notify_all();
cout << " and goes to sleep..." << endl;
syncCV.wait(lk, []{return sync == numFields;});
cout << "Thread num: " << mFieldNum << " woke up" << endl;
}

这在主线程中循环运行:

unique_lock<mutex> lk(syncMutex);
syncCV.wait(lk, []{return sync == 0;});
sync = 3;
lk.unlock();
cout << "Notifying all threads!" << endl;
syncCV.notify_all();

这是它失败时产生的输出(线程#3 没有唤醒):

Thread num: 1 got sync value: 3 and goes to sleep...
Thread num: 2 got sync value: 2 and goes to sleep...
Thread num: 3 got sync value: 1 and goes to sleep...
Notifying all threads!
Thread num: 1 woke up
Thread num: 2 woke up
Thread num: 3 woke up
Thread num: 2 got sync value: 3 and goes to sleep...
Thread num: 1 got sync value: 2 and goes to sleep...
Thread num: 3 got sync value: 1 and goes to sleep...
Notifying all threads!
Thread num: 2 woke up
Thread num: 1 woke up
Thread num: 2 got sync value: 3 and goes to sleep...
Thread num: 1 got sync value: 2 and goes to sleep...

有人知道吗?感谢您的阅读。

【问题讨论】:

  • 由于每个线程都在一个循环中运行,所以在线程 1 或 2 唤醒后,sync-- 被执行,在线程 3 []{return sync == numFields;} 谓词执行之前。谓词被评估为假,因此线程 3 没有唤醒。
  • 谢谢@TonyJ 你知道如何解决这个问题吗?

标签: c++ multithreading synchronization mutex condition-variable


【解决方案1】:

您的线程同步存在许多问题。托尼在他的评论中提到了一个。在调用 syncCV.notify_all() 之前调用 lk.unlock() 的主循环代码中也有潜在的竞争条件。 (这可能允许线程错过 notify_all 信号。)

我会以两种方式调整您的代码。首先,为了解决使用“sync == numFields”作为您的条件,正如 Tony 指出的那样,在另一个线程执行同步后可能无法为真,使用每个线程仅运行作为您的条件是有意义的每个主线程循环一次。在我的示例代码中,这是通过引入“done[numFields]”变量来实现的。其次,引入两个条件变量是有意义的——一个向工作线程发出新的主循环迭代已经开始的信号,第二个向主线程发出工作线程完成的信号。 (请注意,这两个条件变量使用相同的互斥锁。)

这是一个完整的程序,以您的示例代码为模型,包含这两种方法:

#include <iostream>
using std::cout;
using std::endl;

#include <condition_variable>
#include <mutex>
#include <thread>
#include <vector>

std::mutex syncMutex;
std::condition_variable readyCV;
std::condition_variable doneCV;
int sync;
bool exitFlag;

const int numFields = 5;
bool done[numFields];

const int nloops = 10;

void thread_func(int i) {
  int mFieldNum = i;
  while (true) {
    std::unique_lock<std::mutex> lk(syncMutex);
    readyCV.wait(lk, [mFieldNum]{return  exitFlag || !done[mFieldNum-1];});
    if (exitFlag)  break;
    cout << "Thread num: " << mFieldNum << " woke up, got sync value: " << sync;
    if (--sync == 0)  doneCV.notify_all();
    done[mFieldNum-1] = true;
    readyCV.notify_all();
    cout << " and goes to sleep..." << endl;
  }
}

int main (int argc, char* argv[]) {
  exitFlag = false;
  sync = 0;
  std::vector<std::thread> threads;
  for (int i = 0; i < numFields; i++) {
    done[i] = true;
    threads.emplace_back (thread_func, i+1);
  }
  for (int i = 0; i <= nloops; i++) {
    std::unique_lock<std::mutex> lk(syncMutex);
    doneCV.wait(lk, []{return sync == 0;});
    cout << "main loop (lk held), i = " << i << endl;
    sync = numFields;
    if (i == nloops)  exitFlag = true;
    else              for (auto &b : done)  b = false;
    cout << "Notifying all threads!" << endl;
    readyCV.notify_all();
  }

  for (auto& t : threads)  t.join();
}

(我还添加了一个 exitFlag 和 std::thread::join(),因此程序可以很好地清理和终止。)

这与经典的生产者-消费者实现非常相似(一个生产者,numFields 个消费者),增加了每个消费者线程在每个生产者线程循环中只能运行一次的约束。

如果您愿意放弃重用工作线程,您还可以更简单地实现基本相同的程序逻辑。 (在您的示例代码和我上面的示例中,它们充当一种专门的线程池。)在我的下一个示例中,为主循环的每次迭代创建新线程。这使得线程同步更简单,并且消除了条件变量。

#include <iostream>
using std::cout;
using std::endl;

#include <atomic>
#include <mutex>
#include <thread>
#include <vector>

std::mutex coutMutex;

std::atomic<int> sync;

const int numFields = 5;
bool done[numFields];

const int nloops = 10;

void thread_func(int i) {
  int mFieldNum = i;
  int mySync = sync--;
  {
    std::lock_guard<std::mutex> lk(coutMutex);
    cout << "Thread num: " << mFieldNum << " woke up, got sync value: " << mySync << endl;
  }
}  

int main (int argc, char* argv[]) {
  for (int i = 0; i < nloops; i++) {
    cout << "main loop, i = " << i << endl;
    std::vector<std::thread> threads;
    sync = numFields;
    for (int i = 0; i < numFields; i++)  threads.emplace_back (thread_func, i+1);
    for (auto& t : threads)  t.join();
  }
}

(coutMutex 是一个很好的方法,因此控制台输出不会出现乱码,但对于核心同步逻辑来说不是必需的。)

如果在您的实际用例中,您不需要 thread_func 从迭代到迭代保持活动状态(例如,为了保留某些状态),并且如果对 thread_func 的每次调用都完成了足够的工作,那么创建一个相比之下,运行新线程并不重要,然后为每个主循环迭代创建新线程(而不是重用线程)是直接、明智和简单的。

快乐的多线程黑客攻击!

K.弗兰克

【讨论】:

  • 谢谢!这是我在这里的第一个问题,您的帮助确实很好地说明了这个社区。经过数小时的挫折后,我最终也找到了与您类似的解决方案。我将同步变量设置为一个向量,这样每个线程都会通过将它们在向量中的位置设置为零来指示它已经完成,然后主线程将等待看到所有零重置。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-16
  • 1970-01-01
相关资源
最近更新 更多