C ++中不同向量之间没有线程安全?

Posted

技术标签:

【中文标题】C ++中不同向量之间没有线程安全?【英文标题】:No thread-safety between different vectors in C++? 【发布时间】:2019-07-12 21:07:49 【问题描述】:

我的问题是同时使用不同的向量。我知道我不能指望同一个向量同时在多个线程中工作。我已经分解了程序,以便更容易理解它。我有一个ThreadClass 类,它有一个构造函数,它只是将一个元素添加到向量k,然后调用一个线程toCall,然后输出应该是一个向量的大小。此类的对象是使用向量的push_back 成员在main() 函数内部的不同向量内创建的。

输出结果为 0。有时我也可以得到 1。如果我切换到调试模式,我可以产生更多的数字 1。我已经在 gnu C++17 编译器(Ubuntu 16.04)和 Visual Studio 编译器(Windows 10)上测试了这个问题。我现在的问题是,如果这个例子表明我应该完全避免在多线程程序中使用向量?

class ThreadClass 

private:
    std::vector<int> k;
    std::thread thr;
public:
    ThreadClass() 
        k.push_back(27);
        thr = std::thread(&ThreadClass::toCall, this);
    
    void toCall() 
        std::cout << k.size() << std::endl;
    
    void close() 
        if (thr.joinable())thr.join();
    
;

int main()
    std::vector<ThreadClass> lols;
    lols.push_back(ThreadClass());
    lols[0].close();
    return 0;

【问题讨论】:

【参考方案1】:

问题在于ThreadClass 类型的值包含对其自身的引用。具体来说,thr 包含this 的副本。

当您复制或移动此类值时,例如当临时的ThreadClass() 被移动到lols 时,副本拥有一个重复的this ptr,即它指向旧的临时,其生命周期在对lols.push_back 的调用完成后结束。

我们可以在没有线程的情况下复制这个问题:

class Foo

private:
    std::vector<int> k;
    Foo* possibly_this;
public:
    Foo() 
        k.push_back(27);
        possibly_this = this;
    
    void toCall() 
        std::cout << possibly_this->k.size() << std::endl;
    
;

int main()
    std::vector<Foo> lols;
    lols.push_back(Foo);
    lols[0].toCall();

(对我来说,它在 7.3.1 上用 -O0 打印 0,但同样,它是 UB,所以它可以在你的机器上做任何事情。)

lols.emplace() 不会提供帮助。如果 std::vector 调整大小,则所有指向它的指针/迭代器都将失效。不幸的是,您无法更改存储在thr 中的指针,因此您只有一种解决方案:禁用ThreadClass 的复制和移动构造函数,如下所示:

  //within the definition of ThreadClass
  ThreadClass(ThreadClass const&) = delete;

为了将ThreadClass 放置在容器中,您需要额外的间接级别以允许ThreadClass 类型值的实际对象具有稳定的位置。 std::list&lt;ThreadClass&gt;std::vector&lt;std::unique_ptr&lt;ThreadClass&gt;&gt; 都可以解决问题。

【讨论】:

哦,非常感谢,这个解释启发了我。【参考方案2】:

一个问题是您的线程可以在构造函数返回之前调用toCall。在构造函数中创建回调到对象的线程不是一个好主意。将线程创建推迟到某种startlaunch 函数,并在构造函数返回后调用。

这也是个问题:

lols.push_back(ThreadClass());

在这里,(临时的)析构函数甚至可以在toCall 被调用之前运行!那肯定行不通。这是不在构造函数中创建线程的另一个很好的理由——它使临时对象成为灾难。

【讨论】:

谢谢。我接受了你的建议,现在效果很好。但我不确定你的第二部分。据我了解,总是会在调用线程之前调用析构函数。例如,您有一个名为CallMyThread() 的函数,它调用对象内部的一个线程。然后突然无论什么原因都会调用析构函数。 @SouthParkGermany 那么也许析构函数应该确保线程在返回之前完成。

以上是关于C ++中不同向量之间没有线程安全?的主要内容,如果未能解决你的问题,请参考以下文章

如果没有插入并且只有 .find(),在多个线程上使用 C++ STL 容器是不是安全?

C 读取和线程安全 (linux)

C ++ - std :: vector安全多线程

是否在向量C ++中读取和写入向量线程安全操作? [重复]

线程安全之 synchronized 和 ReentrantLock

c++ string线程安全吗