在主线程中填充数据时从结构数组的较低索引元素读取是否线程安全
Is it Thread Safe to read from lower index elements of an struct array while it is being filled with data in main thread
原题:
我得到了一个结构数组,并在主线程中读取它时将其填充到一个单独的线程中:
struct DataModel MyData[1024];
struct DataModel
{
bool IsFilled;
float a;
float b;
}
我有一个线程正在填充 Mydata
数组,从 0 索引到最后一个索引(上面是 1024)。
然后我从填充线程中获取最后填充的结构索引。
然后我尝试读取索引低于填充索引的元素的值。
假设当第 500 个元素被填充时,我从 MyData
数组的第 499 个元素读取值,所以我保证我没有读取正在读取的数组元素写了。
Q1:这个线程安全吗?
Q2:是否有可能发生未定义的行为或误读值?
进一步编辑:
问题编辑不当以添加更多详细信息,这就是为什么它引入了答案不一致的原因,因此我将之前的编辑分开以提高答案和接受答案的一致性。
编辑 1:
这是可能实施的建议。虽然它可能会显示错误的结果,但我只是想询问线程安全和未定义的行为,以下解决方案可能会显示各种结果,但我试图先询问线程安全。
std::atomic<int> FilledIndex;
void FillingMyData(struct DataModel myData[])
{
for(size_t i = 0; i < 1024; i++)
{
myData[i].a = rand();
myData[i].b = rand();
myData[i].IsFilled = true;
FilledIndex = i;
}
}
int main()
{
std::thread ReadThread(FillingMyData, MyData);
while(FilledIndex < 1024)
{
std::cout << MyData[FilledIndex].a;
}
ReadThread.join();
return 0;
}
是的,在同一个数组中处理不同的对象是安全的。尽管数组是一个对象,但我们正在处理的是数组的元素,而这些元素是单独的对象。只要您不读取编写器正在写入的元素,就不会发生数据竞争,并且代码具有已定义的行为。您发布的代码确实存在同步问题,但此处的其他答案涵盖了这些问题。
这里可能发生的事情就是所谓的false sharing。在这些情况下发生的情况是,单独的对象位于内存中的同一缓存行中。当 core/thread 更改一个对象时,该缓存行被标记为已更改。这意味着另一个 core/thread 必须重新同步该行以引入任何更改,这意味着 cores/threads 不能同时 运行。不过,这只是性能损失,程序仍会给出正确的结果,只是速度变慢了。
所写的代码不一定安全,可能没有任何用处。
- FilledIndex 的初始值为零,因此它可以在写入之前从索引零读取数据,包括可能部分写入的值。您可能希望将其设置为 -1 并在输出之前等待它 >= 0。
- 没有什么可以阻止主线程无限期地执行,永远输出相同的零值——这取决于调度程序;也没有什么可以阻止主线程多次输出给定索引处的值。您可能希望从零计数到填充索引,而不是输出填充索引处的值。
以上问题可能意味着您选择了不同的方式将填充索引的值传回主线程。
代码存在数据竞争,将永远循环下去。
数据竞争的发生是因为 FilledIndex
的初始值是 0
因此,第一次通过循环时,您正在从正在写入的同一索引读取(因为 i == 0
).
循环永远不会结束,因为 i
将 永远不会 达到终止值 - 循环将在将 FilledIndex
设置为 1024
之前退出.
我认为这段代码绝对不是线程安全的。
首先,变量 FilledIndex
未初始化:如 cplusplus.com 所述,如果您不向构造函数提交任何值,原子变量将处于未初始化状态。
这可能会导致意外行为。
另一个问题是主线程中的退出条件,因为ReadThread
中的for语句循环到1023,所以FilledIndex
永远不会假定值1024,主线程永远不会退出。
但主要问题是线程调度的不可预测性:是什么确保 ReadThread
在主线程之后执行?没有什么!
因此您无法确定是否在数组的所有值上循环。事实上,如果你尝试多次执行你的程序,你会发现每次的输出都是不同的,并且打印出不同的数组值。
比如我们将ReadThread
命名为T,主线程命名为M,数组命名为A,这些是可能的调度(假设 A 的大小为 5 以实现 semplicity):
- T T T M T 输出将为 A[2]
- M M T M T 输出将是 A[0] A[0] A[1]
事实上,您正在打印 A[FilledIndex
] 并且您无法预测 FilledIndex
将如何更新,因为它取决于线程调度。
我希望你能理解我想说的。如有任何问题或说明,显然我在这里!我会尽快回复!
原题:
我得到了一个结构数组,并在主线程中读取它时将其填充到一个单独的线程中:
struct DataModel MyData[1024];
struct DataModel
{
bool IsFilled;
float a;
float b;
}
我有一个线程正在填充
Mydata
数组,从 0 索引到最后一个索引(上面是 1024)。然后我从填充线程中获取最后填充的结构索引。
然后我尝试读取索引低于填充索引的元素的值。
假设当第 500 个元素被填充时,我从
MyData
数组的第 499 个元素读取值,所以我保证我没有读取正在读取的数组元素写了。
Q1:这个线程安全吗?
Q2:是否有可能发生未定义的行为或误读值?
进一步编辑:
问题编辑不当以添加更多详细信息,这就是为什么它引入了答案不一致的原因,因此我将之前的编辑分开以提高答案和接受答案的一致性。
编辑 1: 这是可能实施的建议。虽然它可能会显示错误的结果,但我只是想询问线程安全和未定义的行为,以下解决方案可能会显示各种结果,但我试图先询问线程安全。
std::atomic<int> FilledIndex;
void FillingMyData(struct DataModel myData[])
{
for(size_t i = 0; i < 1024; i++)
{
myData[i].a = rand();
myData[i].b = rand();
myData[i].IsFilled = true;
FilledIndex = i;
}
}
int main()
{
std::thread ReadThread(FillingMyData, MyData);
while(FilledIndex < 1024)
{
std::cout << MyData[FilledIndex].a;
}
ReadThread.join();
return 0;
}
是的,在同一个数组中处理不同的对象是安全的。尽管数组是一个对象,但我们正在处理的是数组的元素,而这些元素是单独的对象。只要您不读取编写器正在写入的元素,就不会发生数据竞争,并且代码具有已定义的行为。您发布的代码确实存在同步问题,但此处的其他答案涵盖了这些问题。
这里可能发生的事情就是所谓的false sharing。在这些情况下发生的情况是,单独的对象位于内存中的同一缓存行中。当 core/thread 更改一个对象时,该缓存行被标记为已更改。这意味着另一个 core/thread 必须重新同步该行以引入任何更改,这意味着 cores/threads 不能同时 运行。不过,这只是性能损失,程序仍会给出正确的结果,只是速度变慢了。
所写的代码不一定安全,可能没有任何用处。
- FilledIndex 的初始值为零,因此它可以在写入之前从索引零读取数据,包括可能部分写入的值。您可能希望将其设置为 -1 并在输出之前等待它 >= 0。
- 没有什么可以阻止主线程无限期地执行,永远输出相同的零值——这取决于调度程序;也没有什么可以阻止主线程多次输出给定索引处的值。您可能希望从零计数到填充索引,而不是输出填充索引处的值。
以上问题可能意味着您选择了不同的方式将填充索引的值传回主线程。
代码存在数据竞争,将永远循环下去。
数据竞争的发生是因为 FilledIndex
的初始值是 0
因此,第一次通过循环时,您正在从正在写入的同一索引读取(因为 i == 0
).
循环永远不会结束,因为 i
将 永远不会 达到终止值 - 循环将在将 FilledIndex
设置为 1024
之前退出.
我认为这段代码绝对不是线程安全的。
首先,变量 FilledIndex
未初始化:如 cplusplus.com 所述,如果您不向构造函数提交任何值,原子变量将处于未初始化状态。
这可能会导致意外行为。
另一个问题是主线程中的退出条件,因为ReadThread
中的for语句循环到1023,所以FilledIndex
永远不会假定值1024,主线程永远不会退出。
但主要问题是线程调度的不可预测性:是什么确保 ReadThread
在主线程之后执行?没有什么!
因此您无法确定是否在数组的所有值上循环。事实上,如果你尝试多次执行你的程序,你会发现每次的输出都是不同的,并且打印出不同的数组值。
比如我们将ReadThread
命名为T,主线程命名为M,数组命名为A,这些是可能的调度(假设 A 的大小为 5 以实现 semplicity):
- T T T M T 输出将为 A[2]
- M M T M T 输出将是 A[0] A[0] A[1]
事实上,您正在打印 A[FilledIndex
] 并且您无法预测 FilledIndex
将如何更新,因为它取决于线程调度。
我希望你能理解我想说的。如有任何问题或说明,显然我在这里!我会尽快回复!