I recommend reading the paper that introduced the term - the primary author, Herlihy, kicked off this whole business when he introduced the concept of wait-freedom 25ish years ago.
The core difference between lock-freedom and obstruction-freedom is that the latter doesn't guarantee progress if two or more threads are running (but does if only one is running).
The meat of the paper gives you want you want, an example of a dequeue that is obstruction-free but not lock-free.
To make it even simpler, I'll just describe an array-based stack which operates in the same way, and is obstruction-free but not lock free.
Imagine a stack implemented on top of an array, such that the zero or more elements of the stack are stored consecutively at the beginning of the array, followed by "null" elements in all remaining positions. Each element of the stack is stored as a tuple: (val, seq)
, where val
is the user-provided value and seq
is a sequence number which is key to the algorithm (and also avoids the ABA problem1).
To push an element onto the stack, you first locate the last element in the stack (position A[k-1]
) which is directly before the first null element (at position A[k]
). You try increment, using CAS, the sequence number of the A[k-1]
(the element doesn't change) and if successful, you try to simultaneously replace the value the null element at position A[k]
and increment its sequence number. If either CAS fails you retry.
The pop algorithm is similar, but handles the elements in the reverse order (incrementing the sequence number of the fist null element, then trying to make the last element null and incrementing its sequence number).
The correctness of this structure is described in more detail in the paper above, but basically by successfully incrementing the A[k-1]
th element you ensure that at that moment it is still the last element in the list, and you inform any concurrently racing push operations that you "get the next shot" by causing their initial CAS to fail. If the second CAS succeeds you successfully added an element.
Note that a concurrent push and pop operation can cause each other to fail, indefinitely. The push thread increments A[k-1]
and the pop increments A[k]
, and then each fails as they see the increment of the other. Rinse and repeat. This is an example of livelock.
Note that the problem goes away if only one thread is running, which is the key observation in obstruction-freedom.
1 ... it also avoids "wraparound" problems in the full version of the dequeue, but I don't think that happens in the case of the stack.