Lock-free Programming Practical
Lock-free Queue
-
We will implement a simple queue
- No internal or external locks
-
The queue is only accessed by two threads
- A producer thread inserts elements into the queue
- A consumer thread removes elements from the queue
-
The code is carefully designed
- The consumer and producer threads never work on adjacent elements
- The two threads always work on different parts of the queue
-
The queue has two iterators,
iHeadandiTailiHeadpoints to the element before the oldest elementiTailpoints to the element after the newest (most recently added)
Consumer Thread
- The Consumer thread does not modify the queue
- It "removes" an element by incrementing
iHead
- It "removes" an element by incrementing
Producer Thread
- The Producer inserts elements
- It increments
iTail - It also erases any elements which the Consumer has removed
- It increments
Thread Separation
-
Only the Producer thread can modify the queue
- The Producer queue inserts elements
- The Producer queue erases elements
-
The two threads never overlap
iHeadandiTailnever refer to the same element- The Producer thread never modifies
iHead - The Consumer thread never accesses elements after
iHead
template <typename T> struct LockFreeQueue {
private:
std::list<T> list;
typename std::list<T>::iterator iHead, iTail;
public:
LockFreeQueue() {
list.push_back(T()); // Create a "dummy" element
iHead = list.begin();
iTail = list.end();
}
bool Consume(T &t) {
auto iFirst = iHead; // Go to the first element
++iFirst;
if (iFirst != iTail) { // If queue is not empty
iHead = iFirst; // update iHead
t = *iHead; // Fetch this first element
return true;
}
return false; // No elements to fetch
}
void Produce(const T &t) {
list.push_back(t); // Add the new element
iTail = list.end(); // Update tail
list.erase(list.begin(), iHead); // Erase the removed elements
}
void Print() {
auto head = iHead;
++head;
for (auto el = head; el != iTail; ++el) {
std::cout << *el << ", ";
}
std::cout << '\n';
}
};
int main() {
LockFreeQueue<int> lfq;
std::vector<std::thread> threads;
int j = 1;
for (int i = 0; i < 10; ++i) {
std::thread produce(&LockFreeQueue<int>::Produce, &lfq, std::ref(i));
threads.push_back(std::move(produce));
std::thread consume(&LockFreeQueue<int>::Consume, &lfq, std::ref(j));
threads.push_back(std::move(consume));
}
for (auto &thr : threads) {
thr.join();
}
lfq.Print();
}
Thread Safety
- The code has a data race
iHeadandiTailcan be accessed from different threads- At least one thread modifies them
- The threads are not synchronized when they access these variables
Avoiding the Data Race
-
iHeadandiTailcannot be atomic typesstd::list<T>::iteratoris not trivially copyable
-
We must use mutexes