Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conflicting threads on a local variable

Tags:

c#

why is it, that in the following code, n doesn't end up being 0, it's some random number with a magnitude less than 1000000 each time, somtimes even a negative number?

static void Main(string[] args)
{
    int n = 0;

    var up = new Thread(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                n++;
            }
        });

    up.Start();

    for (int i = 0; i < 1000000; i++)
    {
        n--;
    }

    up.Join();

    Console.WriteLine(n);

    Console.ReadLine();
}

Doesn't up.Join() force both for loops to finish before WriteLine is called?

I understand that the local variable is actually part of a class behind the scenes (think it's called a closure), however because the local variable n is actually heap allocated, would that affect n not being 0 each time?

like image 767
David Klempfner Avatar asked Aug 09 '13 05:08

David Klempfner


People also ask

Do threads share local variables?

Tip: Unlike class and instance field variables, threads cannot share local variables and parameters. The reason: Local variables and parameters allocate on a thread's method-call stack. As a result, each thread receives its own copy of those variables.

Are local variables shared between threads C#?

But: No, threads do not share real local variables. But in for example C#, a local variable may be captured or 'closed over' and then it becomes a different story.

How is data shared between two threads in Java?

You should use volatile keyword to keep the variable updated among all threads. Using volatile is yet another way (like synchronized, atomic wrapper) of making class thread safe. Thread safe means that a method or class instance can be used by multiple threads at the same time without any problem.

What is thread local variable in C++?

A thread-local variable is what it sounds like: a variable that is localized to a single thread. We can set a thread-local variable using by treating a thread as if it were a Hash, using the square brackets to set and retrieve values using symbol keys.

Are your local variables thread-safe?

Therefore, other threads do not share our local variables, which is what makes them thread-safe. 3. Example Let's now continue with a small code example containing a local primitive and a (primitive) field: On line five, we instantiate one copy of the LocalVariables class. On the next two lines, we start two threads.

What is the use of ThreadLocal in Java?

The Java ThreadLocal class enables you to create variables that can only be read and written by the same thread. Thus, even if two threads are executing the same code, and the code has a reference to the same ThreadLocal variable, the two threads cannot see each other's ThreadLocal variables.

Is it possible to synchronize a variable between threads?

Instead, give each thread its own instance of the object to work with. A good alternative to synchronization or threadlocal is to make the variable a local variable. Local variables are always thread safe.


2 Answers

The n++ and n-- operations are not guaranteed to be atomic. Each operation has three phases:

  1. Read current value from memory
  2. Modify value (increment/decrement)
  3. Write value to memory

Since both of your threads are doing this repeatedly, and you have no control over the scheduling of the threads, you will have situations like this:

  • Thread1: Get n (value = 0)
  • Thread1: Increment (value = 1)
  • Thread2: Get n (value = 0)
  • Thread1: Write n (n == 1)
  • Thread2: Decrement (value = -1)
  • Thread1: Get n (value = 1)
  • Thread2: Write n (n == -1)

And so on.

This is why it is always important to lock access to shared data.

-- Code:

static void Main(string[] args)
{

    int n = 0;
    object lck = new object();

    var up = new Thread(() => 
        {
            for (int i = 0; i < 1000000; i++)
            {
                lock (lck)
                    n++;
            }
        });

    up.Start();

    for (int i = 0; i < 1000000; i++)
    {
        lock (lck)
            n--;
    }

    up.Join();

    Console.WriteLine(n);

    Console.ReadLine();
}

-- Edit: more on how lock works...

When you use the lock statement it attempts to acquire a lock on the object you supply it - the lck object in my code above. If that object is already locked, the lock statement will cause your code to wait for the lock to be released before continuing.

The C# lock statement is effectively the same as a Critical Section. Effectively it is similar to the following C++ code:

// declare and initialize the critical section (analog to 'object lck' in code above)
CRITICAL_SECTION lck;
InitializeCriticalSection(&lck);

// Lock critical section (same as 'lock (lck) { ...code... }')
EnterCriticalSection(&lck);
__try
{
    // '...code...' goes here
    n++;
}
__finally
{
    LeaveCriticalSection(&lck);
}

The C# lock statement abstracts most of that away, meaning that it's much harder for us to enter a critical section (acquire a lock) and forget to leave it.

The important thing though is that only your locking object is affected, and only with regard to other threads trying to acquire a lock on the same object. Nothing stops you from writing code to modify the locking object itself, or from accessing any other object. YOU are responsible for making your sure your code respect the locks, and always acquires a lock when writing to a shared object.

Otherwise you're going to have a non-deterministic outcome like you've seen with this code, or what the spec-writers like to call 'undefined behavior'. Here Be Dragons (in the form of bugs you'll have endless trouble with).

like image 53
Corey Avatar answered Sep 18 '22 12:09

Corey


Yes, up.Join() will ensure that both of the loops end before WriteLine is called.

However, what is happening is that the both of the loops are being executed simultaneously, each one in it's own thread.

The switching between the two threads is done all the time by the operation system, and each program run will show a different switching timing set.

You should also be aware that n-- and n++ are not atomic operations, and are actually being compiled to 3 sub-operations, e.g.:

Take value from memory
Increase it by one
Put value in memory

The last piece of the puzzle, is that the thread context switching can occur inside the n++ or n--, between any of the above 3 operations.

That is why the final value is non-deterministic.

like image 27
Liel Avatar answered Sep 18 '22 12:09

Liel