我试图实现一个不使用C ++ 11中的锁的受保护变量。我已经阅读了一些关于乐观并发的内容,但我无法理解它既不能用C ++也不能用任何语言实现。
我尝试实现乐观并发的方式是使用最后修改ID'。我正在做的过程是:
我看到的问题是,经过比较后续修改ID' (本地副本和当前副本)并且在提交更改之前,无法确保没有其他线程修改受保护变量的值。
下面是一个代码示例。让我们假设有许多线程执行该代码并共享变量var
。
/**
* This struct is pretended to implement a protected variable,
* but using optimistic concurrency instead of locks.
*/
struct ProtectedVariable final {
ProtectedVariable() : var(0), lastModificationId(0){ }
int getValue() const {
return var.load();
}
void setValue(int val) {
// This method is not atomic, other thread could change the value
// of val before being able to increment the 'last modification id'.
var.store(val);
lastModificationId.store(lastModificationId.load() + 1);
}
size_t getLastModificationId() const {
return lastModificationId.load();
}
private:
std::atomic<int> var;
std::atomic<size_t> lastModificationId;
};
ProtectedVariable var;
/**
* Suppose this method writes a value in some sort of database.
*/
int commitChanges(int val){
// Now, if nobody has changed the value of 'var', commit its value,
// retry the transaction otherwise.
if(var.getLastModificationId() == currModifId) {
// Here is one of the problems. After comparing the value of both Ids, other
// thread could modify the value of 'var', hence I would be
// performing the commit with a corrupted value.
var.setValue(val);
// Again, the same problem as above.
writeToDatabase(val);
// Return 'ok' in case of everything has gone ok.
return 0;
} else {
// If someone has changed the value of var while trying to
// calculating and commiting it, return error;
return -1;
}
}
/**
* This method is pretended to be atomic, but without using locks.
*/
void modifyVar(){
// Get the modification id for checking whether or not some
// thread has modified the value of 'var' after commiting it.
size_t currModifId = lastModificationId.load();
// Get a local copy of 'var'.
int currVal = var.getValue();
// Perform some operations basing on the current value of
// 'var'.
int newVal = currVal + 1 * 2 / 3;
if(commitChanges(newVal) != 0){
// If someone has changed the value of var while trying to
// calculating and commiting it, retry the transaction.
modifyVar();
}
}
我知道上面的代码是错误的,但我不明白如何以正确的方式实现上述内容,没有错误。
答案 0 :(得分:1)
这里的关键是获取 - 释放语义和测试 - 增量。获取 - 释放语义是您执行操作顺序的方式。测试和增量是您在竞赛中选择哪个线程获胜的方式。
因此,您的问题是.store(lastModificationId+1)
。您需要.fetch_add(1)
。它返回旧值。如果这不是预期的值(来自之前你的阅读),那么你就输掉了比赛并重试。
答案 1 :(得分:1)
乐观并发并不意味着你不使用锁,它只是意味着你在大多数操作过程中都没有保持锁。
您的想法是将修改分为三个部分:
这里的主要假设是计算部分比提交部分贵得多。如果你的修改很简单并且计算成本低廉,那么你可以使用一个更简单的锁。
构成这三个部分的一些示例代码可能如下所示:
struct Data {
...
}
...
std::mutex lock;
volatile const Data* value; // The protected data
volatile int current_value_version = 0;
...
bool modifyProtectedValue() {
// Initialize.
int version_on_entry = value_version;
// Compute the new value, using the current value.
// We don't have any lock here, so it's fine to make heavy
// computations or block on I/O.
Data* new_value = new Data;
compute_new_value(value, new_value);
// Commit or fail.
bool success;
lock.lock();
if (current_value_version == version_on_entry) {
value = new_value;
current_value_version++;
success = true;
} else {
success = false;
}
lock.unlock();
// Roll back in case of failure.
if (!success) {
delete new_value;
}
// Inform caller about success or failure.
return success;
}
// It's cleaner to keep retry logic separately.
bool retryModification(int retries = 5) {
for (int i = 0; i < retries; ++i) {
if (modifyProtectedValue()) {
return true;
}
}
return false;
}
这是一种非常基本的方法,尤其是回滚是微不足道的。在现实世界中,重新创建整个Data对象(或它的对应物)的示例可能是不可行的,因此版本控制必须在内部某处完成,并且回滚可能要复杂得多。但我希望它能显示出一般的想法。
答案 2 :(得分:0)
如果我理解了您的问题,那么您的意思是确保var
和lastModificationId
都已更改,或两者都未更改。
为什么不使用std::atomic<T>
,其中T
是同时包含int
和size_t
的结构?
struct VarWithModificationId {
int var;
size_t lastModificationId;
};
class ProtectedVariable {
private std::atomic<VarWithModificationId> protectedVar;
// Add your public setter/getter methods here
// You should be guaranteed that if two threads access protectedVar, they'll each get a 'consistent' view of that variable, but the setter will need to use a lock
};
答案 3 :(得分:0)
当预期不同的用户很少访问相同的数据时,会在数据库引擎中使用¶ptimisticconcurrency。它可以是这样的:
第一个用户读取数据和时间戳。用户处理数据一段时间,用户检查数据库中的时间戳是否自读取数据后没有变化,如果没有,则用户更新数据和时间戳。
但是,内部数据库引擎无论如何都会使用锁进行更新,在此锁定期间它会检查时间戳是否已更改,如果尚未更改,则引擎会更新数据。与悲观并发相比,锁定数据的时间更短。而且你还需要使用某种锁定。