在线程之间共享数据时,如何从不变性中受益?

时间:2017-02-09 18:33:26

标签: c++ concurrency immutability

我听说使用不可变数据类型可以使并发编程更安全。 (例如,参见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指向的值可能无法完全构造。

如何以一种使并发更安全的方式在线程之间复制不可变数据,因为不变性应该如何?

2 个答案:

答案 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()方法。如果您是唯一拥有该共享资源的人,那么它只是为您提供写访问权限。否则,它会克隆资源并保证它是唯一的,然后为您提供写访问权。

两个不同的线程printbackup线程可以访问ptr。他们不能更改另一个线程可以看到的任何数据(允许他们编辑它,但这只会修改他们的本地数据副本)。

回到主线程中,我们将文档重命名为新名称。打印和备份线程都不会看到这一点,因为它们具有不可变(逻辑)副本。

两个访问相同ptr变量的线程都不合法,但他们可以访问该ptr变量的副本

如果文档本身是由cow_ptr构建的,那么文档的“副本”只会复制内部cow_ptr;也就是说,它会原子地增加一些参考计数,而不是整个状态。

修改深层元素会涉及面包屑;您需要breadcrumb_ptr来跟踪到达给定cow_ptr所需的路线。然后它上面的.write()将重复所有内容,直到“文档”的根目录,可能会替换每个指针(使用.write()调用)。

在这个系统下,我们能够在线程之间以O(1)成本共享极大且复杂的数据结构shapeshot,并且唯一的同步开销是引用计数。

这仍然不是纯粹的不变性。但在实践中,这种不纯的不变性形式带来了许多好处,并使你能够高效安全地做出极其危险或昂贵的事情。

答案 1 :(得分:0)

如果您有多个线程并且至少有一个线程将写入变量,则只需要对变量进行同步。使用不可变对象,您无法写入它。这意味着您可以拥有从中读取的尽可能多的线程,而不会产生任何不良影响,因为数据永远不会改变。

因此,在这种情况下,您可以静态初始化C ++ 11及更高版本中线程安全的对象,或者在线程启动之前初始化它,然后与它们共享。