即使使用互斥锁,C++ std::thread 上的死锁

deadlock on c++ std::thread even with mutex lock

我试图更好地理解 C++ 线程。我实现了一个小示例,其中我希望四个线程​​在 std::vector.

中的索引区域上工作

当然我运行进入了死锁并读入了互斥锁和锁的使用。据我了解,一般假设是所有变量都由线程共享,除非另有明确说明(thread_local)。如果线程更改任何全局数据,明智的做法是首先锁定资源,对数据进行处理以避免数据竞争,然后再次解锁数据以便其他线程可以使用它。

在我的示例中,std::cout 上的锁工作正常,线程创建正常,函数调用正常,但程序仍然挂起,即使我在之前和之后实现了 data_lock数据被操纵。 如果我注释掉数据操作并改为显示一条消息,它也可以正常工作。输出与运行 运行不同,所以我不发布它。

我的感觉是我错过了一些我不知道的概念 c++ 线程(我之前使用过 MPI)。

我的问题:

  1. 有没有我缺少的概念?我还需要什么 know/read?
  2. 除了使用互斥体、锁和 thread_local 来正确执行之外,还有哪些工具?

编译指令:

g++ -std=c++1y -O0 -g3 -Wall -c -fmessage-length=0 -pthread -MMD -MP -MF"src/main.d" -MT"src/main.d" -o "src/main.o" "../src/main.cpp"

代码:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
using namespace std;

std::mutex data_lock;
std::mutex cout_lock;

void output(std::string message){
    cout_lock.lock();
    cout << message << endl;
    cout_lock.unlock();
}

void work(std::vector<double>& data, const int s_ind, const int e_ind)     {
    thread_local int i = 0;
    for (i = s_ind; i <= e_ind; i++) {
        data_lock.lock();
        data[i] = 1.0;
        data_lock.unlock();
        //msg("work");
    }
}

int main() {
    const int size = 1000;
    const int cpus = 4;
    const int chunksize = size / cpus;

    //create Data vector
    std::vector<double> dat { (size) };

    //thread vector
    std::vector<std::thread> threads;

    //create and start threads with proper ranges (ranges tested)
    for (int cpu = 0; cpu < cpus; cpu++) {
        threads.push_back(std::thread(work, ref(dat), (cpu * chunksize),(((cpu + 1) * chunksize) - 1)));
        output("thread created");
    }

    //delete threads
    for (int cpu = 0; cpu < cpus; cpu++) {
        threads[cpu].join();
        output("thread joined");
    }
return 0;
}

我知道有人建议在 ctor 中使用 {},它消除了隐式函数声明的问题。但是,如果 class 具有以 std::initializer_list<T> 作为参数的构造函数重载,而 std::vector 则有问题。所以这一行:

std::vector<double> dat { (size) };

创建包含一个元素 size 的向量。

正如您所说 运行 陷入僵局并且必须使用互斥锁,这几乎是不可能的。很可能您的程序挂起,但这不被视为死锁。死锁是一种情况,当您的线程互相阻塞试图以错误的顺序(或类似情况)锁定互斥锁时。

注意:这不是修复,但您应该使用 RAII 进行互斥锁,标准库为此提供了工具:std::lock_guard, std::unique_lock or std::scoped_lock if you have c++17. Reason, why RAII should be used in this case (and many others) are described here

RAII guarantees that the resource is available to any function that may access the object (resource availability is a class invariant, eliminating redundant runtime tests). It also guarantees that all resources are released when the lifetime of their controlling object ends, in reverse order of acquisition. Likewise, if resource acquisition fails (the constructor exits with an exception), all resources acquired by every fully-constructed member and base subobject are released in reverse order of initialization. This leverages the core language features (object lifetime, scope exit, order of initialization and stack unwinding) to eliminate resource leaks and guarantee exception safety. Another name for this technique is Scope-Bound Resource Management (SBRM), after the basic use case where the lifetime of an RAII object ends due to scope exit.