4

I am learning TPL and stuck with a doubt. It is only for learning purpose and I hope people will guide me in the correct direction.

I want only one thread to access the variable sum at one time so that it does not get overwritten.

The code I have is below.

using System;
using System.Threading.Tasks;


class ThreadTest
{
    private Object thisLock = new Object();
    static int sum = 0;
    public void RunMe()
    {
        lock (thisLock)
        {
            sum = sum + 1;
        }
    }

    static void Main()
    {
        ThreadTest b = new ThreadTest();
        Task t1 = new Task(()=>b.RunMe());
        Task t2= new Task(() => b.RunMe());
        t1.Start();
        t2.Start();
        Task.WaitAll(t1, t2);
        Console.WriteLine(sum.ToString());
        Console.ReadLine();
    }
}

Question -Am i right in this code ?

Question-Can I do it without lock because I read it somewhere that it should be avoided as it does not allow task to communicate with each other.I have seen some examples with async and await but I am using .Net 4.0 .

Thanks

Murray Foxcroft
  • 12,785
  • 7
  • 58
  • 86
vic90
  • 89
  • 1
  • 8
  • 1
    You are doing right. The variable won't be overwritten. Only one thread at time will acess lock scope. You can make thisLock static. – Mauro Sampietro Aug 25 '16 at 07:30
  • 5
    This is a decently drafted question, I don't find a reason to down-vote, please refrain without a valid reason – Mrinal Kamboj Aug 25 '16 at 07:31
  • 3
    DOWNVOTES! Most often downvotes are made by people who by stackoverflow have completely different background than question at hands. I am getting downvotes and question deletions or degradations from people with PHP, JavaScript, Java, SQL background and almost zero participation in the C#, .NET which are usually my questions and answers. – ipavlu Aug 25 '16 at 07:36
  • Your code is thread safe across a class instance. You can make it more thread safe across the application by making the lock object static, i.e.:private static object thisLock = new object(); – Murray Foxcroft Aug 25 '16 at 07:38
  • @sam lock needn't be `static`, it belongs to same `object` on both the `Tasks`, thus will do as expected, block the `Tasks`, if they come together – Mrinal Kamboj Aug 25 '16 at 07:38
  • 1
    @MrinalKamboj yeah, anyway you could instantiate ThreadTest twice and call the static method Main. Since the method is static and sum variable is static and the lock is not, you will face unexpected behavior. isn't it? – Mauro Sampietro Aug 25 '16 at 07:45
  • Sam is correct. The lock object needs to be static too. – Matthew Watson Aug 25 '16 at 07:51
  • 1
    @MatthewWatson In the corrent code the lock object doesn't **need** to be static, since there is only a single instance of the class `ThreadTest`. If there are going to be created more instances, then, yes, it needs to be static. I admit, that the lock is created to access the static variable `sum`, so the lock should have the same 'scope' as the variable. – Maarten Aug 25 '16 at 07:55
  • Great thanks for your answer.If it doesnot not bother much,second question that ask for any other method instead of locks as locks are not always good practice(i read somewhere,i may be wrong).Why dont we have something like synchrisation property in Task. I was confused about `SynchronizationContext` in task – vic90 Aug 25 '16 at 07:55

3 Answers3

3

Am i right in this code

Implementation wise Yes, but understanding wise No, as you have mixed up the new and old world while trying to implement the logic, let me try to explain in detail.

  • Task t1 = new Task(()=>b.RunMe()); doesn't mean as expected as in case of Thread API a new thread every time
  • Task API will invoke a thread pool thread, so chances are two Task objects t1,t2, gets executed on same thread most of the times for a short running logic and there's never a race condition, which needs an explicit lock, while trying to update the shared object
  • Better way to prevent race condition for Sum object would be Interlocked.Increment(ref sum), which is a thread safe mechanism to do basic operations on primitive types
  • For the kind of operation you are doing a better API would be Parallel.For, instead of creating a separate Task, the benefit would be you can run any number of such increment operations with minimal effort, instead of creating a Separate Task and it automatically blocks the Main thread, so your code shall look like:

    using System;
    using System.Threading.Tasks;
    
    class ThreadTest
    {    
       public static int sum;
    }
    
    static void Main()
    {
    
     Parallel.For(0, 1, i =>
    {
        // Some thread instrumentation
        Console.WriteLine("i = {0}, thread = {1}", i,
        Thread.CurrentThread.ManagedThreadId);
    
        Interlocked.Increment(ref ThreadTest.sum);
    });
    
       Console.WriteLine(ThreadTest.sum.ToString());
       Console.ReadLine();
    }
    }
    
  • While using the Thread instrumentation you will find that chances are that for two loops, 0,1, managed thread id is same, thus obviating the need for thread safety as suggested earlier

Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
  • Sir,Parallel for will use it as a shared resource?Am i correct ? – vic90 Aug 25 '16 at 07:57
  • @Mrinal-Sir,can i use the same `Interlocked.Increment` can be used for file and other operations? – vic90 Aug 25 '16 at 08:06
  • @vic90 Shared resource ? Not sure, please explain. `Parallel.For` is for `data parallelization` – Mrinal Kamboj Aug 25 '16 at 08:09
  • Actually, from the code here the threads WILL be run on separate managed threads most of the time because they are started almost simultaneously. Also note that your code won't compile - you can't pass a ref property to Interlocked.Increment(). Try fixing the code and changing the Parallel.For() to finish at index 5 instead of 1, and check the output. – Matthew Watson Aug 25 '16 at 08:11
  • @Matthew Watson thanks for the correction, removed the property part. Writing code directly on SO, doesn't helps to compile, Thank you again – Mrinal Kamboj Aug 25 '16 at 08:13
  • Thanks,I want to mark both as Answers but cant.Really grateful . – vic90 Aug 25 '16 at 08:17
  • You have to count with running on multiple threads each time. With complex operations in the tasks you have to count even with cases where it wont, as it might cause blocking in complex scenarios. – ipavlu Aug 25 '16 at 08:22
  • Because it was mistaken point, I read in hurry from multiple lines in wrong order. So I actually commented on something else:). But but but, look on my next comment:). – ipavlu Aug 26 '16 at 08:38
  • The second part of my comment is right, in complex scenarios, you have count with tasks running concurrently, but also with case, that they wont. The usual problem is with TaskSchedulers, that in certain cases it clearly depends on what context you are currently in from where you start new task. For example StartNew is problematic as it is using the current task scheduler, in UI thread the task scheduelkr is the thread itself, In thread pool thread it is targetting thread pool. So complex scenarios where tasks wont actually run concurrently, but they can run asynchronously. – ipavlu Aug 26 '16 at 08:42
