views:

162

answers:

2

Imagine a program with two threads. They are running the following code (CAS refers to Compare and Swap):

// Visible to both threads
static int test;

// Run by thread A
void foo()
{
    // Check if value is 'test' and swap in 0xdeadbeef
    while(!CAS(&test, test, 0xdeadbeef)) {}
}

// Run by thread B
void bar()
{
    while(1) {
        // Perpetually atomically write rand() into the test variable
        atomic_write(&test, rand());
    }
}

Is it possible for thread B to perpetually cause thread A's CAS to fail such that 0xdeadbeef is never written to 'test'? Or does natural scheduling jitter mean that in practice this never happens? What if some work was done inside thread A's while loop?

+3  A: 

Starvation surely can happen in such cases. Quoting the wikipedia page,

It has also been shown that the widely-available atomic conditional primitives, CAS and LL/SC, cannot provide starvation-free implementations of many common data structures without memory costs growing linearly in the number of threads. Wait-free algorithms are therefore rare, both in research and in practice.

(See the link from the page in question for the mathematical proof).

Alex Martelli
+5  A: 

As a theoretical matter, yes. If you could manage somehow to get the two threads running in lockstep like this

    time     thread A     thread B
    ----     --------     --------
     ||       CAS
     ||                   atomic_write
     ||       CAS
     \/                   atomic_write

Then CAS would never return true.

In practice this will never happen when threads share a CPU/Core, and is unlikely to happen when threads are running on different CPUs or Cores. In practice it's incredibly unlikely to happen for more than few cycles, and astronomically unlikely to happen for more than a scheduler quantum.

And that's if this code

void foo()
{
    // Check if value is 'test' and swap in 0xdeadbeef
    while(!CAS(&test, test, 0xdeadbeef)) {}
}

does what it appears to do, which is to fetch the current value of test, and compare it to test to see if it has changed. In the real world iterations of CAS would be separated by code that did actual work. The volatile keyword would be needed to insure that the compiler fetched test before calling CAS rather than assuming that a copy it might still have in a register would still be valid.

Or the value that you would be testing against wouldn't be the current value of test, but rather some sort of last known value.

In other words, this code example is a test of the theory, but you wouldn't use CAS like this in practice, so even if you could get this to fail, it doesn't necessarily tell you how it could fail when used in real world algorithms.

John Knoeller
Why would it execute only twice? If it optimizes away the fetch of test, isn't the effect that it could loop *forever*? The compare and swap will always fail because the compare will be perpetually wrong, unless you're lucky and rand() returns the value that test happened to be at before thread A entered the loop and thread A happens to run at the right time.
Joseph Garvin
Also when you say incredibly unlikely, do you mean unlikely enough that you wouldn't be opposed to using this code in production software? It'd be interesting to try to compute a simplified probability.
Joseph Garvin
When I personally say incredibly unlikely when referring to a threading issue, I mean I want it fixed so it really doesn't happen. I've had too many "not gonna happens" blow up on me.
David Thornley
@Joseph: I have in fact used this in production software. The software has been for sale for years (you can buy at at Best Buy) and the code that uses this has proved bulletproof. The fact that you can't _prove_ this never fails is fine for the Computer Scientists, but it's irrelevant to the engineers.
John Knoeller
@Joseph: Sorry, editing mistake. I meant forever. will fix
John Knoeller
@David: I mean that in practice this is not a genuine concern.
John Knoeller
Can you quantify "very likely not"? What compilers fetch `test` once before the loop and never again? CAS can modify `test`, so the naive assumption is that the compiler should re-load. But of course CAS doesn't modify `test` if it fails, so the compiler can use that fact as a special case when optimising loops in which the only way a variable can be modified is by a CAS. It seems to me perverse to implement that special case in the compiler, considering that you know the only code that will be sped up by it is buggy code. Am I missing something?
Steve Jessop
@Steve: actually, it's not so much fetching 'before' the loop as assuming that the value isn't being changed behind the compiler's back. but it amounts to the same thing. But it hardly matters, this is the way you use CAS anyway. I'll try for a better phrasing.
John Knoeller
@John: that's what I mean: if you take a pointer to a local variable, and pass it as a parameter to some kind of call, then it doesn't matter whether it's volatile or not, if the compiler doesn't know what the call does then it must assume it can change the value, and must re-load it. For CAS it does know what the call does, so it could make that optimisation, but I'm amused that the optimisation seems only applicable to broken code ;-) So I'm curious when you really need `volatile`, in practice rather than in theory.
Steve Jessop
@Steve: you are right, volatile is not needed in this example. I was mixing up my knowledge of real world use of CAS and this synthetic test.
John Knoeller
@John: Does the code that you deployed only do this between two threads? With work actually being done and multiple competing threads, would it potentially become more likely that a particular thread would could become starved?
Joseph Garvin
@Joseph: two threads, one producer of data, one consumer. More threads increases the likelihood that a thread will go a few iterations with CAS until things settle, but actual starvation is no more likely. Starvation comes from flaws in the way you USE lock-free primatives not from the primitives themselves. See Alex's post and link.
John Knoeller