简体   繁体   English

使用std :: thread时主线程的while循环陷入困境

[英]While loop in main thread is getting stuck when using std::thread

I have a simple C++ code to test and understand threading. 我有一个简单的C ++代码来测试和理解线程。 The code has the main thread + a secondary thread. 该代码具有主线程+辅助线程。 The secondary updates the value of a variable which the main thread loop depends on. 辅助服务器更新主线程循环所依赖的变量的值。 When I add a print statement inside the main loop the program finishes successfully, but when I remove this print statement it goes into an infinite loop. 当我在主循环中添加一条打印语句时,程序成功完成,但是当我删除此打印语句时,它将进入无限循环。 This is the code that I'm using, and the print statement that I'm referring to is print statement 2 这是我正在使用的代码,而我指的是print语句2

#include <mpi.h>
#include <iostream>
#include <fstream>
#include <thread>
#include <mutex>
std::mutex mu;
int num;
using namespace std;

void WorkerFunction()
{
    bool work = true;
    while(work)
    {
            mu.lock();
            num --;
            mu.unlock();

            if(num == 1)
               work = false;
    }
}


int main(int argc, char **argv)
{
    bool work = true;
    num = 10;
    int numRanks, myRank, provided;
    MPI_Init_thread(&argc, &argv, MPI_THREAD_FUNNELED, &provided);
    MPI_Comm_size(MPI_COMM_WORLD, &numRanks);
    MPI_Comm_rank(MPI_COMM_WORLD, &myRank);

    std::thread workThread (WorkerFunction);
    //print statement 1
    cerr<<"Rank "<<myRank<<" Started workThread \n";

     int mult = 0;
     while(work)
     {
          mult += mult * num;
         //print statement 2
         if(myRank == 0) cerr<<"num = "<<num<<"\n";
         if(num == 1)
           work = false;
      }
   if(work == false)
      workThread.join();

   //print statement 3
   cerr<<"Rank "<<myRank<<" Done with both threads \n";

   MPI_Finalize();

 };

This is the output I get when I have print statement 2 这是我有打印语句2时得到的输出

mpirun -np 4 ./Testing
Rank 0 Started workThread 
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
num = 10
Rank 1 Started workThread 
Rank 0 Done with both threads 
Rank 1 Done with both threads 
Rank 2 Started workThread 
Rank 3 Started workThread 
Rank 2 Done with both threads 
Rank 3 Done with both threads

If I comment out that print statement then it goes into an infinte loop and this is the output I get 如果我注释掉该打印语句,那么它将进入无限循环,这是我得到的输出

mpirun -np 4 ./Testing
Rank 0 Started workThread 
Rank 0 Done with both threads 
Rank 1 Started workThread 
Rank 2 Started workThread 
Rank 3 Started workThread 
Rank 2 Done with both threads 
Rank 3 Done with both threads

I'm not sure what am I doing wrong, any help is appreciated. 我不确定自己在做什么错,我们会提供任何帮助。

Concerning MPI, I haven't any experience. 关于MPI,我没有任何经验。 (I used it decades ago, and I'm sure that fact is completely worthless.) However, OP claimed (我几十年前使用过它,我敢肯定这个事实是完全没有价值的。)但是,OP声称

I have a simple C++ code to test and understand threading. 我有一个简单的C ++代码来测试和理解线程。

Considering, that multiprocessing (with MPI ) as well as multithreading (with std::thread ) are complicated topics on its own, I would separate the topics first, and try to put them together after having gained some experience in each of them. 考虑到,多处理(使用MPI )和多线程(使用std::thread )本身就是复杂的主题,我将首先将这些主题分开,并在对每个主题都有一定的经验之后将它们放在一起。

So, I elaborate a bit about the multithreading (which I feel able to). 因此,我详细介绍了多线程(我认为能够做到)。


First sample is a revised version of OPs code (all references to MPI removed): 第一个示例是OPs代码的修订版(已删除所有对MPI引用):

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtxNum;
int num;

const std::chrono::milliseconds delay(100);

void WorkerFunction()
{
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    int num_;
    mtxNum.lock();
    num_ = --num;
    mtxNum.unlock();
    work = num_ != 1;
  }
}

int main()
{
  num = 10;
  std::thread workThread(&WorkerFunction);
  int mult = 0;
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    int num_;
    mtxNum.lock();
    num_ = num;
    mtxNum.unlock();
    std::cout << "num: " << num_ << '\n';
    mult += mult * num_;
    work = num_ != 1;
  }
  if (workThread.joinable()) workThread.join();
  std::cout << "Both threads done.\n";
}

Output: 输出:

num: 10
num: 8
num: 7
num: 6
num: 5
num: 4
num: 3
num: 2
num: 2
num: 1
Both threads done.

Live Demo on coliru 在coliru上进行现场演示

