【问题标题】:Is it thread safe to modify a static variable?修改静态变量是线程安全的吗?
【发布时间】:2017-10-05 20:53:05
【问题描述】:

从 C++11 开始,静态变量初始化保证是线程安全的。但是如何在多个线程中修改静态变量呢?如下所示

static int initialized = 0;
Initialize()
{
 if (initialized)
    return;
 initialized = 1; // Is this thread safe? 
}

我问这个问题的原因是我正在阅读源代码 Py_Initialize(),我正在尝试将 Python 嵌入到多线程 C++ 应用程序中,我想知道在多个线程中多次调用 Py_Initialize() 是否安全? Py_Initialize() 的实现归结为 函数_Py_InitializeEx_Private,如下所示

// pylifecycle.c
static int initialized = 0;

_Py_InitializeEx_Private(int install_sigs, int install_importlib)
{
    if (initialized)
        return;
    initialized = 1;
 // a bunch of other stuff
 }

C 的结论是否与 C++ 相同?

编辑 所以所有的答案都很好,我选择了最让我头脑清醒的一个。

【问题讨论】:

  • 不允许两个线程不同步修改同一个内存位置。
  • 好吧,错误..他们是被允许的!您必须了解可能的结果以及这可能会如何影响您应用的功能。
  • @ThingyWotsit,你能说清楚什么是允许的吗?
  • @Allanqunzi 他的意思是允许不同步的读取和写入,但您必须准备好处理可能对程序状态产生的影响。例如,将 64 位整数写入内存通常不是 32 位架构上的原子操作,这意味着如果您不采取措施,可能会得到一个值的上半部分和另一个值的下半部分同步读取和写入。
  • @Allanqunzi 如果您连续多次访问同一个静态变量,则允许编译器将这些访问优化为单个操作,假设值为 not i> 将被另一个线程同时修改。这就是std::atomicvolatile(对后者的作用有正确理解)之类的东西可能有用的地方。

标签: python c++ c multithreading c++11


【解决方案1】:

第一个代码示例是所谓的“延迟初始化”的典型起点。它对于保证“昂贵对象”的一次性初始化很有用;但只有在需要使用该对象之前才这样做。

那个具体的例子没有任何严重的问题,但它过于简单化了。当您更全面地看待延迟初始化时,您会发现多线程延迟初始化并不是一个好主意。


线程安全”的概念远远超出了单个变量(静态或其他)。您需要退后一步,考虑同时发生在相同1 资源(内存、对象、文件等)上的事情

1:同一个类的不同实例不是同一个东西;但它们的静态成员是。

考虑以下第二个示例的摘录。

if (initialized)
    return;
initialized = 1;
// a bunch of other stuff

在前 3 行中,如果多个线程大致同时执行该代码,则不会造成严重损害。一些线程可能会提前返回;其他人可能有点“太快”并且都执行设置initialized = 1;的任务。但是,这不是问题,因为无论有多少线程设置共享变量,最终效果始终相同。

问题出现在第四行。那个几乎是漫不经心地把它当作“一堆其他的东西”置之不理。 “other stuff”是真正关键的代码,因为如果initialized = 1; 可能被多次调用,您需要考虑多次调用“other stuff”的影响和同时


现在,万一您确信“其他东西”可以被多次调用,还有另一个问题...

考虑可能使用 Python 的客户端代码。

Py_Initialize();
//use Python

如果2个线程同时调用上面的; 1个“提前返回”,另一个实际执行初始化。然后“提前返回线程”将使用 Python完全初始化之前开始(或尝试开始)

作为一个小技巧,您可以尝试在初始化过程中阻塞if (initialized) 行。但这是不可取的,原因有两个:

  • 多个线程可能会在其处理的早期阶段卡住等待。
  • 即使在初始化完成之后,每次“延迟初始化”Python 框架时,检查锁的开销也会很小(但完全是浪费)。

结论

延迟初始化有其用途。但是最好不要尝试从多个线程执行延迟初始化。而是有一个“安全线程”(主线程通常足够好),甚至可以在创建任何尝试使用已初始化内容的线程之前执行延迟初始化。 那么你就完全不用担心线程安全了。

