I tried a lock free single producer-single consumer implementation link. The implementation uses list as the underlying data structure and rely on the fact that only producer thread modifies the list.The consumer thread moves the head variable if _head is not equal to _tail.
int produced_count_, consumed_count_;
std::list<int> data_queue_;
std::list<int>::iterator head_, tail_;
void ProducerConumer::produce() {
static int count = 0;
data_queue_.push_back(int(count++));
++produced_count_;
tail_ = data_queue_.end();
data_queue_.erase(data_queue_.begin(), head_);
}
bool ProducerConumer::consume() {
auto it = head_;
++it;
if(it != tail_) {
head_ = it;
++consumed_count_;
int t = *it;
return true;
}
return false;
}
At any point head iterator points to a value that has already been read.
As there is no synchronized here, I was under the impression that the implementation would not work as the writes by one thread might not be visible to the other thread. But when I tested my code producer and consumer always produced/consumed the same number of units. Can someone explain how this code can work without explicit synchronization? ( I did not expect changes to tail_ and head_ variable visible to the other thread)
Code to control producer/consumer thread is as follows
consumer_thread_ = std::thread([this]() {
set_cpu_affinity(0);
std::chrono::milliseconds start_time = current_time();
while((current_time() - start_time) < std::chrono::milliseconds(150)) {
this->consume();
}
std::cout << "Data queue size from consumer is " << data_queue_.size() << " time " << current_time().count() << "\n";
});
producer_thread_ = std::thread([this]() {
set_cpu_affinity(7);
std::chrono::milliseconds start_time = current_time();
while((current_time() - start_time) < std::chrono::milliseconds(100)) {
this->produce();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "Data queue size from producer is " << data_queue_.size() << " time " << current_time().count() << "\n";
});
I ensure that the producer thread outlives consumer thread by adding the sleep_for at the end of the producer thread.
BTW, here is a dissection of the implementation where Herb Sutter discussed what's wrong with it link. But he never talked about whether changes to tail_ and head_ are visible to other thread.
std::vector
with atomic numerical indexes rather thanstd::list::iterator
which has no atomicity guarantees – Alan Birtles