有没有更好的方法让这段代码线程安全? Thread_local static 似乎是一个生硬的工具

Posted

技术标签:

【中文标题】有没有更好的方法让这段代码线程安全? Thread_local static 似乎是一个生硬的工具【英文标题】:Is there a better way to make this code thread-safe? Thread_local static seems a blunt tool 【发布时间】:2019-01-10 00:06:10 【问题描述】:

以下代码模拟了一个更大的程序,该程序创建了一个模拟实例,然后使用firstprivate 将其并行化以使我的实例私有。但是,实例本身在其方法中创建了另外两个实例。

结构看起来很做作,但我的双手有些束缚:类及其依赖关系由我想使用的工具决定,我认为这种情况在科学计算社区中很常见。

它编译得很好,并且在手动测试后似乎是线程安全的并且可以工作。

但我不确定我是否以最佳方式使用 C++ 技术,因为我怀疑我可以在内存层次结构中进一步声明实例,并且可能通过传递在parallel 区域通过引用我的其他实例或类似的东西。我怀疑这是最好的,因为#pragma omp parallel 大括号内的所有内容都是线程本地的。

因此,我的目标是为每个类创建两个(或更多)线程本地的独立实例,特别是 GenNo,因为它模拟了一个随机数生成器,每个线程播种一次,然后简单地使用相同的种子,尽管在这里我以可预测的方式更改我所谓的“种子”,只是为了了解程序的行为并揭示违反线程安全/竞争条件的情况。

被注释掉的代码不起作用,但产生了“分段错误”,程序以SIGSEGV 11. 退出我相信并行部署时“唯一”指针并不是那么唯一。总的来说,这个解决方案看起来更优雅,我想让它工作,但很高兴听你的 cmets。

为了获得std::unique_ptr 功能,必须注释掉以thread_local static 开头的行,并且必须删除其他cmets。

#include <iostream>
#include <omp.h>
//#include <memory>

class GenNo

public:

    int num;

    explicit GenNo(int num_)
    
        num = num_;
    ;

    void create(int incr)
    
        num += incr;
    
;

class HelpCrunch
public:
    HelpCrunch() 

    

    void helper(int number)
    
        std::cout << "Seed is " << number << " for thread number: " << omp_get_thread_num() << std::endl;
    
;

class calculate : public HelpCrunch

public:

    int specific_seed;
    bool first_run;

    void CrunchManyNos()
    
        HelpCrunch solver;

        thread_local static GenNo RanNo(specific_seed);
        //std::unique_ptr<GenNo> GenNo_ptr(nullptr);
        /*
        if(first_run == true)
        
            GenNo_ptr.reset(new GenNo(specific_seed));
            first_run = false;
        
         solver.helper(GenNo_ptr->num);
*/
        RanNo.create(1);
        solver.helper(RanNo.num);



        //do actual things that I hope are useful.
    ;
;




int main()


    calculate MyLargeProb;
    MyLargeProb.first_run = true;

#pragma omp parallel firstprivate(MyLargeProb)
    
        int thread_specific_seed = omp_get_thread_num();
        MyLargeProb.specific_seed = thread_specific_seed;

        #pragma omp for
        for(int i = 0; i < 10; i++)
        
            MyLargeProb.CrunchManyNos();
            std::cout << "Current iteration is " << i << std::endl;
        

    
    return 0;

现在带有thread_local static 关键字的输出是:

Seed is 2 for thread number: 1
Current iteration is 5
Seed is 3 for thread number: 1
Current iteration is 6
Seed is 4 for thread number: 1
Current iteration is 7
Seed is 5 for thread number: 1
Current iteration is 8
Seed is 6 for thread number: 1
Current iteration is 9


Seed is 1 for thread number: 0
Current iteration is 0
Seed is 2 for thread number: 0
Current iteration is 1
Seed is 3 for thread number: 0
Current iteration is 2
Seed is 4 for thread number: 0
Current iteration is 3
Seed is 5 for thread number: 0
Current iteration is 4

虽然不使用thread_local 但保留static 我得到:

