65

NOTE: I am really not very good at Multithreaded programming, but my current project has me doing it so I am trying to get my head around what is thread safe and what is not.

I was reading one of Eric Lippert's awesome answers on what ++i does. He says this is what really happens:

  1. x is evaluated to produce the variable
  2. the value of the variable is copied to a temporary location
  3. the temporary value is incremented to produce a new value (not overwriting the temporary!)
  4. the new value is stored in the variable
  5. the result of the operation is the new value

This got me to thinking, what if two threads where calling ++i? If the first thread is at step 3 when the second thread is on step 2. (Meaning what if the second thread copies the value off to the temp location before the first thread stores the new value in the variable?)

If that happens then it would seem that both threads would only increment i once instead of twice. (Unless the whole thing is in a lock.)

Community
  • 1
  • 1
Vaccano
  • 78,325
  • 149
  • 468
  • 850
  • 9
    Here's [another Eric](http://blog.decarufel.net/2009/02/why-operator-is-not-thread-safe.html) that explains why the `++` operator is not thread safe. The point is that a CPU cannot do math directly in memory—it has to load the value from memory into a register first; that creates the possibility for a race condition. The thing to keep in mind with multi-threaded programming is that in general, you should assume that things are **not** thread safe unless the documentation *explicitly* tells you that they are. – Cody Gray - on strike Jan 07 '11 at 17:23

3 Answers3

90

As other answers have pointed out, no, ++ is not "threadsafe".

Something that I think will help as you learn about multithreading and its hazards is to start being very precise about what you mean by "threadsafe", because different people mean different things by it. Essentially the aspect of thread safety you are concerned about here is whether the operation is atomic or not. An "atomic" operation is one which is guaranteed to not be halfway complete when it is interrupted by another thread.

