1

I am trying to understand multi-threading and I have the following code and I need to ensure thread safety (typically without using lock statement) by getting the final result 10,000,000, but if you run the following code multiple time in VS you get different values close to 10,000,000 but never reaches it, so I need to fix this code without using lock statement.

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        private static int _counter = 0;
        private static void DoCount()
        {
            for (int i = 0; i < 100000; i++)
            {
                _counter++;
            }
        }

        static void Main(string[] args)
        {
            int threadsCount = 100;
            Thread[] threads = new Thread[threadsCount];

            //Allocate threads
            for (int i = 0; i < threadsCount; i++)
                threads[i] = new Thread(DoCount);

            //Start threads
            for (int i = 0; i < threadsCount; i++)
                threads[i].Start();

            //Wait for threads to finish
            for (int i = 0; i < threadsCount; i++)
                threads[i].Join();

            //Print counter
            Console.WriteLine("Counter is: {0}", _counter);
            Console.ReadLine();
        }
    }
}

appreciate your help.

Ahmed Moheb
  • 764
  • 1
  • 10
  • 20
  • 2
    Did you try [`volatile`](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/volatile) ? If not sufficient you have for example [`Monitor`](https://learn.microsoft.com/dotnet/api/system.threading.monitor). –  Dec 09 '20 at 16:46
  • 3
    `volatile` is not sufficient but `System.Threading.Interlocked.Increment()` would be. – Ben Voigt Dec 09 '20 at 16:50
  • You can take a look at [Managed threading best practices (MSDoc)](https://learn.microsoft.com/dotnet/standard/threading/managed-threading-best-practices) and [Basic Synchronization (Albahari)](http://www.albahari.com/threading/part2.aspx) and [Multi-threading concept and lock in c# (SO)](https://stackoverflow.com/questions/10152613/multi-threading-concept-and-lock-in-c-sharp) –  Dec 09 '20 at 16:53
  • @OlivierRogier yes I did but got no change in results – Ahmed Moheb Dec 09 '20 at 16:55
  • 2
    Lock free programming is *really* hard to do correctly. You should avoid it at *all possible costs*. – Servy Dec 09 '20 at 16:57
  • That's right, I probably never use in a real world application – Ahmed Moheb Dec 09 '20 at 17:00
  • 1
    On the contrary, you should use lock-free programming whenever you can, especially for simple things like adding numbers (`Interlock.Add`) or capturing data (`Interlock.Exchange`). Also the reason why your count is close to 10,000,000 is because the time to create the threads is so long the previous thread almost finishes first. You'd get a much lower sum with your original code if you use the thread pool (ie `Task.Run`). – Blindy Dec 09 '20 at 17:06
  • @Blindy I tried Thread pool and got 9,3XX,XXX which is less that before – Ahmed Moheb Dec 09 '20 at 17:10
  • You can use threads as multithread mechanism, but is it enough for scale of multi process? – OO7 Dec 09 '20 at 17:11
  • 2
    @Blindy It requires an *enormous* amount of expertise to know when you can and cannot actually avoid a lock in multithreaded programs, in addition to the fact that the costs of using locks, in most contexts, are not a problem. If you reach for lock free programming first odds are *very* high your programs are full of bugs, and you're getting no benefits out of it as a lock wouldn't have been a bottleneck. As for the thread pool being different in this program, the difference is unlikely to be that significant, the window of time for the bug isn't enormous even when running in parallel. – Servy Dec 09 '20 at 17:22
  • I'll be honest with you, you're not going to convince me to avoid compare-exchange lock-free code, and I quote, "at all costs". In my personal experience, it's generally easy to understand, in fact sometimes easier than the mess nested locks can make. And the performance difference is very significant, which is important in low-level libraries where locks are usually hidden in the first place. You never know who's going to call your code in a tight loop, after all. – Blindy Dec 09 '20 at 17:34
  • 2
    @Blindy And how often are people writing "low level libraries" where the performance is so important they don't have a single digit number of nanoseconds to take out a lock? The number isn't zero, but it's *super* low. If you think it's easy, then that just tells me you don't understand all of the ways that it can go wrong and all of the things you need to think about when using it. If you have *nested* locks, then that means you have a *really* complex situation where the odds that a lock free solution is even *possible* is very low, and if it is it's far from easy. – Servy Dec 09 '20 at 17:37
  • 1
    @Blindy *"I personally am not anywhere near smart enough to do correct low-lock programming beyond `Interlocked.Increment`"* Eric Lippert [Mar 27 '10](https://stackoverflow.com/questions/2528969/lock-free-multi-threading-is-for-real-threading-experts/2529773#2529773) – Theodor Zoulias Dec 10 '20 at 01:47
  • @Blindy "is because the time to create the threads is so long the previous thread almost finishes first" - no, it is not. – Enigmativity Dec 10 '20 at 04:58
  • @Blindy - The IL for `_counter++` is `ldfld int32, ldc.i4.1, add, stfld int32` - there's a clear race condition here. – Enigmativity Dec 10 '20 at 05:04

2 Answers2

1

Yes, the Interlocked class of functions are the way to go for lock-free multi-threaded synchronization. In your specific case, you want Interlocked.Increment

Blindy
  • 65,249
  • 10
  • 91
  • 131
1

You're better off getting an understanding of threads, but then moving on quickly to a library that abstracts the complexity for you.

If you use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive and add using System.Reactive.Linq; - then you can do this:

void Main()
{
    var query =
        from n in Observable.Range(0, 100)
        from x in Observable.Start(() => DoCount())
        select x;
        
    var counter = query.Sum().Wait();
        
    Console.WriteLine("Counter is: {0}", counter);
    Console.ReadLine();
}

private int DoCount()
{
    int counter = 0;
    for (int i = 0; i < 100000; i++)
    {
        counter++;
    }
    return counter;
}

Much easier and with the result: Counter is: 10000000

Enigmativity
  • 113,464
  • 11
  • 89
  • 172