如何使 std::execution::par_unseq 线程安全?
How to make threads with std::execution::par_unseq thread-safe?
我正在阅读 C++ 并发实战。
它说当你使用 std::execution::par 时,你可以像下面这样为每个内部元素使用互斥锁。
#include <mutex>
#include <vector>
class X {
mutable std::mutex m;
int data;
public:
X() : data(0) {}
int get_value() const {
std::lock_guard guard(m);
return data;
}
void increment() {
std::lock_guard guard(m);
++data;
}
};
void increment_all(std::vector<X>& v) {
std::for_each(v.begin(), v.end(), [](X& x) { x.increment(); });
}
但是它说当你使用 std::execution::par_unseq 时,你必须用下面的整个容器互斥体替换这个互斥体
#include <mutex>
#include <vector>
class Y {
int data;
public:
Y() : data(0) {}
int get_value() const { return data; }
void increment() { ++data; }
};
class ProtectedY {
std::mutex m;
std::vector<Y> v;
public:
void lock() { m.lock(); }
void unlock() { m.unlock(); }
std::vector<Y>& get_vec() { return v; }
};
void incremental_all(ProtectedY& data) {
std::lock_guard<ProtectedY> guard(data);
auto& v = data.get_vec();
std::for_each(std::execution::par_unseq, v.begin(), v.end(),
[](Y& y) { y.increment(); });
}
但是即使使用第二个版本,并行算法线程中的y.increament() 也会出现数据竞争条件,因为并行算法线程之间没有锁。
带有 std::execution::par_unseq 的第二个版本如何保证线程安全?
它只是线程安全的,因为您不访问并行算法中的共享数据。
唯一并行执行的是对 y.increment()
的调用。这些可以以任何顺序在任何线程上发生,并且可以任意地相互交错,甚至在单个线程中也是如此。但是 y.increment()
只访问 y
的私有数据,并且每个 y
都与所有其他向量元素不同。所以这里没有数据竞争的机会,因为各个元素之间没有“重叠”。
一个不同的例子是,如果 increment
函数也访问一些在向量的所有不同元素之间共享的全局状态。在这种情况下,现在可能会出现数据竞争,因此需要同步对共享全局状态的访问。但是因为并行无序策略的特殊要求,这里不能只用mutex来同步。
请注意,如果在并行算法的上下文中使用互斥锁,它可能会防止不同的危害:一种用途是使用互斥锁在执行 for-each 的不同线程之间进行同步。这适用于并行执行策略,但不适用于 并行无序。这不是您的示例中的用例,因为在您的情况下没有共享数据,因此我们不需要任何同步。相反,在您的示例中,互斥锁仅将 for-each 的调用与任何 other 线程同步,这些线程可能仍然是 运行 作为较大应用程序的一部分,但没有同步在 for-each 本身中。这是并行和并行无序的有效用例,但在后一种情况下,它不能通过使用每个元素互斥锁来实现。
我正在阅读 C++ 并发实战。 它说当你使用 std::execution::par 时,你可以像下面这样为每个内部元素使用互斥锁。
#include <mutex>
#include <vector>
class X {
mutable std::mutex m;
int data;
public:
X() : data(0) {}
int get_value() const {
std::lock_guard guard(m);
return data;
}
void increment() {
std::lock_guard guard(m);
++data;
}
};
void increment_all(std::vector<X>& v) {
std::for_each(v.begin(), v.end(), [](X& x) { x.increment(); });
}
但是它说当你使用 std::execution::par_unseq 时,你必须用下面的整个容器互斥体替换这个互斥体
#include <mutex>
#include <vector>
class Y {
int data;
public:
Y() : data(0) {}
int get_value() const { return data; }
void increment() { ++data; }
};
class ProtectedY {
std::mutex m;
std::vector<Y> v;
public:
void lock() { m.lock(); }
void unlock() { m.unlock(); }
std::vector<Y>& get_vec() { return v; }
};
void incremental_all(ProtectedY& data) {
std::lock_guard<ProtectedY> guard(data);
auto& v = data.get_vec();
std::for_each(std::execution::par_unseq, v.begin(), v.end(),
[](Y& y) { y.increment(); });
}
但是即使使用第二个版本,并行算法线程中的y.increament() 也会出现数据竞争条件,因为并行算法线程之间没有锁。
带有 std::execution::par_unseq 的第二个版本如何保证线程安全?
它只是线程安全的,因为您不访问并行算法中的共享数据。
唯一并行执行的是对 y.increment()
的调用。这些可以以任何顺序在任何线程上发生,并且可以任意地相互交错,甚至在单个线程中也是如此。但是 y.increment()
只访问 y
的私有数据,并且每个 y
都与所有其他向量元素不同。所以这里没有数据竞争的机会,因为各个元素之间没有“重叠”。
一个不同的例子是,如果 increment
函数也访问一些在向量的所有不同元素之间共享的全局状态。在这种情况下,现在可能会出现数据竞争,因此需要同步对共享全局状态的访问。但是因为并行无序策略的特殊要求,这里不能只用mutex来同步。
请注意,如果在并行算法的上下文中使用互斥锁,它可能会防止不同的危害:一种用途是使用互斥锁在执行 for-each 的不同线程之间进行同步。这适用于并行执行策略,但不适用于 并行无序。这不是您的示例中的用例,因为在您的情况下没有共享数据,因此我们不需要任何同步。相反,在您的示例中,互斥锁仅将 for-each 的调用与任何 other 线程同步,这些线程可能仍然是 运行 作为较大应用程序的一部分,但没有同步在 for-each 本身中。这是并行和并行无序的有效用例,但在后一种情况下,它不能通过使用每个元素互斥锁来实现。