9

I've been doing simple multi-threading in VB.NET for a while, and have just gotten into my first large multi-threaded project. I've always done everything using the Synclock statement because I didn't think there was a better way.

I just learned about the Interlocked Class - it makes it look as though all this:

Private SomeInt as Integer
Private SomeInt_LockObject as New Object

Public Sub IntrementSomeInt
    Synclock SomeInt_LockObject
        SomeInt += 1
    End Synclock
End Sub

Can be replaced with a single statement:

Interlocked.Increment(SomeInt)

This handles all the locking internally and modifies the number. This would be much simpler than writing my own locks for simple operations (longer-running or more complicated operations obviously still need their own locking).

Is there a reason why I'd rolling my own locking, using dedicated locking objects, when I can accomplish the same thing using the Interlocked methods?

SqlRyan
  • 33,116
  • 33
  • 114
  • 199

5 Answers5

9

You're correct; Interlocked should be used here, and will be faster than SyncLock.
However, the Interlocked class is not well-known.

However, there are situations where you need to use SyncLock and Interlocked will not help.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • Can you provide an example of such a situation? If I'm just doing a simple increment/decrement (or .Add if I need to change the value by more than 1), it seems like Interlocked would be better in all cases. If I need something more advanced (like a 4-step process where I need to guarantee it's the only thread running it at any given time), I need some Synclocking. I'm interested in a situation where Interlocked would appear to be proper, but is a bad choice. – SqlRyan Mar 28 '10 at 21:09
  • 3
    If you're working with two different variables, `Interlocked` is not enough. – SLaks Mar 28 '10 at 21:55
5

This is because no one knows about it. Spread the word!

ChaosPandion
  • 77,506
  • 18
  • 119
  • 157
  • That's what I thought - I see that it's been around since 1.0, and so I was astounded when I just found it. I thought "There has to be something else going on here that I'm not seeing, or else this method of locking would be much more popular". I really just want to make sure it's as great is it appears to be on first glance. – SqlRyan Mar 28 '10 at 21:11
  • 1
    This answer does not convey enough information, nor does it directly answer the question. – Kir Dec 16 '11 at 15:23
4

