"benign" 数据竞争真的是良性的吗?
Is "benign" data race actually benign?
我正在使用一些包含以下比赛的第三方库代码,简化显示:
struct Object
{
void (*func)(void *);
void *data;
};
void called_from_thread1(struct Object *obj)
{
obj->func(obj->data);
}
void called_from_thread2(struct Object *obj, void (*func)(void *), void *data)
{
obj->func = func;
obj->data = data;
}
两个线程的访问是不同步的,所以一般我会这样分析:
func
上的数据竞赛
data
上的数据竞赛
- 对
func
/data
对的访问不一致(只有一个被更改时可能会发生访问)
然而,在这个特定的用例中,值 总是 改回默认值。像这样:
void default_func(void *data) { /* snip */ };
void *default_data = 0; // Actually something valid
struct Object object = {
.func = default_func,
.data = &default_data,
};
线程 1 和线程 2 都引用 object
生成,线程 2 将同一个对象设置回默认值,类似于:
called_from_thread1(&object);
// elsewhere...
called_from_thread2(&object, default_func, &default_data);
从技术上讲,这仍然是一场竞赛(线程清理程序报告了这一点),但可能的后果是什么?
在这种情况下,函数和数据不会不同步(它们实际上从未发生过更改),因此逻辑不一致应该不是问题。
指针在复制时会不会暂时失效?例如,如果地址 space 是 16 位宽,那么在 8 位处理器上复制函数指针可能需要两条指令?在这种情况下,我正在使用 BeagleBoneBlack(ARM Cortex A8?),并且我猜测指针是在一条指令中复制的,并且应该始终是准确的。但是,我对处理器的了解还不够确定。
我错过了什么?
谢谢!
您有 32 位 ARM 处理器,并且是 reading/writing 两个 32 位指针,它们在 32 位 ARM 上以原子方式读写。这是关于此的 post:ARM: Is writing/reading from int atomic?
考虑到这一点,而且每次都存储相同的值,所以它不可能出错,如果生成的代码符合预期。
但是可能仍然存在问题,因为虽然加载和存储指针是原子的,但 C 语言不允许数据竞争,因此您的编译器可能会假设地注意到这种数据竞争并编译出与您期望的完全不同的东西。如果您在没有优化的情况下进行编译,或者如果代码没有足够内联以使编译器注意到正在发生的事情,则这种情况极不可能发生。但是,如果编译器确实看到了竞争,那么生成不符合您预期的代码将在标准的字母范围内:
The execution of a program contains a data race if it contains two
conflicting actions in different threads, at least one of which is not
atomic, and neither happens before the other. Any such data race
results in undefined behavior.
-- C Standard section 5.1.2.4, paragraph 25 [ISO/IEC 9899:2011]
如果你想保证正确性,我想你有以下三种选择:
- 检查生成的汇编代码(每次更改代码或编译器版本时)以验证加载和存储是否符合预期。
- 用已知这些操作是合法的汇编代码替换这些小块 C 代码。
- 更改代码以避免 C 中的数据竞争。这是我的偏好,正如下面的评论中所指出的,可能不会更改生成的程序集。
看这里,特别是“2.4冗余写入”:https://www.usenix.org/legacy/event/hotpar11/tech/final_files/Boehm.pdf
我正在使用一些包含以下比赛的第三方库代码,简化显示:
struct Object
{
void (*func)(void *);
void *data;
};
void called_from_thread1(struct Object *obj)
{
obj->func(obj->data);
}
void called_from_thread2(struct Object *obj, void (*func)(void *), void *data)
{
obj->func = func;
obj->data = data;
}
两个线程的访问是不同步的,所以一般我会这样分析:
func
上的数据竞赛
data
上的数据竞赛
- 对
func
/data
对的访问不一致(只有一个被更改时可能会发生访问)
然而,在这个特定的用例中,值 总是 改回默认值。像这样:
void default_func(void *data) { /* snip */ };
void *default_data = 0; // Actually something valid
struct Object object = {
.func = default_func,
.data = &default_data,
};
线程 1 和线程 2 都引用 object
生成,线程 2 将同一个对象设置回默认值,类似于:
called_from_thread1(&object);
// elsewhere...
called_from_thread2(&object, default_func, &default_data);
从技术上讲,这仍然是一场竞赛(线程清理程序报告了这一点),但可能的后果是什么?
在这种情况下,函数和数据不会不同步(它们实际上从未发生过更改),因此逻辑不一致应该不是问题。
指针在复制时会不会暂时失效?例如,如果地址 space 是 16 位宽,那么在 8 位处理器上复制函数指针可能需要两条指令?在这种情况下,我正在使用 BeagleBoneBlack(ARM Cortex A8?),并且我猜测指针是在一条指令中复制的,并且应该始终是准确的。但是,我对处理器的了解还不够确定。
我错过了什么?
谢谢!
您有 32 位 ARM 处理器,并且是 reading/writing 两个 32 位指针,它们在 32 位 ARM 上以原子方式读写。这是关于此的 post:ARM: Is writing/reading from int atomic?
考虑到这一点,而且每次都存储相同的值,所以它不可能出错,如果生成的代码符合预期。
但是可能仍然存在问题,因为虽然加载和存储指针是原子的,但 C 语言不允许数据竞争,因此您的编译器可能会假设地注意到这种数据竞争并编译出与您期望的完全不同的东西。如果您在没有优化的情况下进行编译,或者如果代码没有足够内联以使编译器注意到正在发生的事情,则这种情况极不可能发生。但是,如果编译器确实看到了竞争,那么生成不符合您预期的代码将在标准的字母范围内:
The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.
-- C Standard section 5.1.2.4, paragraph 25 [ISO/IEC 9899:2011]
如果你想保证正确性,我想你有以下三种选择:
- 检查生成的汇编代码(每次更改代码或编译器版本时)以验证加载和存储是否符合预期。
- 用已知这些操作是合法的汇编代码替换这些小块 C 代码。
- 更改代码以避免 C 中的数据竞争。这是我的偏好,正如下面的评论中所指出的,可能不会更改生成的程序集。
看这里,特别是“2.4冗余写入”:https://www.usenix.org/legacy/event/hotpar11/tech/final_files/Boehm.pdf