【讨论】:

  • 我想补充一点,Andrew's answer 中的call_once 已指定,以便为多线程延迟初始化提供适当的保护。 …它阻塞每个调用该方法的线程,直到初始化成功完成;此时所有调用者(当前和未来)只需返回,无需进一步处理。它还允许一次只允许一个线程实际尝试初始化;甚至具有针对异常的内置保护。随意使用call_once;它干净利落地处理“一点点黑客”。
【解决方案2】:

Py_Initialize 不是线程安全的。只有在知道 Python 解释器已经初始化的情况下,才能从多个线程调用它,但如果你能证明调用该函数是愚蠢的。

确实,大多数 Python C-API 调用都不是线程安全的;您需要获取全局解释器锁 (GIL) 才能与 Python 解释器进行交互。 (有关详细信息,请参阅Python C-API docs。请仔细阅读。)

但是,据我所知,在解释器初始化之前,您不能使用标准 API 来获取 GIL。因此,如果您有多个线程,其中任何一个都可能初始化同一个 Python 解释器,您需要使用自己的互斥锁来保护对 Py_Initialize 的调用。如果您的程序逻辑可能的话,您最好在启动任何线程之前进行一次初始化。


你引用的代码:

static int initialized = 0;

void Initialize_If_Necessary() 
{
    if (initialized)
        return;
    initialized = 1;
    // Do the initialization only once
}

显然在任何语言中都不是线程安全的,即使initialized 是原子类型。假设在任何初始化发生之前两个线程同时执行此代码:它们都将initialized 视为假,因此它们都继续进行初始化。 (如果你没有两个核心,你可以想象第一个过程是在initialized的测试和分配之间切换任务。)

【讨论】:

  • 有道理,我认为python doc有点含糊,会误导人们。我认为下面@NirFriedman 的解决方案将是我要做的。
【解决方案3】:

修改静态变量不是线程安全的,但初始化静态变量是线程安全的。所以你可以这样做:

void my_py_init() {
    static bool x = (Py_Initialize(), true);
}

就是这样。您现在可以从任意多个线程调用my_py_init,而Py_Initialize 只会被调用一次。

【讨论】:

    【解决方案4】:

    跨多个线程修改静态变量是不安全的,因为如果将变量放入寄存器中,那么相同寄存器中的其他内核的信息将不同(在另一个线程中修改变量将与尝试访问该内核的寄存器版本,其中包含完全不同的数据)。

    【讨论】:

    • 跨多个线程修改静态变量是不安全的,因为标准是这样说的。而且因为标准说它是不安全的,编译器可以将它们放入寄存器中。你的描述已经颠倒了因果关系。
    • 标准规定跨多个线程修改线程是不安全的,这是有原因的。以上很可能是原因。
    【解决方案5】:

    不,在这种情况下,静态仅与存储持续时间有关(请参阅http://en.cppreference.com/w/c/language/static_storage_duration)。 与其他变量相比,该变量根本没有额外的线程安全性。

    为此尝试使用 std::call_once,请参阅 http://en.cppreference.com/w/cpp/thread/call_once

    【讨论】:

    • 那为什么初始化是线程安全的,如果它只是关于存储持续时间呢?
    • 在这种情况下你是对的,但请注意,在 C++11 及更高版本中,局部静态变量的初始化是线程安全的。
    • @Allanqunzi:因为初始化是独一无二的。它只发生一次,你不能在那个单一的初始化周围放置一个互斥锁。进一步使用是正常的,并且可以根据需要/需要由互斥锁保护。但是您也可以使用原子变量而不是互斥体,并且您也需要特殊处理来运行初始化程序一次。在初始化程序运行时,原子甚至不存在!
    • 而且因为静态初始化在概念上发生在任何其他代码执行之前。强调概念
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-18
    • 2011-07-07
    • 2010-11-19
    • 2011-01-25
    • 1970-01-01
    相关资源
    最近更新 更多