在线程等待条件变量进入wait状态的时候,需要使用循环来判断条件是否成立。因为wait被唤醒,不一定是被signal了,还有其它可能。
每个cpu核心都有单独的L1 L2缓存,缓存的基本单位不是字节,而是cache line,一般为64字节,所以每次读取数据,都会读取相应的字节数。这样就可能会产生性能问题。假设有一个数组,为了降低锁损耗,每个线程使用数组里相应的元素。多个线程一起运行后,就会使得该数组读入每个cpu的cache里,均为S(share,多cpu cache相同数据缓存)状态。当某个线程需要修改自己的元素的时候,需要获得拥有权,修改当前cache line,于是cache line的状态会变为M(Modified),同时为了保证数据一致性,必须是得其它核心的缓存失效(I)。由于读每个元素的时候读了64字节,会把其余的数据读到缓存里,所以每次对自身线程cache line的失效都会影响到别的线程,导致不断的失效,重新读取,会很影响性能。
可以通过将变量填充为64字节解决。
核心在于写入的时候,拷贝数据的副本进行修改。等待其余写入操作完成后,修改副本值原始数据中。难点在于修改副本至原始数据的时机。网上大概看了下,比较简单并且好理解的方法是使用了阶段计数器。
原理:
读的时候新增计数,读完后减少计数,当计数为0的时候,检查是否有副本,然后覆盖原始数据。这是最简单的想法,但也仅仅是想法,因为写入后的等待过程中,还会有源源不断的读过来,那么计数器为0是很难的。
为了解决这个问题,我们把原先的计数器分为2个,分别是old reader counter和new reader counter。在任意时刻(这儿理解有点不好,按我的理解应该在写入的时刻,这里有疑问),将原先的计数器停止计数,作为old reader counter,后续所有的计数都在new中。这样old只会降不会增,当old减少到0的时候,执行覆盖操作。
COW也是一种能很大程度上提高并发性能的技术。
核心思想是假设要进行修改,则会拷贝一份完整的数据,修改好后写入原值。借助shared_ptr
可以很好的完成这项工作。
下面看一下简单的代码片段:
void read() {
std::shared_ptr<COWTaskList> tptr;
// Read the pointer to make a copy and increase the reference count
{
qmu_.lock();
tptr = queue_;
qmu_.unlock();
}
// To traverse the list
// tptr is point to the original list, and queue_ may be a new list if
// write is called
printf("Task ");
for (auto& task : *tptr) {
printf("%d ", task.id);
}
printf("\r\n");
}
void write(const COWTask& task) {
qmu_.lock();
// Check reference
if (!queue_.unique()) {
// Read in progress, make a copy
queue_.reset(new COWTaskList(*queue_));
copy_times_++;
} else {
// No read, just push to queue
}
if (queue_->size() > 5) {
queue_->pop_front();
}
queue_->push_back(task);
qmu_.unlock();
}
成员定义:
std::atomic<int> copy_times_;
std::shared_ptr<COWTaskList> queue_;
std::mutex qmu_;
在read中,访问queue_来增加queue_的计数,然后通过queue_的拷贝来访问源list。而在write中,全程持锁,判断queue_是否独享,若是,则直接修改源list;若否,则进行拷贝,修改queue_指向为拷贝,然后进行修改。
假设read获取了原始指针的拷贝,则原始指针的计数会增加,假设在这个时候write,则write会进行复制,原始指针会指向新的副本,而read依旧有原始指针,只是引用计数只有1了,在函数退出后,原始数据将会被销毁;假设read还没有获取拷贝,则write直接修改原始数据。