I was implementing some multithreading unit-tests and came up with a problem that it is hard to ensure two jobs are actually executed in parallel - one always starts earlier then the other. Let's consider my initial implementation of test scenario to demonstrate the behavior:
static void Main(string[] args)
{
var repeats = 1000000;
var firstWinCount = 0;
var secondWinCount = 0;
int x = 0;
long time1 = 0;
long time2 = 0;
long totalTimeDiff = 0;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < repeats; i++)
{
x = 0;
var task1 = new Task(() =>
{
Interlocked.CompareExchange(ref x, 1, 0);
time1 = sw.ElapsedMilliseconds;
});
var task2 = new Task(() =>
{
Interlocked.CompareExchange(ref x, 2, 0);
time2 = sw.ElapsedMilliseconds;
});
task1.Start();
task2.Start();
Task.WaitAll(task1, task2);
totalTimeDiff += Math.Abs(time1 - time2);
if (x == 1)
{
firstWinCount++;
}
else
{
if (x == 2)
{
secondWinCount++;
}
}
}
Console.WriteLine("First win count: {0}, percentage: {1}", firstWinCount, firstWinCount / (double)repeats * 100);
Console.WriteLine("Second win count: {0}, percentage: {1}", secondWinCount, secondWinCount / (double)repeats * 100);
Console.WriteLine("Avg sync diff: {0}ns", totalTimeDiff * 1000000 / repeats);
}
The output is:
First win count: 950538, percentage: 95,0538
Second win count: 49462, percentage: 4,9462
Avg sync diff: 1012ns
As we can see, most of the time first task starts executing earlier then second because it gets into thread pool first:
task1.Start();
task2.Start();
Since ThreadPool is quite unpredictable in a way tasks are scheduled, there is absolutely no guarantee that first task will not complete until second task starts. So it is hard to ensure we are actually testing multithreading scenario.
Surprisingly I couldn't find similar questions on the internet.
My own considerations and ideas with AutoResetEvents, locks and Interlock synchronization constructions led to the following solution of task synchronization:
int sync = 0;
var task1 = new Task(() =>
{
Interlocked.Increment(ref sync);
while (Interlocked.CompareExchange(ref sync, 3, 2) != 2) ;
Interlocked.CompareExchange(ref x, 1, 0);
time1 = sw.ElapsedMilliseconds;
});
var task2 = new Task(() =>
{
while (Interlocked.CompareExchange(ref sync, 2, 1) != 1) ;
Interlocked.CompareExchange(ref x, 2, 0);
time2 = sw.ElapsedMilliseconds;
});
Basically this idea ensures that both threads aren't blocked (so are likely having processor time) while waiting for other task to start processing. As a result I managed to reduce synchronization time difference from ~1000 nanoseconds to ~130 nanoseconds and greatly increase the probability of short-running tasks to be executed in parallel:
First win count: 23182, percentage: 2,3182
Second win count: 976818, percentage: 97,6818
Avg sync diff: 128ns
Remaining downside is that ordering of tasks is still quite well-defined: first task always waits for second to complete, and second, once knows first is waiting for it, doesn't wait anymore and starts executing it's job. So second job is likely to start first. Exclusins (2.3%) are possible due to [relatively rare] thread switching as far as I understand. I can workaround it with randomizing sync order, but it's another complication.
I wonder whether I'm reinventing the wheel and if there is better way to make sure two tasks are executed simultaneously and with even probability of each starting slightly earlier.
PS: I understand that multithreading scenarios usually are far slower then 100 nanoseconds (any thread switch or block on sync construction is at least 1000 times slower), so this delay in synchronization isn't that important in most cases. But it might be critical in testing non-blocking high-performance code.
Aucun commentaire:
Enregistrer un commentaire