You could increment atomically the counter
without locking, with the Interlocked.Increment
method, and enter the lock
only if the result of the atomic increment is divisible by 200:
int counter = 0;
ParallelOptions options = new() { MaxDegreeOfParallelism = Environment.ProcessorCount };
Parallel.For(0, totalSIM, options, i => {
/* do some stuff */
int current = Interlocked.Increment(ref counter);
if (current % 200 == 0)
{
lock (options) _printConcurrent(current);
}
});
This way the _printConcurrent
will be called once every 200 iterations, without blocking the rest 199 iterations.
Update: The above approach has a minor flaw. It doesn't guarantee that the _printConcurrent
will be called sequentially with an incremented argument. For example it is theoretically possible that the _printConcurrent(400)
will be called before the _printConcurrent(200)
. That's because the operating system can suspend any thread at any time for a duration of around 10-30 milliseconds (demo), so a thread could be suspended immediately after the Interlocked.Increment
line, and lose the race to enter the lock
by a non-suspended thread. In case this is a problem, you can solve it be throwing a second counter in the mix, that is incremented only inside the lock
:
int counter1 = 0;
int counter2 = 0;
const int step = 200;
ParallelOptions options = new() { MaxDegreeOfParallelism = Environment.ProcessorCount };
Parallel.For(0, totalSIM, options, i => {
/* do some stuff */
if (Interlocked.Increment(ref counter1) % step == 0)
{
lock (options) _printConcurrent(counter2 += step);
}
});