我不明白如何在C ++ 11中实现乐观并发

时间:2018-03-26 06:44:16

标签: c++ concurrency optimistic-locking optimistic-concurrency

我试图实现一个不使用C ++ 11中的锁的受保护变量。我已经阅读了一些关于乐观并发的内容,但我无法理解它既不能用C ++也不能用任何语言实现。

我尝试实现乐观并发的方式是使用最后修改ID'。我正在做的过程是:

  • 获取最后修改ID的副本。
  • 修改受保护的值。
  • 将修改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();
   }
}

我知道上面的代码是错误的,但我不明白如何以正确的方式实现上述内容,没有错误。

4 个答案:

答案 0 :(得分:1)

这里的关键是获取 - 释放语义和测试 - 增量。获取 - 释放语义是您执行操作顺序的方式。测试和增量是您在竞赛中选择哪个线程获胜的方式。

因此,您的问题是.store(lastModificationId+1)。您需要.fetch_add(1)。它返回旧值。如果这不是预期的值(来自之前你的阅读),那么你就输掉了比赛并重试。

答案 1 :(得分:1)

乐观并发并不意味着你不使用锁,它只是意味着你在大多数操作过程中都没有保持锁。

您的想法是将修改分为三个部分:

  1. 初始化,就像获取lastModificationId一样。这部分可能需要锁定,但不一定。
  2. 实际计算。所有昂贵或阻塞代码都在这里(包括任何磁盘写入或网络代码)。结果的编写方式使它们不会模糊以前的版本。它可能的工作方式是将新值存储在旧值旁边,由尚未提交的版本索引。
  3. 原子提交。此部分已锁定,必须简短,简单且无阻塞。它可能的工作方式是它只是碰撞版本号 - 在确认后,在此期间没有其他版本提交。此阶段没有数据库写入。
  4. 这里的主要假设是计算部分比提交部分贵得多。如果你的修改很简单并且计算成本低廉,那么你可以使用一个更简单的锁。

    构成这三个部分的一些示例代码可能如下所示:

    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)

如果我理解了您的问题,那么您的意思是确保varlastModificationId都已更改,或两者都未更改。

为什么不使用std::atomic<T>,其中T是同时包含intsize_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。它可以是这样的:

  

第一个用户读取数据和时间戳。用户处理数据一段时间,用户检查数据库中的时间戳是否自读取数据后没有变化,如果没有,则用户更新数据和时间戳。

但是,内部数据库引擎无论如何都会使用锁进行更新,在此锁定期间它会检查时间戳是否已更改,如果尚未更改,则引擎会更新数据。与悲观并发相比,锁定数据的时间更短。而且你还需要使用某种锁定。