我一直在辩论一个关于多线程环境中局部变量的案例。
问题在于形成如下的程序:
std::mutex mut;
int main()
{
std::size_t i = 0;
doSomethingWhichMaySpawnAThreadAndUseTheMutex();
mut.lock();
i += 1; // can this be reordered?
mut.unlock();
return i;
}
问题在于i += 1
是否可以在互斥锁定之上重新排序。
明显的部分是mut.lock()
发生在 i += 1
之前,所以如果任何其他线程可能能够观察到i
的值,编译器有义务不增加它。从C ++规范的3.9.2.3开始,"如果类型T的对象位于地址A,则类型为cv T *的指针(其值为地址A)被称为指向该对象,无论如何获得该值。""这意味着如果我使用任何方法获得指向i
的指针,我可以期望看到正确的值。
但是,规范确实说明编译器可能会使用" as-if"规则不给对象一个记忆地址(第1.8.6节的脚注4)。例如,i
可以存储在没有内存地址的寄存器中。在这种情况下,没有内存地址可以指向,因此编译器可以证明没有其他线程可以访问i
。
我感兴趣的问题是如果编译器没有这样做" as-if"优化,确实存储了对象。是否允许编译器存储i
,但是如果实际上没有存储i
那么会重新排序吗?从实现的角度来看,这意味着i
可能存储在堆栈中,因此可能有一个指针指向它,但让编译器假设没有人能看到i
,并且重新订购?
答案 0 :(得分:2)
只要在没有这些优化的情况下合法地获得程序执行的可观察结果(" as-if"),就允许编译器执行优化。 [1] 所以这个问题使用" as-if"以误导的方式,如果不是实际问一个倒退的问题:
是否允许编译器存储
i
,但是如果i
实际上没有存储,那么会重新排序吗?
只要可以通过优化获得程序执行结果,就会询问是否允许编译器执行操作。这不是问题。该问题应使用非优化行为作为参考。所以更像是:"编译器是否允许重新排序语句?" 答案是肯定的,只要可观察的结果不变。这个特定函数的外部没有任何东西被告知如何访问i
,因此应该允许编译器在它的周围使用之间的任何地方实现增量(具体来说:它的定义和return
语句)。 / p>
话虽这么说,我希望编译器在这种情况下做的是既不给i
一个内存地址也不把它当作寄存器变量。我希望编译器将其视为常量,有效地将您的函数更改为:
int main()
{
doSomethingWhichMaySpawnAThreadAndUseTheMutex();
mut.lock();
mut.unlock();
return 1;
}
只要你无法检测到它已经完成(没有直接检查机器代码),这是允许的。
注意:
[1] 使用"可能是"是承认C ++规范的某些部分使用了单词" unspecified"。这些部分允许编译器做出选择(当处理非健壮的代码时)可能改变可观察的行为。也就是说,可以存在一组允许的行为,而不是单个允许的行为。只要结果保留在此集合中,就允许进行优化。
答案 1 :(得分:1)
我发现这个问题非常混乱。如果代码已发布,编译器显然意识到i
语句中唯一使用return
,因此i
将被优化掉,故事结束。互斥体不会进入它。
但是一旦你拿到i
的地址 - 并将其交给其他人 - 游戏就会改变。现在,编译器必须在堆栈上放置一个实变量,并在mutex.lock()
和mutex.unlock()
之间仅操作它。做其他事情会改变程序的语义。互斥锁还为你提供了一个记忆围栏。
您可以在Godbolt清楚地看到这一点。
编辑:我修复了该代码中的一个愚蠢的错误,这个错误掩盖了我试图制作的内容,对不起。
答案 2 :(得分:0)
整个序列:
unsigned char* send_buffer[32] = {0}; // zero out buffer to use as scratch
send_buffer[0] = 0x90;
send_buffer[1] = 0x48;
send_buffer[2] = 0x00;
SuperpoweredUSBMIDI::send(deviceID, send_buffer, 3);
可以编译为 mut.lock(); // i == 0 at that point
i += 1; // can this be reordered?
mut.unlock(); // i == 1 at that point
exit(i);
}
,其他线程可以忽略,因为没有正确的同步。
您需要等待其他线程终止,或者让他们再次无所事事。你不这样做,所以可以假设所有其他线程都没有做任何事情。
互斥体在这里没有任何有意义的作用。