2017-02-09 9 views
1

Я слышал, что использование неизменяемых типов данных может сделать одновременное программирование более безопасным. (См., Например, this question.) Я кодирую на 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 не может быть полностью построен, когда я пытаюсь получить доступ к нему на Thread 2.

Как я могу копировать неизменяемые данные между потоками таким образом, чтобы сделать параллелизм более безопасным, поскольку предполагается, что он должен выполнять?

+0

Я думаю, идея состоит в том, чтобы создать общий объект перед запуском нескольких потоков. – Justin

ответ

1
// Thread 1: 
sharedMemory = std::make_shared<Immutable>(1); 

// Thread 2: 
DoSomething(*sharedMemory); 

Это не пример непреложности. Общее состояние sharedMemory: не неизменный.

Неизменяемость была бы двумя разными потоками, как для чтения sharedMemory, построенного до либо нить существовала.

Если они хотят внести в него изменения, они возвращают изменения.

Обязательства по неизменности все общее состояние не может быть изменено. Вы все равно можете передавать данные в поток (через аргументы threading) или передавать данные из потока (через 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) стоимостью между потоками, а единственными служебными данными синхронизации являются подсчет ссылок.

Это еще не чистая неизменность. Но на практике эта нечистая форма неизменности дает много преимуществ и позволяет вам эффективно и безопасно делать вещи, которые в противном случае чрезвычайно опасны или дороги.

0

Вам нужна только синхронизация по переменной, если у вас есть несколько потоков, и по крайней мере из них будет записываться в переменную. С неизменным объектом вы не можете писать на него. Это означает, что у вас может быть столько потоков, сколько вы хотите читать из него без каких-либо вредных эффектов, поскольку данные никогда не могут измениться.

В этом случае вы либо статизировали бы инициализацию объекта, который является потокобезопасным, в C++ 11 и выше, либо вы инициализируете его до начала потоков, а затем делитесь им с ними.