Parallel.ForEach does not work as you think. It is important to note that the method is built on top of the Task classes and that the ratio between Task and Thread not 1: 1 . You can have, for example, 10 tasks that are performed on 2 managed threads.
Try using this line in your test method instead of the current one:
Console.WriteLine("ThreadId {0} -- TaskId {1} ", Thread.CurrentThread.ManagedThreadId, Task.CurrentId);
You should see that ThreadId will be reused in many different tasks, showing their unique identifiers. You will see this more if you left or increased your call to Thread.Sleep .
The main idea of how the Parallel.ForEach method works is that it requires your counter to create a series of tasks that will run sections of the enumeration process, since this is done, a lot depends on the input. There is also a special logic that checks the case when a task exceeds a certain number of milliseconds without completion. If this case is true, then a new task can be created to help facilitate the work.
If you looked at the documentation for the localinit function in Parallel.ForEach , you will notice that it says that it returns the initial state of the local data for each _task_ , and not every stream.
You may ask why there are more than 8 tasks. This answer is similar to the last one found in the documentation for ParallelOptions.MaxDegreeOfParallelism .
Changing MaxDegreeOfParallelism by default limits the number of parallel tasks.
This restriction applies only to the number of simultaneous tasks, and not to the hard limit of the number of tasks that will be created during the entire processing time. And, as I mentioned above, there are times when a separate task arises, as a result, your localinit function localinit called several times and writes hundreds of files to disk.
Writing to disk is an operation with a slight delay, especially if you use synchronous I / O. When a disk operation occurs, it blocks the entire stream; the same thing happens with Thread.Sleep . If a Task does this, it blocks the thread in which it is currently running, and no other tasks can run on it. Usually in these cases, the scheduler will create a new Task to help get slack.
And finally, what is a good way to get the desired functionality, ThreadLocal?
The bottom line is that thread locators do not make sense with Parallel.ForEach , because you are not dealing with threads; you are dealing with tasks. A local thread can be shared between tasks because many tasks can use the same thread at the same time. In addition, the local task flow can change the average execution, since the scheduler can prevent it from starting, and then continue its execution in another thread, which will have a different thread.
I'm not sure if this is the best way, but you can rely on the localinit function to pass any resource you need, only allowing you to use the resource in one thread at a time. You can use localfinally to mark it as no longer in use and therefore available for another task. This is what these methods were developed for; each method is called only once for each given task (see the Parallel.ForEach comments section of the MSDN documentation).
You can also split the work yourself and create your own set of threads and do your work. However, in my opinion, this is less, since the Parallel class is already doing this hard climb for you.