У меня возникли проблемы с определением семантики спецификатора типа shared
в 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*)
видимость всех предыдущих записей, которые были выполнены через этот указатель? - Существует ли где-нибудь формальная спецификация модели памяти D?Потому что я не смог его найти.
Дополнительные вопросы
- Нужно ли добавлять дополнительные барьеры или синхронизацию, если я использую
send
иreceive
from std.concurrency ? - Если бы я делал это вручную (без использования std.concurrency), было бы достаточно для синхронизации в общей очереди при вставке и удалении моей (в противном случае не общей)контейнер данных?
- Что, если бы я использовал только одну собственную инструкцию CAS для передачи указателя на мои данные?
Дополнительная информация
Я передаюбольшие блоки данных между потоками. Я не хочу их копировать. Поскольку эти передачи происходят в незаметных точках (например, получение-работа-отправка), я не хочу, чтобы багаж был связан с классификатором типа shared
(например,быть вынужденным использовать атомарные операции, любые отключенные оптимизации, дополнительные ненужные барьеры памяти и т. д.).
Модели памяти - педантичные вещи, и нарушать их - очень плохая идея.В ряде мест я видел, что в нем указывалось, что компилятор может строго полагать, что не используемые совместно переменные доступны только из текущего потока.Таким образом, я пытаюсь убедиться, что ничего, кроме приведения к общему виду при передаче данных в функцию, не достаточно, чтобы гарантировать видимость за пределами текущего потока.Пока что это работает на практике, но кажется, что актерский состав обладает такими сверхспособностями, как минимум, немного странно;Я бы предпочел не узнавать позже, что я на самом деле полагаюсь на неопределенное поведение.Например, в C ++ или Java мне нужно было бы вручную указать любые необходимые барьеры памяти на каждой стороне точки передачи, использовать мьютекс или структуру данных без блокировки и, при необходимости, обнулить локальную ссылку, чтобы впоследствии предотвратить случайный доступ.
Оглядываясь вокруг, я обнаружил несколько примеров, которые примерно соответствуют тому, что я описываю с указателями и со структурами , но я не чувствуюкак они квалифицируются как официальная документация.Из второй ссылки:
защищает общий объект с помощью мьютекса и временно отбрасывает общий, пока мьютекс заблокирован, чтобы вы могли что-то сделать с объектом - и затем убедитесь, что нет потока -при освобождении мьютекса существуют локальные ссылки
Обратите внимание, что в этом случае разделяется, а не добавляется , что мне кажется важной деталью.
Формулировка FAQ явно указывает на то, что приведение между общим и неразделенным является определенным поведением, при условии, что вы никогда не пытаетесь использовать неразделенные данные из двух потоков одновременно.
Проверка Type Qualifiers Spec , мы видим, что программист должен проверить правильность при явном приведении классификаторов.К сожалению, это ничего не говорит нам о том, когда фактически разрешено перемещение между общим и не общим.
В противном случае выражение CastExpression может использоваться для принудительного преобразования, когда неявная версия запрещена, но это не может быть сделано в коде @safe, и правильность его должна проверяться пользователем.
С Язык программирования D :
- Порядок чтения и записи общих данных, выданных одним потоком, соответствует порядку, указанному в исходном коде.
- Глобальный порядок чтения и записи общих данных представляет собой некоторое чередование операций чтения и записи из нескольких потоков.
...
общий доступ должен бытьв окружении специальных инструкций машинного кода, называемых барьерами памяти, гарантирующие, что порядок чтения и записи общих данных такой же, как у всех запущенных потоков
...
В сочетании два ограниченияприводит к резкому замедлению - на один порядок.
...
Компилятор оптимизирует код, используя нераспространенные данные до максимума, вполная уверенность, что никакой другой поток не сможет получить к нему доступ, и только на цыпочках вокруг общих данных.[выделение мое]