1

Answer 1:

This is threadsafe for the code that you posted.

However, as Sam has pointed out, this is not currently threadsafe in the general case because the field being incremented is static, but the locking object is not static.

This means that two separate instances of ThreadTest could be created on two separate threads, and then RunMe() could be called from those threads and because each instance has a separate locking object, the locking wouldn't work.

The solution here is to make the locking object static too.

Answer 2:

You can do this without explicit locking using Interlocked.Increment():

public void RunMe()
{
    Interlocked.Increment(ref sum);
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Thanks Sir, just for clarification,Interlocked.Increment can also be used for file and any other ooperations (in case i have to do some other job instead of adding number)? – vic90 Aug 25 '16 at 08:02
  • @vic90 No you cannot `Interlocked` is a specific class, which expose API for primitive types value modification, otherwise you need a lock or a thread safe data structure – Mrinal Kamboj Aug 25 '16 at 08:06
  • @vic90 No, Interlocked.Increment() is just for numbers - for other stuff, use `lock` like you are already doing. – Matthew Watson Aug 25 '16 at 08:06
  • Thanks a lot.Really helpful – vic90 Aug 25 '16 at 08:07
1

Now to the point, as I was adding some unhappy comments about downvotes, that have no reasons:).

Your scenario is working and is classic multithreading usage. Classic because of using system lock, that is lock are actually WinAPI locks of the OS, so in order to synchronize, the code has to manage to ring down to the OS and back and of course lose some time with switching threads as some contention may happen especially if you would access RunMe multiple times in each thread running given task or created even more concurrent tasks thank 2.

Please try look on atomic operations. For your scenario it would work very well, Interlocked.Increment(ref sum). From there you have to restrain yourself from directly accessing the sum, but that is not a problem, because the Increment method is returning latest result.

Another option is to use SpinLock, that is IF YOU OPERATION IS REALLY FAST. NEVER ON something ,like Console.WriteLine or any other system operations or long running calculations etc.

Here Inrelocked examples:

using System;
using System.Threading;
using System.Threading.Tasks;

class ThreadTest
{
    /// <summary>  DO NOT TOUCH ME DIRECTLY  </summary>
    private static int sum/* = 0 zero is default*/;
    private static int Add(int add) => Interlocked.Add(ref sum, add);
    private static int Increment() => Interlocked.Increment(ref sum);
    private static int Latest() => Interlocked.Add(ref sum, 0);
    private static void RunMe() => Increment();

    static void Main()
    {
        Task t1 = new Task(RunMe);
        Task t2 = new Task(RunMe);
        t1.Start();
        t2.Start();
        Task.WaitAll(t1, t2);
        Console.WriteLine(Latest().ToString());
        Console.ReadLine();
    }
}
ipavlu
  • 1,617
  • 14
  • 24
  • Thanks for your answer.I understand `Interlocked.Increment` can be used for primitive datatypes.So next time ,i will use `Interlocked.Increment` for primitive and `lock` for any other function(file etc).Am i correct.Did it make some sense?and again thanks for your response Sir. – vic90 Aug 25 '16 at 08:22
  • Yes you are right, the Interlocked is very limited on datatypes that are on the platform atomic. Just few datatypes really. For more complex and fast SpinLock and for rest lock but even then as with safran, as little as possible and as short time in the lock as possible:). – ipavlu Aug 25 '16 at 08:28
  • Good variation :) – Mrinal Kamboj Aug 25 '16 at 08:33