(There are plenty of other threading problems that have nothing to do with atomicity but which may still fall under some people's definitions of thread safety. For example, given two threads each mutating a variable, and two threads each reading the variable, are the two readers guaranteed to agree on the order in which the other two threads made mutations? If your logic depends on that, then you have a very difficult thread safety problem to deal with even if every read and write is atomic.)

In C#, practically nothing is guaranteed to be atomic. Briefly:

  • reading a 32 bit integer or float
  • reading a reference
  • writing a 32 bit integer or float
  • writing a reference

are guaranteed to be atomic (see the specification for the exact details.)

In particular, reading and writing a 64 bit integer or float is not guaranteed to be atomic. If you say:

C.x = 0xDEADBEEF00000000;

on one thread, and

C.x = 0x000000000BADF00D;

on another thread, then it is possible to on a third thread:

Console.WriteLine(C.x);

have that write out 0xDEADBEEF0BADF00D, even though logically the variable never held that value. The C# language reserves the right to make writing to a long equivalent to writing to two ints, one after the other, and in practice some chips do implement it that way. A thread switch after the first of the writes can cause a reader to read something unexpected.

The long and short of it is: do not share anything between two threads without locking something. Locks are only slow when they are contented; if you have a performance problem because of contended locks then fix whatever architectural flaw is leading to contended locks. If the locks are not contended and are still too slow, only then should you consider going to dangerous low-lock techniques.

The common low-lock technique to use here is of course to call Threading.Interlocked.Increment, which does an increment of an integer in a manner guaranteed to be atomic. (Note however that it still does not make guarantees about things like what happens if two threads are each doing interlocked increments of two different variables at different times, and other threads are trying to determine which increment happened "first". C# does not guarantee that a single consistent ordering of events is seen by all threads.)

Martin Brown
  • 24,692
  • 14
  • 77
  • 122
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • @Eric: Do you know if there is a way to make everything atomic for a development environment/platform or language? I am not sure if that kind of tech exists for the purposes of parallel programming. But I don't know what it takes to make something atomic. Maybe it's because there is overhead to make something atomic? – Joan Venge Jan 07 '11 at 19:39
  • 4
    @Joan: Locks make non-atomic operations into atomic operations. That's the whole point of locks. I'm not sure what you mean by making "everything" atomic. Suppose you want to have an atomic operation called "read-this-queue-and-then-send-an-email". You'd do that by putting a lock around calls that read the queue and sends the email. – Eric Lippert Jan 07 '11 at 22:01
  • 1
    @Eric: Thanks Eric, I see what you mean. I was wondering if this is also how you guys make the things you listed (writing a reference, etc) atomic? Also as for everything, I was mainly thinking like every construct or action using the C# language being atomic, like creating a class, etc. But I guess that's not very desirable? – Joan Venge Jan 07 '11 at 22:22
  • 6
    @Joan: The atomic operations guaranteed by the language are precisely the operations that are guaranteed by hardware to be atomic. The CLR only runs on hardware that has 32 bit atomicity. – Eric Lippert Jan 07 '11 at 22:25
  • @Eric: Thanks Eric, that's what I was wondering. – Joan Venge Jan 07 '11 at 22:31
  • @Eric: Confused. How can one write a reference atomically if one cannot even write a 64-bit IntPtr atomically? – Jason Kresowaty Jan 08 '11 at 23:36
  • 5
    @binarycoder: Platforms which natively have 64 bit pointers obviously have 64 bit atomicity in hardware. The CLR only runs on platforms that have *at least* 32 bit atomicity, not *at most* 32 bit atomicity. I thought that was clear, but apparently not. Sorry. – Eric Lippert Jan 09 '11 at 15:44
  • 3
    It's important to note that the real reason this situation is bad is that dead beef is actually considered to be GOOD food, at least by most carnivores. – Igby Largeman Oct 19 '11 at 21:23
  • @EricLippert I'm a bit confused about your note that other threads would not be able to determine which variable was updated first. I'm struggling to imagine a situation where that *is* possible. If the updating threads *were* able to guarantee a known order, the threads attempting to read this would still be vulnerable to the fact that the variable A could be updated immediately after the check for A completed, and variable B also being updated (but after A) before the check for B executed. Even with locks, there is still a possibility for an incorrect result. – Rob Nov 21 '15 at 15:57
  • 1
    @Rob: What I'm getting at is illustrated here: http://blog.coverity.com/2014/03/26/reordering-optimizations/#.VlESHY-cHic The code looks like it should make a particular guarantee, but because even volatile writes may be re-ordered arbitrarily, things can go very wrong. – Eric Lippert Nov 22 '15 at 00:55
  • @EricLippert Thanks for the response. Never knew this re-ordering was possible, very interesting – Rob Nov 22 '15 at 12:11
  • The link in the comment above is dead; I have moved that article to https://ericlippert.com/2014/03/26/reordering-optimizations/. – Eric Lippert Oct 05 '20 at 23:40
34

No, it isn't.

The analysis you have come up with is quite correct - the ++ (and --) operators are vulnerable to race conditions if they are not used with appropriate locking semantics.

These are difficult to get right, but thankfully, the BCL has a ready made solution for these specific cases:

You should use Interlocked.Increment if you want an atomic increment operation. There is also a Decrement and several other useful atomic operations defined on the Interlocked class.

Increments a specified variable and stores the result, as an atomic operation.

Oded
  • 489,969
  • 99
  • 883
  • 1,009
  • does it matter if performed on byte? would that make it thread safe? – Demetris Leptos Apr 25 '18 at 11:25
  • 2
    @DemetrisLeptos - it doesn't matter. `++` and `--` are not atomic at any variable size. They are translated to a number of instructions, hence, are not atomic. – Oded Apr 25 '18 at 11:52
11

No, i++ looks compact but it really is just shorthand for i = i + 1 and in that form it is easier to see that it involves a read and a write of 'i'. Without locking it is simply not thread-safe.

2 other arguments are :

  • ++ is not defined as thread-safe
  • the existence of Interlocked.Increment (ref int x)

Thread-safety is rare and expensive, it will be clearly advertised when available.

H H
  • 263,252
  • 30
  • 330
  • 514