The short answer is because using a Monitor lock (SyncLock in VB and lock { } in C#) not only assures that only one thread at a time can access the variable (or, in a strict sense, only one thread at a time can obtain a lock on the locking object), but it also creates the memory barrier required to ensure that reads on the variable aren't optimized away.

If you're never simply reading the value of the variable (in other words, all of your work is done through calls to Interlocked), then you'll be OK. However, if you need to be able to perform a normal read of the variable then the situation is more complicated. Lockless reads/writes are usually accomplished in C# using the volatile keyword. This instructs the compiler to read the value of the variable everywhere it's used, rather than optimizing away any of these reads into a local cache. Unfortunately there is no equivalent in VB.NET, so you'll have to use something else.

The accepted answer to this question should provide some more information on what you can do. In short, most people use SyncLock in VB.NET because it's easier and less complicated than the logic required to do it without SyncLock.

Community
  • 1
  • 1
Adam Robinson
  • 182,639
  • 35
  • 285
  • 343
  • Not entirely correct, when you change your variables with Interlocked, you can safely read them without any special measures. Because the barrier is around the writes, the reads are safe. – H H Mar 28 '10 at 21:23
  • @Henk: Couldn't multiple reads that are subsequent to the `Interlocked` call be optimized into a single cached read? – Adam Robinson Mar 28 '10 at 21:25
  • Adam, yes they could. And from a real-time perspective that might seem 'wrong' but it would not lead to incorrect behaviour. Check the Interlocked members. There only is a Read for 64 bit values (on 32 bit systems). http://msdn.microsoft.com/en-us/library/system.threading.interlocked_members.aspx – H H Mar 28 '10 at 21:37
  • @Henk: How would it not be incorrect behavior if another thread updated the value after it had been cached? – Adam Robinson Mar 28 '10 at 21:40
  • Funny stuff, hey? Because what if that read had happened a millisecond earlier or later, would it have been 'incorrect' then? Note that Interlocked.Increment() does its own read for just this reason. – H H Mar 28 '10 at 21:45
  • @Henk: Correctness in terms of output, then no, it wouldn't have been "incorrect". In terms of behavior, then yes, that would be "incorrect". You said that the fact that read optimizations subsequent to the `Interlocked` call could be optimized out "would not lead to incorrect behavior". The very existence of volatile variables (and the problem that they address) seems to contradict what you're saying. – Adam Robinson Mar 28 '10 at 21:48
  • volatile and Interlocked address the same problem, on different levels. Note that `volatile int x; x = x + 1;` is not thread-safe. – H H Mar 28 '10 at 22:00
  • No, they don't. Interlocked allows for a single-step execution and prevention of the CPU from interrupting the internal multi-step process. There is a memory barrier created by the functions in the `Interlocked` class, but any actions outside of it (i.e. a normal use of the variable) is not going to be given the same protection. – Adam Robinson Mar 28 '10 at 22:07
  • And you're correct, `x = x + 1` is not guaranteed to execute uninterrupted. It is, however, guaranteed that whenever `x` is used later in the procedure that the variable is actually read and not cached. – Adam Robinson Mar 28 '10 at 22:08
  • Adam, try to create a concrete scenario where caching Interlocked vars matters. – H H Mar 28 '10 at 22:32
  • @Henk: Two threads executing a repetitive action in a loop. For each iteration, the procedure uses `Interlocked.Increment` to increment a counter, storing the return value in a local variable. The thread executes code, then uses a spinwait with a `Thread.Sleep(0)` call in a `while` loop until the counter has been incremented again (in other words, it processes once, then waits for the second thread to do the same) – Adam Robinson Mar 28 '10 at 23:27
  • 1
    In such a case, there is the potential for the variable to be read only once after the `Increment` call. Unless the other thread processes its action between the call to `Increment` and this read, the thread will wait forever, which will then cause the *other* thread to wait forever. – Adam Robinson Mar 28 '10 at 23:28
2

I once read a very good explanation on so called non-atomic and atomic (in VB: interlocked) operations and will try to sum that up.

Normal "non-atomic" operations consist of several steps 

-> other threads can work in between those streps

"Atomic" operations consist of one only one step 

-> other threads cannot perform work while atomic operations are processed, atomic operations are always processed as whole

The interlocked class is a collection of such atomic operations and therefor threadsafe by definition. Even with multiple threads performing read and write operations on the same variable, these operations are absolutely thread safe.

Still a combination of those threadsafe commands can be unsafe, as race conditions can occur in between the atomic operations.

So if you want to for example compare 2 variables and then increment the smaller one, this is not thread safe even though the single operations for themself are (interlocked.compare, interlocked.increment). Here you still have to use synclocks.

Other than that limitation there is no "hidden bad side" of interlocked.

One example for a racecondition with a = 5:

 Thread1: a+=1
 Thread2: a+=2    
 --> supposed to be 8, but can be only 6 or 7,
 but can also be 6 or 7 depending on which thread wins the race

option 1:

T1 step 1: read 5
T1 step 2: add 1 = 6
T1 step 3: write 6
T2 step 1: read 6
T2 step 2: add 2 = 8
T2 step 3: write 8
--> is 8 as supposed

or option 2:

T1 step 1: read 5
T2 step 1: read 5
T1 step 2: add 1 = 6
T2 step 2: add 2 = 7
T2 step 3: write 7
T1 step 3: write 6
--> is only 6

or option 3:

T1 step 1: read 5
T2 step 1: read 5
T1 step 2: add 1 = 6
T2 step 2: add 2 = 7
T1 step 3: write 6
T2 step 3: write 7
--> is only 7

With interlocked.increment:

option 1:

T1 step 1: read 5, add 1, write 6
T2 step 1: read 6, add 2, write 8

or option 2:

T2 step 1: read 5, add 2, write 7
T1 step 1: read 7, add 1, write 8

-> in all cases a = 8 as supposed, threadsafe solution

All the questions that were posted here can be solved by applying this simple example to the questionable code.

Hope this helps other people who google this topic. Janis

Janis H
  • 21
  • 1
1

Interlocked is limited to simple operations on Integer, Long and Boolean and such.

If you want to add an item to a shared List(of T) for example, you will still need SynClock.

H H
  • 263,252
  • 30
  • 330
  • 514
  • Indeed - that's because it's an atomic operation implemented at hardware level i.e the CPU will guarantee that the increment cannot be contended hence it needs to map to CPU primitives such as ints, longs etc – zebrabox Mar 28 '10 at 22:25