我听说使用不可变数据类型可以使并发编程更安全。 (例如,参见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);
但我仍然需要使用锁定或某种障碍来使这段代码线程安全,因为当我尝试在线程2上访问它时,sharedMemory指向的值可能无法完全构造。
如何以一种使并发更安全的方式在线程之间复制不可变数据,因为不变性应该如何?
答案 0 :(得分:1)
// 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
构建的,那么文档的“副本”只会复制内部cow_ptr
;也就是说,它会原子地增加一些参考计数,而不是整个状态。
修改深层元素会涉及面包屑;您需要breadcrumb_ptr
来跟踪到达给定cow_ptr
所需的路线。然后它上面的.write()
将重复所有内容,直到“文档”的根目录,可能会替换每个指针(使用.write()
调用)。
在这个系统下,我们能够在线程之间以O(1)成本共享极大且复杂的数据结构shapeshot,并且唯一的同步开销是引用计数。
这仍然不是纯粹的不变性。但在实践中,这种不纯的不变性形式带来了许多好处,并使你能够高效安全地做出极其危险或昂贵的事情。
答案 1 :(得分:0)
如果您有多个线程并且至少有一个线程将写入变量,则只需要对变量进行同步。使用不可变对象,您无法写入它。这意味着您可以拥有从中读取的尽可能多的线程,而不会产生任何不良影响,因为数据永远不会改变。
因此,在这种情况下,您可以静态初始化C ++ 11及更高版本中线程安全的对象,或者在线程启动之前初始化它,然后与它们共享。