Как извлечь выгоду из неизменности при обмене данными между потоками?
Я слышал, что использование неизменяемых типов данных может сделать параллельное программирование более безопасным. (См., Например, этот вопрос.) Я пишу код на C++ и пытаюсь воспользоваться этими преимуществами. Но я изо всех сил пытаюсь понять концепцию.
Если я создаю неизменный тип данных, например, так:
struct Immutable
{
public:
const int x;
Immutable(const int x)
: x(x)
{}
}
И я создаю его в одном потоке, как я могу использовать его в другом потоке; т.е. я мог бы сделать:
std::shared_ptr<Immutable> sharedMemory;
// Thread 1:
sharedMemory = std::make_shared<Immutable>(1);
// Thread 2:
DoSomething(*sharedMemory);
Но мне все равно придется использовать блокировку или какие-то барьеры, чтобы сделать этот поток кода безопасным, потому что значение, на которое указывает sharedMemory, может не быть полностью сконструировано, когда я пытаюсь получить к нему доступ в потоке 2.
Как я могу скопировать неизменяемые данные между потоками таким образом, чтобы сделать параллелизм более безопасным, как и предполагалось при неизменности?
2 ответа
// Thread 1:
sharedMemory = std::make_shared<Immutable>(1);
// Thread 2:
DoSomething(*sharedMemory);
Это не пример неизменности. Общее состояние sharedMemory
не является неизменным
Неизменность будет две разные темы, оба читают sharedMemory
построен до того, как какой-либо поток существовал.
Если они хотят внести изменения в него, они возвращают изменения.
Неизменность означает, что все общее состояние не может быть изменено. Вы по-прежнему можете передавать данные в поток (через аргументы потоков) или передавать данные из потока (через future
)
Вы даже можете создать изолированное изменяемое общее состояние, например, очередь задач для рабочих потоков. Здесь сама очередь изменчива и тщательно написана. Рабочие потоки потребляют задачи.
Но задачи работают только в неизменном общем состоянии и возвращают данные в другие потоки через future
что поставленная в очередь задача вернулась.
Мягкая форма изменчивости - это фьючерсы.
std::shared_future<std::shared_ptr<Immutable>> sharedMemory = create_shared_memory_async();
std::future<void> r = DoSomethingWithSharedMemoryAsync( sharedMemory );
// in DoSomethingWithSharedMemory
auto sharedMemoryV = sharedMemory.get(); // blocks until memory is ready
DoSomething(*sharedMemory);
Это не полностью неизменное общее состояние.
Вот еще одно нечистое использование неизменного общего состояния:
cow_ptr<Document> ptr = GetCurrentDocument();
std::future<error_code> print = print_document_async(ptr);
std::future<error_code> backup = backup_document_async(ptr);
ptr.write().name = "new name";
cow_ptr
является копией указателя записи. Это разрешает постоянный доступ только для чтения.
Если вы хотите изменить это, вы называете .write()
метод. Если вы являетесь единственным владельцем этого общего ресурса, он просто дает вам право на запись. В противном случае он клонирует ресурс и гарантирует его уникальность, а затем дает вам право на запись.
Две разные темы, print
а также backup
темы, имеют доступ к ptr
, Они не могут изменять какие-либо данные, которые может видеть другой поток (им разрешено редактировать их, но это только изменит их локальную копию данных).
Вернувшись в основной поток, мы переименовываем документ в новое имя. Ни потоки печати, ни потоки резервного копирования не увидят этого, поскольку они имеют неизменяемую (логическую) копию.
Два потока оба обращаются к одному ptr
переменная недопустима, но они могут получить доступ к ее копии ptr
переменная.
Если сам документ был построен из cow_ptr
s везде, "копия" документа будет копировать только внутреннюю cow_ptr
s; то есть, он будет увеличивать атом на несколько отсчетов, а не на весь штат.
Изменение глубоких элементов будет включать в себя панировочные сухари; ты бы хотел breadcrumb_ptr
который отслеживает маршрут, необходимый для достижения данного cow_ptr
, Тогда .write()
он будет дублировать все обратно до корня "документа", возможно, заменив каждый указатель (.write()
звони) как идет.
В этой системе у нас есть возможность обмениваться между потоками чрезвычайно большими и сложными шаблонными снимками структуры данных с затратами O(1), и единственными накладными расходами синхронизации являются подсчет ссылок.
Это все еще не чистая неизменность. Но на практике эта нечистая форма неизменности дает много преимуществ и позволяет вам эффективно и безопасно делать вещи, которые в противном случае чрезвычайно опасны или дороги.
Синхронизация с переменной необходима только в том случае, если у вас более одного потока, и по крайней мере один из них запишет в переменную. С неизменным объектом вы не можете писать в него. Это означает, что вы можете иметь столько потоков, сколько хотите, чтобы прочитать их без каких-либо негативных последствий, так как данные никогда не могут измениться.
Таким образом, в этом случае вы либо статически инициализируете объект, который является потокобезопасным в C++11 и выше, либо инициализируете его до запуска потоков, а затем делитесь им с ними.