在线程之间共享数据时如何从不变性中获益?
How can I benefit from immutability when sharing data between threads?
我听说使用不可变数据类型可以使并发编程更安全。 (参见,例如 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 指向的值可能未完全构造。
如何以一种使并发更安全的方式在线程之间复制不可变数据,就像不可变性应该做的那样?
如果您有多个线程并且至少其中一个线程将写入变量,则只需要对变量进行同步。对于不可变对象,您无法对其进行写入。这意味着您可以从中读取任意数量的线程而不会产生不良影响,因为数据永远不会改变。
所以在这种情况下,您要么静态初始化在 C++11 及更高版本中线程安全的对象,要么在线程启动之前初始化它,然后与它们共享。
// Thread 1:
sharedMemory = std::make_shared<Immutable>(1);
// Thread 2:
DoSomething(*sharedMemory);
这不是不变性的例子。 sharedMemory
的共享状态 不是 不可变的。
不变性将是两个不同的线程都读取sharedMemory
构造的在任何一个线程存在之前。
如果他们想对其进行更改,他们会return进行更改。
不变性意味着无法更改所有共享状态。您仍然可以将数据传递到线程(通过线程参数),或从线程传递数据(通过 future
)
您甚至可以制作隔离的可变共享状态,例如供工作线程使用的任务队列。这里的队列本身是可变的并且是精心编写的。工作线程消耗任务。
但任务仅在不可变的共享状态上运行,并且它们 return 通过 future
将任务排队的 return 数据发送给其他线程 returned。
未来是可变性的一种软形式。
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";
a cow_ptr
是写指针的副本。它允许 read-only 不可变访问。
如果你想改变它,你调用.write()
方法。如果您是唯一拥有该共享资源的人,它只会为您提供写入权限。否则,它会克隆资源并保证它是唯一的,然后为您提供写入权限。
两个不同的线程,print
和 backup
线程,可以访问 ptr
。他们不能更改其他线程可以看到的任何数据(允许他们编辑它,但这只会修改他们的本地数据副本)。
回到主线程,我们将文档重命名为新名称。打印线程和备份线程都不会看到这一点,因为它们有一个不可变的(逻辑)副本。
两个线程同时访问同一个 ptr
变量是不合法的,但它们可以访问那个 ptr
变量的 copy。
如果文档本身是用 cow_ptr
s 构建的,文档的 "copy" 只会复制内部的 cow_ptr
s;即,它会自动增加一些引用计数,而不是整个状态。
修改深层元素会涉及面包屑;您需要一个 breadcrumb_ptr
来跟踪到达给定 cow_ptr
所需的路线。然后它上面的 .write()
将继续复制所有内容回到 "document" 的根,可能会替换每个指针(使用 .write()
调用)。
在这个系统下,我们能够在线程之间以 O(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);
但我仍然必须使用锁定或某种屏障来使此代码线程安全,因为当我尝试在线程 2 上访问它时,sharedMemory 指向的值可能未完全构造。
如何以一种使并发更安全的方式在线程之间复制不可变数据,就像不可变性应该做的那样?
如果您有多个线程并且至少其中一个线程将写入变量,则只需要对变量进行同步。对于不可变对象,您无法对其进行写入。这意味着您可以从中读取任意数量的线程而不会产生不良影响,因为数据永远不会改变。
所以在这种情况下,您要么静态初始化在 C++11 及更高版本中线程安全的对象,要么在线程启动之前初始化它,然后与它们共享。
// Thread 1:
sharedMemory = std::make_shared<Immutable>(1);
// Thread 2:
DoSomething(*sharedMemory);
这不是不变性的例子。 sharedMemory
的共享状态 不是 不可变的。
不变性将是两个不同的线程都读取sharedMemory
构造的在任何一个线程存在之前。
如果他们想对其进行更改,他们会return进行更改。
不变性意味着无法更改所有共享状态。您仍然可以将数据传递到线程(通过线程参数),或从线程传递数据(通过 future
)
您甚至可以制作隔离的可变共享状态,例如供工作线程使用的任务队列。这里的队列本身是可变的并且是精心编写的。工作线程消耗任务。
但任务仅在不可变的共享状态上运行,并且它们 return 通过 future
将任务排队的 return 数据发送给其他线程 returned。
未来是可变性的一种软形式。
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";
a cow_ptr
是写指针的副本。它允许 read-only 不可变访问。
如果你想改变它,你调用.write()
方法。如果您是唯一拥有该共享资源的人,它只会为您提供写入权限。否则,它会克隆资源并保证它是唯一的,然后为您提供写入权限。
两个不同的线程,print
和 backup
线程,可以访问 ptr
。他们不能更改其他线程可以看到的任何数据(允许他们编辑它,但这只会修改他们的本地数据副本)。
回到主线程,我们将文档重命名为新名称。打印线程和备份线程都不会看到这一点,因为它们有一个不可变的(逻辑)副本。
两个线程同时访问同一个 ptr
变量是不合法的,但它们可以访问那个 ptr
变量的 copy。
如果文档本身是用 cow_ptr
s 构建的,文档的 "copy" 只会复制内部的 cow_ptr
s;即,它会自动增加一些引用计数,而不是整个状态。
修改深层元素会涉及面包屑;您需要一个 breadcrumb_ptr
来跟踪到达给定 cow_ptr
所需的路线。然后它上面的 .write()
将继续复制所有内容回到 "document" 的根,可能会替换每个指针(使用 .write()
调用)。
在这个系统下,我们能够在线程之间以 O(1) 的成本共享极其庞大和复杂的数据结构形状,唯一的同步开销是引用计数。
这仍然不是纯粹的不变性。但在实践中,这种不纯形式的不变性提供了许多好处,并允许您高效、安全地做一些否则极其危险或代价高昂的事情。