Notes: 笔记:

  1. While multithreading is running, and variable num is shared, and variable num is modified in at least one thread, every access should be put into a critical section (a pair of mutex lock and unlock). 而多线程运行时,和可变num是共同的,可变num是在至少一个线程修改,每次访问应该被放入一个临界区 (一对互斥锁和解锁)。

  2. The critical section should always be kept as short as possible. 关键部分应始终保持尽可能短。 (Only one thread can pass the critical section at one time. Hence, it introduces re-serialization which consumes speed-up intended by concurrency.) I introduced a local variable num_ in each thread to copy current value of shared variable and to use it after critical section in the respective thread. (一次只能有一个线程通过关键部分。因此,它引入了重新序列化,这消耗了并发性的加速。)我在每个线程中引入了一个局部变量num_以复制共享变量的当前值并使用它在相应线程的关键部分之后。 * *

  3. I added a sleep_for() to both threads for better illustration. 我为两个线程都添加了sleep_for()以便进行更好的说明。 Without, I got 没有,我得到了

     num: 10 num: 1 Both threads done. 

    which I found somehow boring. 我觉得有点无聊。

  4. The output skips num == 9 and prints num == 2 twice. 输出跳过num == 9并输出num == 2两次。 (This may look differently in other runs.) The reason is that the threads work asynchronously by definition. (这在其他运行中可能看起来有所不同。)原因是线程根据定义异步地工作。 (The equal delay of 100 milliseconds in both threads is no reliable synchronization.) The OS is responsible to wake a thread if nothing (like eg the locked mutex) prevents this. (两个线程中100毫秒的相等延迟是不可靠的同步。)如果没有任何阻止(例如锁定的互斥锁)的操作,则OS负责唤醒线程。 It is free to suspend the thread at any time. 可以随时随时挂起线程。

Concerning mtxNum.lock() / mtxNum.unlock() : Imagine that the critical section contains something more complicated than a simple --num; 关于mtxNum.lock() / mtxNum.unlock() :想象关键部分包含的内容比简单的--num;更复杂--num; which may throw an exception. 这可能会引发异常。 If an exception is thrown, the mtxNum.unlock() is skipped, and a deadlock is produced preventing any thread to proceed. 如果引发异常,那么将跳过mtxNum.unlock() ,并产生死锁 ,从而阻止任何线程继续进行。

For this, the std library provides a nice and handy tool: std::lock_guard : 为此, std库提供了一个方便的工具: std::lock_guard

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtxNum;
int num;

const std::chrono::milliseconds delay(100);

void WorkerFunction()
{
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    int num_;
    { std::lock_guard<std::mutex> lock(mtxNum); // does the mtxNum.lock()
      num_ = --num;
    } // destructor of lock does the mtxNum.unlock()
    work = num_ != 1;
  }
}

int main()
{
  num = 10;
  std::thread workThread(&WorkerFunction);
  int mult = 0;
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    int num_;
    { std::lock_guard<std::mutex> lock(mtxNum); // does the mtxNum.lock()
      num_ = num;
    } // destructor of lock does the mtxNum.unlock()
    std::cout << "num: " << num_ << '\n';
    mult += mult * num_;
    work = num_ != 1;
  }
  if (workThread.joinable()) workThread.join();
  std::cout << "Both threads done.\n";
}

Output: 输出:

num: 10
num: 8
num: 7
num: 6
num: 5
num: 4
num: 3
num: 2
num: 1
Both threads done.

Live Demo on coliru 在coliru上进行现场演示

The trick with std::lock_guard is that the destructor unlocks the mutex in any case, even if an exception is thrown inside of critical section. std::lock_guard的诀窍是,即使在关键部分内引发了异常,析构函数也可以在任何情况下解锁互斥量。

May be, I'm a bit paranoid but it annoys me that non-guarded access to a shared variable may happen by accident without being noticed in any debugging session nor any compiler diagnostics. 可能是,我有点偏执,但是让我感到烦恼的是,对共享变量的无保护访问可能是偶然发生的,而在任何调试会话或任何编译器诊断中都没有注意到。 ** Hence, it might be worth to hide the shared variable into a class where access is only possible with locking it. **因此,可能有必要将共享变量隐藏到只能通过锁定才能访问的类中。 For this, I introduced Shared in the sample: 为此,我在示例中介绍了Shared

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

template <typename T>
class Shared {
  public:
    struct Lock {
      Shared &shared;
      std::lock_guard<std::mutex> lock;
      Lock(Shared &shared): shared(shared), lock(shared._mtx) { }
      ~Lock() = default;
      Lock(const Lock&) = delete;
      Lock& operator=(const Lock&) = delete;

