简体   繁体   English

C ++ 11无锁序列号生成器安全吗?

[英]C++11 Lock-free sequence number generator safe?

The goal is to implement a sequence number generator in modern C++. 目标是在现代C ++中实现序列号生成器。 The context is in a concurrent environment. 上下文处于并发环境中。

Requirement #1 The class must be singleton (common for all threads) 要求#1该类必须为单例(所有线程共有)

Requirement #2 The type used for the numbers is 64-bit integer. 要求#2用于数字的类型是64位整数。

Requirement #3 The caller can request more than one numbers 要求#3呼叫者可以请求多个号码

Requirement #4 This class will cache a sequence of numbers before being able serve the calls. 要求#4此类将在可以提供呼叫之前先缓存一个数字序列。 Because it caches a sequence, it must also store the upper bound -> the maximum number to be able to return. 因为它缓存序列,所以它还必须存储上限->可以返回的最大数。

Requirement #5 Last but not least, at startup (constructor) and when there are no available numbers to give ( n_requested > n_avalaible ), the singleton class must query the database to get a new sequence. 要求#5最后但并非最不重要的一点是,在启动(构造函数)并且没有可用数字可提供时(n_requested> n_avalaible),单例类必须查询数据库以获取新序列。 This load from DB, updates both seq_n_ and max_seq_n_. 来自数据库的此负载会同时更新seq_n_和max_seq_n_。

A brief draft for its interface is the following: 其界面的简要草案如下:

class singleton_sequence_manager {

public:

    static singleton_sequence_manager& instance() {
        static singleton_sequence_manager s;
        return s;
    }

    std::vector<int64_t> get_sequence(int64_t n_requested);

private:
    singleton_sequence_manager(); //Constructor
    void get_new_db_sequence(); //Gets a new sequence from DB

    int64_t seq_n_;
    int64_t max_seq_n_;
}

Example just to clarify the use case. 示例只是为了阐明用例。 Suppose that at startup, DB sets seq_n_ to 1000 and max_seq_n_ to 1050: 假设数据库在启动时将seq_n_设置为1000,将max_seq_n_设置为1050:

get_sequence.(20); //Gets [1000, 1019]
get_sequence.(20); //Gets [1020, 1039]
get_sequence.(5); //Gets [1040, 1044]
get_sequence.(10); //In order to serve this call, a new sequence must be load from DB

Obviously, an implementation using locks and std::mutex is quite simple. 显然,使用锁和std :: mutex的实现非常简单。

What I am interested into is implementing a lock-free version using std::atomic and atomic operations. 我感兴趣的是使用std :: atomic和atomic操作实现无锁版本。

My first attempt is the following one: 我的第一次尝试是以下尝试:

int64_t seq_n_;
int64_t max_seq_n_;

were changed to: 改为:

std::atomic<int64_t> seq_n_;
std::atomic<int64_t> max_seq_n_;

Get a new sequence from DB just sets the new values in the atomic variables: 从数据库获取新序列,只需在原子变量中设置新值即可:

void singleton_sequence_manager::get_new_db_sequence() {
    //Sync call is made to DB
    //Let's just ignore unhappy paths for simplicity
    seq_n_.store( start_of_seq_got_from_db );
    max_seq_n_.store( end_of_seq_got_from_db );
    //At this point, the class can start returning numbers in [seq_n_ : max_seq_n_]
}

And now the get_sequence function using atomic compare and swap technique: 现在,使用原子比较和交换技术的get_sequence函数:

std::vector<int64_t> singleton_sequence_manager::get_sequence(int64_t n_requested) {

    bool succeeded{false};
    int64_t current_seq{};
    int64_t next_seq{};

    do {

        current_seq = seq_n_.load();
        do {
            next_seq = current_seq + n_requested + 1;
        }
        while( !seq_n_.compare_exchange_weak( current_seq, next_seq ) );
        //After the CAS, the caller gets the sequence [current_seq:next_seq-1]

        //Check if sequence is in the cached bound.
        if( max_seq_n_.load() > next_seq - 1 )
            succeeded = true;
        else //Needs to load new sequence from DB, and re-calculate again
            get_new_db_sequence();

    }        
    while( !succeeded );

    //Building the response        
    std::vector<int64_t> res{};
    res.resize(n_requested);
    for(int64_t n = current_seq ; n < next_seq ; n++)
        res.push_back(n);

    return res;
}

Thoughts: 思考:

  • I am really concerned for the lock-free version. 我真的很关心无锁版本。 Is the implementation safe ? 实施安全吗? If we ignore the DB load part, obviously yes. 如果我们忽略数据库负载部分,显然可以。 The problem arises (in my head at least) when the class has to load a new sequence from the DB. 当类必须从数据库加载新序列时,就会出现问题(至少在我看来是这样)。 Is the update from DB safe ? DB的更新安全吗? Two atomic stores ? 两个原子商店?

  • My second attempt was to combine both seq_n_ and max_seq_n_ into a struct called sequence and use a single atomic variable std::atomic but the compiler failed. 我的第二次尝试是将seq_n_和max_seq_n_组合到一个名为sequence的结构中,并使用单个原子变量std :: atomic,但是编译器失败了。 Because the size of the struct sequence is greater than 64-bit. 因为结构序列的大小大于64位。

  • Is it possible to somehow protect the DB part by using an atomic flag for marking if the sequence is ready yet: flag set to false while waiting the db load to finish and to update both atomic variables. 是否可以通过使用原子标记来标记序列是否已就绪来以某种方式保护数据库部件:在等待数据库加载完成并更新两个原子变量时将该标记设置为false。 Therefore, get_sequence must be updated in order to wait for flag to bet set to true. 因此,必须更新get_sequence才能等待将标志设置为true。 (Use of spin lock ?) (使用自旋锁吗?)

Your lock-free version has a fundamental flaw, because it treats two independent atomic variables as one entity. 您的无锁版本具有一个基本缺陷,因为它将两个独立的原子变量视为一个实体。 Since writes to seq_n_ and max_seq_n_ are separate statements, they can be separated during execution resulting in the use of one of them with a value that is incorrect when paired with the other. 由于对seq_n_max_seq_n_写入是单独的语句,因此可以在执行期间将它们分开,从而导致使用其中一个的值与另一个配对时不正确。

For example, one thread can get past the CAS inner while loop (with an n_requested that is too large for the current cached sequence), then be suspended before checking if it is cached. 例如,一个线程可以通过CAS内部while循环( n_requested对于当前的高速缓存序列而言太大),然后在检查是否被高速缓存之前被挂起。 A second thread can come thru and update the max_seq_n value to a larger value. 第二个线程可以通过并将max_seq_n值更新为更大的值。 The first thread then resumes, and passes the max_seq_n check because the value was updated by the second thread. 然后,第一个线程继续运行,并通过max_seq_n检查,因为该值由第二个线程更新。 It is now using an invalid sequence. 现在使用的序列无效。

A similar thing can happen in get_new_db_sequence between the two store calls. 两个store调用之间的get_new_db_sequence可能发生类似的情况。

Since you're writing to two distinct locations (even if adjacent in memory), and they cannot be updated atomically (due to the combined size of 128 bits not being a supported atomic size with your compiler), the writes must be protected by a mutex. 由于您要写入两个不同的位置(即使在内存中相邻),并且无法原子更新(由于128位的组合大小不是编译器支持的原子大小),因此必须用互斥。

A spin lock should only be used for very short waits, since it does consume CPU cycles. 自旋锁只能用于很短的等待时间,因为它确实消耗CPU周期。 A typical usage would be to use a short spin lock, and if the resource is still unavailable use something more expensive (like a mutex) to wait using CPU time. 典型的用法是使用短自旋锁,如果资源仍然不可用,则使用更昂贵的东西(例如互斥锁)来等待使用CPU时间。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM