【发布时间】:2019-07-04 10:33:39
【问题描述】:
目标是在现代 C++ 中实现一个序列号生成器。上下文处于并发环境中。
要求 #1 类必须是单例的(所有线程通用)
要求 #2 用于数字的类型是 64 位整数。
要求#3来电者可以请求多个号码
要求 #4 此类将缓存一系列数字,然后才能为调用提供服务。因为它缓存了一个序列,所以它还必须存储上限 -> 能够返回的最大数量。
要求 #5 最后但并非最不重要的一点是,在启动时(构造函数)并且没有可用的数字可提供 (n_requested > n_avalaible),单例类必须查询数据库以获取新序列.从 DB 加载,更新 seq_n_ 和 max_seq_n_。
其界面的简要草稿如下:
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_;
}
示例只是为了阐明用例。 假设在启动时,DB 将 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
显然,使用锁和 std::mutex 的实现非常简单。
我感兴趣的是使用 std::atomic 和原子操作实现无锁版本。
我的第一次尝试如下:
int64_t seq_n_;
int64_t max_seq_n_;
改为:
std::atomic<int64_t> seq_n_;
std::atomic<int64_t> max_seq_n_;
从 DB 中获取新序列只是在原子变量中设置新值:
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_]
}
现在使用原子比较和交换技术的 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;
}
想法:
我真的很担心无锁版本。实施安全吗?如果我们忽略数据库负载部分,显然是的。当类必须从数据库加载新序列时,问题就出现了(至少在我的脑海中)。从数据库更新安全吗?两个原子存储?
我的第二次尝试是将 seq_n_ 和 max_seq_n_ 组合成一个名为 sequence 的结构并使用单个原子变量 std::atomic 但编译器失败了。因为struct序列的大小大于64位。
是否可以通过使用原子标志来标记序列是否已准备好以某种方式保护 DB 部分:在等待数据库加载完成并更新两个原子变量时标志设置为 false。因此,必须更新 get_sequence 以等待 flag 设置为 true。 (使用自旋锁?)
【问题讨论】:
标签: c++ c++11 thread-safety atomic compare-and-swap