      const T& get() const { return shared._value; }
      T& get() { return shared._value; }
    };
  private:
    std::mutex _mtx;
    T _value;
  public:
    Shared() = default;
    explicit Shared(T &&value): _value(std::move(value)) { }
    ~Shared() = default;
    Shared(const Shared&) = delete;
    Shared& operator=(const Shared&) = delete;
};

typedef Shared<int> SharedInt;
SharedInt shNum(10);

const std::chrono::milliseconds delay(100);

void WorkerFunction()
{
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    int num_;
    { SharedInt::Lock lock(shNum);
      num_ = --lock.get();
    }
    work = num_ != 1;
  }
}

int main()
{
  std::thread workThread(&WorkerFunction);
  int mult = 0;
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    int num_;
    { const SharedInt::Lock lock(shNum);
      num_ = lock.get();
    }
    std::cout << "num: " << num_ << '\n';
    mult += mult * num_;
    work = num_ != 1;
  }
  if (workThread.joinable()) workThread.join();
  std::cout << "Both threads done.\n";
}

Output: similar as before. 输出:与以前相似。

Live Demo on coliru 在coliru上进行现场演示

The trick is that a reference to shared value can be retrieved from a Shared::Lock instance → ie while it is locked. 诀窍是可以从Shared::Lock实例→即被Shared::Lock检索对共享值的引用。 Even if the reference is stored: 即使引用已存储:

    { SharedInt::Lock lock(shNum);
      int &num = lock.get();
      num_ = --num;
    }

The lifetime of int &num just ends before the lifetime of SharedInt::Lock lock(shNum); int &num SharedInt::Lock lock(shNum);的生存期在SharedInt::Lock lock(shNum);的生存期之前结束SharedInt::Lock lock(shNum); .

Of course, one could get a pointer to num to use it outside of scope but I would consider this as sabotage. 当然,可以获取指向num的指针以在范围之外使用它,但我认为这是破坏活动。


Another thing, I would like to mention is std::atomic : 我想提到的另一件事是std::atomic

The atomic library provides components for fine-grained atomic operations allowing for lockless concurrent programming. 原子库提供了用于精细原子操作的组件,允许进行无锁并发编程。 Each atomic operation is indivisible with regards to any other atomic operation that involves the same object. 关于涉及同一对象的任何其他原子操作,每个原子操作都是不可分割的。

While a mutex may be subject of OS kernel functions, an atomic access might be done exploiting CPU features without the necessity to enter the kernel. 尽管互斥锁可能是OS内核功能的主题,但可以利用CPU功能来完成原子访问,而无需进入内核。 (This might provide speed-up as well as result in less usage of OS resources.) (这可能会提高速度,并导致较少使用OS资源。)

Even better, if there is no H/W support for the resp. 如果没有相应的硬件支持,那就更好了。 type available it falls back to an implementation based on mutexes or other locking operations (according to the Notes in std::atomic<T>::is_lock_free() ): 可用的类型,它取决于基于互斥或其他锁定操作的实现 (根据std::atomic<T>::is_lock_free()的注释):

All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. 除std :: atomic_flag之外的所有原子类型都可以使用互斥锁或其他锁定操作来实现,而不是使用无锁原子CPU指令来实现。 Atomic types are also allowed to be sometimes lock-free, eg if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks. 原子类型有时也可以是无锁的,例如,如果在给定的体系结构上,只有对齐的内存访问自然是原子的,则相同类型的未对齐对象必须使用锁。

The modified sample with std::atomic : 具有std::atomic的修改后的示例:

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

std::atomic<int> num;

const std::chrono::milliseconds delay(100);

void WorkerFunction()
{
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    work = --num != 1;
  }
}

int main()
{
  num = 10;
  std::thread workThread(&WorkerFunction);
  int mult = 0;
  for (bool work = true; work; std::this_thread::sleep_for(delay)) {
    const int num_ = num;
    std::cout << "num: " << num_ << '\n';
    mult += mult * num_;
    work = num_ != 1;
  }
  if (workThread.joinable()) workThread.join();
  std::cout << "Both threads done.\n";
}

Output: 输出:

num: 10
num: 8
num: 7
num: 7
num: 5
num: 4
num: 3
num: 3
num: 1
Both threads done.

Live Demo on coliru 在coliru上进行现场演示


* I brooded a while over the WorkingThread() . *我在WorkingThread()沉思了一下。 If it's the only thread which modifies num , the read access to num (in WorkingThread() ) outside critical section should be safe – I believe. 如果它是唯一修改num线程,则对关键部分之外的num (在WorkingThread() )的读取访问应该是安全的–我认为。 However, at least, for the sake of maintainability I wouldn't do so. 但是,至少出于可维护性考虑,我不会这样做。

** According to my personal experience, such errors occur rarely (or never) in debug sessions but in the first 180 seconds of a presentation to a customer. **根据我的个人经验,此类错误很少(或永远不会)在调试会话中发生,而是在向客户演示的前180秒内发生。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM