4

In this article:

http://msdn.microsoft.com/en-us/magazine/jj883956.aspx

the author states that the following code can fail due to "loop read hoisting":

class Test
{
  private bool _flag = true;
  public void Run()
  {
    // Set _flag to false on another thread
    new Thread(() => { _flag = false; }).Start();
    // Poll the _flag field until it is set to false
    while (_flag) ;
    // The loop might never terminate!
  }
}

In loop read hoisting, the compiler may change the while loop above to the following because of a single-thread assumption:

if (_flag) { while (true); }

What I'm wondering is this: if the compiler doesn't perform that optimization, is there still potential for the loop to run forever on a multiprocessor machine due to one processor updating _flag in a register or cache and never flushing that cache back to memory readable by the other thread? I've read that "C# writes are volatile," but the article I linked to says this is not actually guaranteed by the ECMA spec and things aren't implemented that way on ARM. I'm trying to figure out how paranoid I have to be to write threaded code that will work on all platforms.

Here is a related question:

Can a C# thread really cache a value and ignore changes to that value on other threads?

but I think the code in the accepted answer is probably getting optimized with loop read hoisting, so it proves nothing about memory visibility...

Community
  • 1
  • 1
adv12
  • 8,443
  • 2
  • 24
  • 48
  • Threading is hard. If you can't **actively say that it is safe**, don't use it. Simple as. Since the *read* optimization is demonstrably risky, the question feels kinda moot... – Marc Gravell Apr 16 '14 at 13:30
  • In addition to possibly never terminating, it's also a busy loop, which is a *really* bad thing to be doing even if you properly synchronized the shared memory. – Servy Apr 16 '14 at 13:54
  • If you were to Join the thread the program would be defined because thread exits and joins act as memory barriers. – usr Apr 16 '14 at 14:13

1 Answers1

6

if the compiler doesn't perform that optimization, is there still potential for the loop to run forever on a multiprocessor machine due to one processor updating _flag in a register or cache and never flushing that cache back to memory readable by the other thread?

Yes.

I've read that "C# writes are volatile," but the article I linked to says this is not actually guaranteed by the ECMA spec and things aren't implemented that way on ARM.

How is that relevant? The main thread isn't writing, it's reading.

I'm trying to figure out how paranoid I have to be to write threaded code that will work on all platforms.

It's not paranoia if they really are out to get you. Threading is hard. Do what I do: leave low-level manipulation of shared memory to experts.

Write your code using the highest possible level of abstraction, with the abstractions written for you by experts. You should almost never write the code as you described, not because it's wrong -- though it is -- but because it's at the wrong level of abstraction. If you want to represent the idea of "this operation can be cancelled" then use a CancellationToken; that's what they're for. If you want to represent the notion of "this work produces its result in the future", use a Task<T>; that's what they're for. Don't try to roll your own; let Microsoft do it for you.

UPDATE: For more information about thread safety in C#, volatile semantics, low-lock techniques, and why you should avoid doing all of these things yourself, see:

Vance's awesome 2005 article on low lock techniques:

http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

My 2011 series of three articles which begins here:

http://ericlippert.com/2011/05/26/atomicity-volatility-and-immutability-are-different-part-one/

In particular the third is relevant to you but the first two might be interesting as well.

Joe Duffy reiterates why you should not use volatile:

http://joeduffyblog.com/2010/12/04/sayonara-volatile/

My 2014 pair of Ask The Bug Guys articles:

http://blog.coverity.com/2014/03/12/can-skip-lock-reading-integer/ http://blog.coverity.com/2014/03/26/reordering-optimizations/

I've given those in a reasonable reading order; if you're finding Vance's article too difficult going, try starting with my three-part series instead and then go back to it.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Thanks for the reply. In response to your question "How is that relevant? The main thread isn't writing, it's reading.": I think maybe I don't understand threading enough to get your point. I figured that unless the second thread wrote all the way to main memory, there'd be nothing there for the main thread to read. But maybe you're saying that doesn't matter because the main thread would have similar caches and wouldn't be looking to main memory for the value anyway? Sorry if this is unclear. Programmer for 12 years and still feel like a total newbie to threading. – adv12 Apr 16 '14 at 14:11
  • @adv12 Imagine that every thread has a cache of its own so it doesn't have to pull from main memory every time. Just because you write to main memory doesn't mean you're also reading from it. – Chris Hannon Apr 16 '14 at 15:34
  • 2
    @adv12: Each processor has a *copy* of a page of memory, called the cache. Suppose the main thread is on one CPU and the new thread is on the other. Both make a copy of the relevant page of memory on their cache. Now the new thread writes a value to the cache and copies the written-to cache back to main memory. **What causes the other CPU to refresh its cache on the next read**? Nothing, that's what. – Eric Lippert Apr 16 '14 at 15:35
  • Guys, thanks for the clarifications. @EricLippert, do I understand correctly that wrapping both the write and the read in a lock on the same object would cause the necessary cache flush and refresh? – adv12 Apr 16 '14 at 15:42
  • @adv12: Wrapping the read and write both in a lock is the safest thing to do here, yes. A lock introduces a "full fence" which prevents reads and writes from moving forwards or backwards in time with respect to the lock. Whether they do so by flushing processor caches is an implementation detail of the runtime. I'll give you some links for further reading. – Eric Lippert Apr 16 '14 at 17:05