我很难确定D中shared
类型限定符的语义。具体地说,是将一个未共享的局部变量强制转换为共享,而没有实际复制内容或将强制转换的结果分配给共享本地变量足以保证线程之间的可见性?
import std.concurrency;
import std.random : uniform;
import std.stdio : writefln;
enum size_t N_STOPS = 10;
enum size_t N_DESTS = 10;
enum size_t N_PACKETS = 5;
class Packet {
Tid[N_STOPS] log;
size_t idx = 0;
bool visit(Tid tid) {
assert(idx < N_STOPS);
log[idx++] = tid;
return idx < N_STOPS;
}
void print(size_t idNum) {
writefln("Packet %d: visited %d threads.", idNum, idx);
for (size_t i = 0; i < idx; ++i) {
string tid;
log[i].toString(delegate (const(char)[] sink) { tid ~= sink.idup; });
writefln("\t%d: %s", i, tid);
}
}
}
shared Tid sender;
shared Tid[N_DESTS] destinations;
void awaitVisitor() {
try {
for(;;) {
Packet packet = cast() receiveOnly!(shared Packet);
bool continueJourney = packet.visit(thisTid());
Tid dest;
if (continueJourney)
dest = cast() destinations[uniform(0, N_DESTS)];
else
dest = cast() sender;
send(dest, cast(shared) packet);
}
} catch (Exception ignore) {
// program is terminating
}
}
void main() {
sender = cast(shared) thisTid();
for (size_t i = 0; i < N_DESTS; ++i)
destinations[i] = cast(shared) spawn(&awaitVisitor);
for (size_t i = 0; i < N_PACKETS; ++i) {
Tid dest = cast() destinations[uniform(0, N_DESTS)];
Packet packet = new Packet();
send(dest, cast(shared) packet);
}
for (size_t i = 0; i < N_PACKETS; ++i)
(cast() receiveOnly!(shared Packet)).print(i);
}
shared
的强制转换是否足以保证toSend
的全部内容在接收线程中可见?cast(shared) &someStruct
。基本上,从void*
到shared(void*)
的转换是否可以保证通过该指针执行的所有先前写入的可见性?send
and receive
from std.concurrency,是否需要添加其他障碍或同步?我正在线程之间传输大量数据。 我不想复制它们。由于这些转移发生在谨慎的位置(即接收工作发送),因此我不希望与shared
类型限定符相关的任何行李(例如,被迫使用原子操作,任何禁用的优化,其他不必要的内存屏障等)。
内存模型是令人讨厌的事情,违反它们通常是一个非常糟糕的主意。在许多地方,我已经看到它指出,编译器可能严格假定未共享变量只能从当前线程访问。因此,我正在尝试验证将数据传递给函数时要进行的强制转换是否足以确保超出当前线程的可见性。到目前为止,它似乎在实践中可行,但是对于拥有这样的超级大国的演员来说,至少感觉有点奇怪。我宁愿以后不知道我实际上是在依赖未定义的行为。例如,在C ++或Java中,我将需要在传输点的每一侧手动指定任何必要的内存屏障,采用互斥锁或无锁数据结构,并可选地使本地引用无效,以防止以后意外访问
环顾四周,我发现一些示例似乎与我所描述的with pointers和with structs大致相符,但我不认为这些可以作为官方文档使用。从第二个链接:
使用互斥锁保护共享对象,并在互斥锁锁定时暂时放弃共享,以便您实际上可以对该对象执行某些操作-然后确保释放互斥锁时不存在线程本地引用
请注意,在这种情况下,共享已被删除,而不是添加,这对我来说似乎是一个重要的细节。
the FAQ的用语似乎强烈暗示着共享和非共享之间的强制转换是已定义的行为,前提是您决不要尝试一次使用两个线程中的非共享数据。
检查Type Qualifiers Spec,我们发现程序员在显式强制转换限定符时必须验证正确性。不幸的是,这实际上并没有告诉我们有关何时允许共享和不共享之间切换的任何信息。
否则,当不允许使用隐式版本时,可以使用CastExpression强制转换,但这不能在@safe代码中完成,并且它的正确性必须由用户验证。
- 一个线程发出的共享数据的读写顺序与源代码指定的顺序相同。
- 共享数据的全局读写顺序是对来自多个线程的读写的某种交织。
...
共享访问必须被称为内存屏障的特殊机器代码指令所包围,以确保共享数据的读取和写入顺序与所有正在运行的线程所看到的顺序相同
...
这两个限制相结合,导致戏剧性的减速-多达一个数量级。
...
编译器使用非共享数据来最大程度地优化代码,完全相信没有其他线程可以访问它,只有在共享数据附近才有脚步。 [强调我的]
答案 0 :(得分:0)
shared
通常令人困惑……但这是一种解释的尝试。
shared
仅仅是编译器对待变量的方式,而与已编译代码中的数据结构本身没有任何区别。除非变量在全局范围内,否则shared
具有隐式__gshared
。
shared
是一个类型限定符,基本上说“您不能对此做任何事”,唯一要做的方法是抛弃shared
。或者,调用一个带有shared
参数的函数(该函数在内部会将其抛弃或对其进行低级处理)。
shared int a;
a++; // Error: read-modify-write operations are not allowed for shared variables.
(cast()a)++; // Works, but be sure that another thread will not be accessing `a`!
共享的意义是什么?只是,除了在明确可以确保线程内存访问的位置进行操作之外,您无法对其进行任何操作。
因此,您的示例
/* Thread 1 */
Data toSend = ...;
... // do stuff
send(tid_a, cast(shared) toSend);
// Thread 1 can still do anything with `toSend` as `toSend` is still not `shared`.
// Casting a variable does not change the type of the variable, as D is a statically typed language, you cannot ever change the type of a variable.
/* Thread 1 */
shared Data toSend = ...;
... // do stuff by using `cast()toSend` every time
{
Data toSendAccess = cast()toSend;
... // do stuff by using toSendAccess
}
send(tid_a, cast(shared) toSend);
或
/* Thread 1 */
{
Data toSend = ...;
... // do stuff
send(tid_a, cast(shared) toSend);
} // now that you cannot guarantee thread safety, get rid of your reference.