Seed is 2 for thread number: 1
Current iteration is 5
Seed is 3 for thread number: 1
Current iteration is 6
Seed is 4 for thread number: 1
Current iteration is 7
Seed is 6 for thread number: 1
Current iteration is 8
Seed is 7 for thread number: 1
Current iteration is 9


Seed is 5 for thread number: 0
Current iteration is 0
Seed is 8 for thread number: 0
Current iteration is 1
Seed is 9 for thread number: 0
Current iteration is 2
Seed is 10 for thread number: 0
Current iteration is 3
Seed is 11 for thread number: 0
Current iteration is 4

如果我完全省略了 static 关键字,实例只会不断被重新分配,虽然我强烈怀疑它会被线程私有,但它几乎没有用,因为计数器会卡在 1 或2 用于双核机器上的线程 0 和 1。 (现实世界的应用程序必须能够“计数”并且不受并行线程的干扰。)

我需要什么帮助

现在,我以这样的方式对示例进行建模,即通过计数器相互干扰,违反线程安全将变得明显,正如我们所看到的那样,thread_local 被忽略但 static 被保留(两者都不傻)。 HelpCrunch 类实际上要复杂得多,并且很可能是线程安全的,并且可以在每次循环重复时重新初始化。 (这实际上更好,因为它从作为私有实例的子实例中获取了一堆变量。)但是您认为我最好将 thread_local 添加到 solver 的创建中,而不使用 static 关键字?或者我应该在其他地方声明实例,在这种情况下,我需要通过指针/引用等传递的帮助。

【问题讨论】:

为什么不简单地为每个线程创建一个生成器并将它们存储在一个向量中(只要确保它们在内存中存储得足够远)? 【参考方案1】:

首先,您的示例以线程不安全的方式使用全局对象std::cout,从多个线程同时访问它。我不得不在某些地方添加#pragma omp critical 以获得可读的输出。

其次,注释代码崩溃,因为GenNo_ptr 具有自动持续时间,因此每次CrunchManyNos() 完成执行时它都会被销毁。因此,当 first_runfalse 时,您正在取消引用 nullptr 指针。

当涉及到您的具体问题时,RanNo staticstatic thread_local 之间存在巨大差异:

如果是static,则将有一个RanNo 实例 第一次执行 CrunchManyNos() 时初始化。这是为了 在某种程度上是一个全局变量,在 您示例的并发上下文。

如果是static thread_local(顺便说一下,使用openmp你应该更喜欢threadprivate)它会 在新线程第一次调用CrunchManyNos() 和 将持续整个线程。

【讨论】:

非常感谢!您是否看到了一种使用指针来避免其被丢弃的方法?另外,您在哪里添加了关键语句?输出更多用于查看程序如何运行。关于静态与指针,哪种方式在风格方面更好? @Hirek 智能指针允许像自动变量一样使用它们。因此,static thread_local std::unique_ptr&lt;GenNo&gt; GenNo_ptr 会起作用。但是,在这种情况下使用动态分配是没有意义的。 @Hirek 我用#pragma omp critical 保护了std::cout 的每次使用,以使您的示例正常工作。但除非绝对必要,否则不要在实际代码中使用它,因为线程同步会降低性能。 @Hirek 您的代码对我来说看起来很可疑,因为您的大多数变量直接或间接依赖于线程 ID,尽管我不知道您的用例,理论上我只能在这里评论那些在您的代码中不起作用。否则为Code Review 材料。 是的,我可以看到。用例要大得多,我知道使用线程 id 生成并行随机数很笨拙,不如使用一些 Unix 随机设备来设置种子

以上是关于有没有更好的方法让这段代码线程安全? Thread_local static 似乎是一个生硬的工具的主要内容,如果未能解决你的问题,请参考以下文章

这段代码安全吗,可以从构造函数 C++ 生成线程吗?

java多线程中synchronize锁的使用和学习,Thread多线程学习

并发编程:线程安全

有没有办法让这段代码运行得更快

QThread 通信线程安全

在Python3中,socket.recv方法如果一段时间内没有收到返回,如何让这段代码跳过,并执行下一步操作