Search code examples
c#parallel-processingparallel.foreachcontext-switch

Parallel class and thread context switch


Trying to understand how the thread context switch affects the execution of iterations of the Parallel class through ForEach or For usage. I tried to increase CPU usage up to 100% with execution of several processes, but not a single Parallel's iteration has changed it's Thread.CurrentThread.ManagedThreadId value.

  • To increase CPU up to 100% usage, I've started several high priority processes, including the example.

  • Code where do we need to handle thread context switch:

    Parallel.For(0, 3, (index, i) =>
    {
        var firstId = Thread.CurrentThread.ManagedThreadId;
        while (true)
        {
            var rnd = new Random(index);
            checkIds(firstId, Thread.CurrentThread.ManagedThreadId);
            var digits = new List<int>();
            for (int j = 0; j < 10000; j++)
            {
                digits.Add(rnd.Next());
                if (continueWriteLine)
                    Console.WriteLine($"ID: = {Thread.CurrentThread.ManagedThreadId}");
            }
    
            if (continueWriteLine)
                digits.ForEach(Console.WriteLine);
        }
    });
    
  • Code that tries to handle thread switch:

    if (firstId != currentId)
    {
        continueWriteLine = false;
        Thread.Sleep(1000);
        Console.WriteLine($"{firstId} - {currentId}");
    }
    

So, I have several questions:

  1. Can a thread switch to another one, during the execution of an iteration of the Parallel class, by some reason e.g. Thread.Sleep, lock statement, Mutex, etc.?

  2. And how this threads' switch affects the ManagedThreadId property, if they really will be switched?

  3. Will it be safe to use ManagedThreadId as unique key of the ConcurrentDictionary from which any information can be retrieved for a current operation e.g. information about file's reading: current line, desired object to read, already read objects, and a lot of other things that are needed during current operation?

P.S. The reason for the solution given in the third question is lack of desire to transfer most of this data between methods that helps me read and process every new line of file in order to maintain context of file's processing. Maybe the solution would be to transfer only one object between parser's methods, something like FileProcessingInfo, that contains all context data (which I mentioned in the third question), but I don't know for sure which solution would be better.


Solution

    1. Can a thread switch to another one, during the execution of an iteration of the Parallel class, by some reason e.g. Thread.Sleep, lock statement, Mutex, etc.?

    No. Each individual iteration of a Parallel.For/Parallel.ForEach loop runs invariably on the same thread from start to finish. This thread is completely dedicated to this iteration, and won't do any unrelated work elsewhere before this iteration completes. After this iteration completes, the thread might dedicate itself to some other iteration, or return to the ThreadPool.

    Each iteration is not guaranteed to run non-stop on one physical CPU-core though. The operating system might perform one or many thread switches, by suspending the execution of this thread and assigning the physical CPU-core to some other thread. This phenomenon is transparent to your program. The thread itself doesn't experience any observable symptom whenever a thread-switch occurs at the operating system level. I don't know if the .NET platform itself receives any notification from the operating system whenever a thread-switch occurs. If I had to guess, I would say probably not.

    It should be noted that the new asynchronous API Parallel.ForEachAsync (.NET 6) invokes an asynchronous body delegate, and asynchronous delegates that contain await statements are routinely switching threads after each await. In this case it's not an operating system thread-switching. It's the kind of thread-switching that you are interested for, with the Thread.CurrentThread.ManagedThreadId changing after the await.

    1. Will it be safe to use ManagedThreadId as unique key of the ConcurrentDictionary from which any information can be retrieved for a current operation e.g. information about file's reading: current line, desired object to read, already read objects, and a lot of other things that are needed during current operation?

    No, because this ID is not guaranteed to be unique. After a thread terminates, its number can be reused. And you have no control over the life-cycle of the ThreadPool threads, the threads that the Parallel APIs use by default. If you want to identify uniquely a Thread, use the Thread object itself as the TKey of the dictionary (ConcurrentDictionary<Thread, FileProcessingInfo>). But you might find using instead a ThreadLocal<FileProcessingInfo> to be more convenient.