使用std :: thread

时间:2019-07-23 19:35:44

标签: c++ multithreading mpi infinite-loop stdthread

我有一个简单的C ++代码来测试和理解线程。该代码具有主线程+辅助线程。 辅助服务器更新主线程循环所依赖的变量的值。当我在主循环中添加一条打印语句时,程序成功完成,但是当我删除此打印语句时,它将进入无限循环。 这是我正在使用的代码,而我所指的print语句是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();

 };

这是我有打印语句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

如果我注释掉该打印语句,那么它将进入无限循环,这是我得到的输出

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

我不确定自己在做什么错,我们会提供任何帮助。

1 个答案:

答案 0 :(得分:4)

关于MPI,我没有任何经验。 (我几十年前就用过,我敢肯定这个事实是完全没有价值的。)但是,OP声称

  

我有一个简单的C ++代码来测试和理解线程。

考虑到,多处理(使用MPI和多线程(使用std::thread)本身就是一个复杂的主题,我将首先分离这些主题,并在获得这些信息后尝试将它们放在一起他们每个人都有一些经验。

因此,我详细介绍了多线程(我认为能够做到)


第一个示例是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";
}

输出:

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

注释:

  1. 在运行多线程并且共享变量num并在至少一个线程中修改变量num的同时,每次访问都应放入critical section(互斥锁对)。

  2. 关键部分应始终保持尽可能短。 (一次只有一个线程可以通过关键部分。因此,它引入了重新序列化,这消耗了并发性的加速。)我在每个线程中引入了局部变量num_,以复制共享变量的当前值。并在相应线程的关键部分之后使用它。 *

  3. 我为两个线程都添加了sleep_for(),以便更好地说明。没有,我得到了

    num: 10
    num: 1
    Both threads done.
    

    我觉得有点无聊。

  4. 输出跳过num == 9并打印num == 2两次。 (这在其他运行中可能看起来有所不同。)原因是线程根据定义异步地工作。 (两个线程中的100毫秒相等的延迟是不可靠的同步。)如果没有任何阻止措施(例如,锁定的互斥锁),则OS负责唤醒线程。随时可以暂停线程。

关于mtxNum.lock() / mtxNum.unlock():想象一下,关键部分包含的内容比简单的--num;可能会引发异常的情况要复杂得多。如果引发异常,则会跳过mtxNum.unlock(),并生成deadlock来阻止任何线程继续进行。

为此,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";
}

输出:

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

使用std::lock_guard的诀窍是,即使在关键部分内引发了异常,析构函数也可以在任何情况下解锁互斥体。

也许是,我有点偏执,但是让我感到烦恼的是,在没有任何调试会话或任何编译器诊断程序的注意下,偶然发生的对共享变量的非保护访问可能会发生。 ** 因此,可能有必要将共享变量隐藏到只能通过锁定才能访问的类中。为此,我在示例中引入了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";
}

输出:与以前类似。

Live Demo on coliru

诀窍是,可以从Shared::Lock实例→即在锁定的情况下检索对共享值的引用。即使参考已存储:

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

int &num的生存期在SharedInt::Lock lock(shNum);的生存期之前结束。

当然,人们可以得到指向num的指针以在范围之外使用它,但我认为这是破坏活动。


我想说的另一件事是std::atomic

  

原子库提供了用于精细原子操作的组件,允许进行无锁并发编程。就涉及同一对象的任何其他原子操作而言,每个原子操作都是不可分割的。

尽管互斥锁可能是OS内核功能的主题,但是可以利用CPU功能来完成原子访问,而无需进入内核。 (这可能会提高速度,并减少操作系统资源的使用。)

更好的是,如果没有硬件支持。类型可用,它可归结为基于实现的互斥对象或其他锁定操作(根据std::atomic<T>::is_lock_free()中的注释):

  

除std :: atomic_flag以外的所有原子类型都可以使用互斥锁或其他锁定操作来实现,而不是使用无锁原子CPU指令来实现。有时也允许原子类型是无锁的,例如如果在给定的体系结构上,只有对齐的内存访问自然是原子的,则相同类型的对齐错误的对象必须使用锁。

修改后的示例为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";
}

输出:

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


* 我在WorkingThread()上沉思了一会儿。我相信,如果它是唯一修改num的线程,则对临界区之外的num(在WorkingThread()中)的读取访问应该是安全的。但是,至少出于可维护性考虑,我不会这样做。

** 根据我的个人经验,此类错误很少(或永远不会)发生在调试会话中,而是在向客户演示的前180秒内发生。