如何确保任务同时执行?

时间:2017-08-03 14:29:54

标签: c# .net multithreading testing synchronization

我正在实施一些多线程单元测试,并提出了一个问题,即难以确保两个作业实际并行执行 - 一个始终比另一个更早开始。让我们考虑一下我最初的测试场景实现来演示行为:

Safari

输出结果为:

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);
}

正如我们所看到的,大部分时间第一个任务开始先执行然后再执行第二个因为它首先进入线程池:

First  win count: 950538, percentage: 95,0538
Second win count: 49462, percentage: 4,9462
Avg sync diff: 1012ns

由于ThreadPool在安排任务的方式上非常不可预测,绝对无法保证在第二个任务开始之前第一个任务无法完成。因此很难确保我们实际测试多线程场景。

令人惊讶的是,我无法在互联网上找到类似的问题。

我自己对AutoResetEvents,锁和互锁同步结构的考虑和想法导致了以下任务同步解决方案:

task1.Start();
task2.Start();

基本上这个想法可以确保在等待其他任务开始处理时,两个线程都不会被阻塞(因此很可能有处理器时间)。因此,我设法将同步时间差从大约1000纳秒减少到大约130纳秒,并大大增加了并行执行短期任务的可能性:

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;
});

剩下的缺点是任务的排序仍然很明确:第一个任务总是等待第二个完成,第二个,一旦知道第一个等待它,不再等待并开始执行它&#39的工作。所以第二份工作可能会先开始。据我所知,由于[相对罕见的]线程切换,排除(2.3%)是可能的。我可以通过随机化同步顺序解决它,但它是另一个复杂问题。

我想知道我是否重新发明轮子,是否有更好的方法可以最大限度地同时执行两个任务的概率,并且每个任务的启动概率都更早。

PS:据我所知,多线程场景通常远慢于100纳秒(任何线程切换或同步构造块的速度至少要慢1000倍),因此同步延迟不会超过100纳秒。在大多数情况下这很重要。但它在测试非阻塞高性能代码时可能至关重要。

2 个答案:

答案 0 :(得分:0)

我会使用ManualResetEvent。

类似的东西:

var waitEvent = new ManualResetEvent(false);


var task1 = new Task(() =>
{
    waitEvent.WaitOne();
    Interlocked.CompareExchange(ref x, 1, 0);
    time1 = sw.ElapsedMilliseconds;
});
var task2 = new Task(() =>
{
    waitEvent.WaitOne();
    Interlocked.CompareExchange(ref x, 2, 0);
    time2 = sw.ElapsedMilliseconds;
});
task1.Start();
task2.Start();

// a startup delay? so the thread can be queued/start executing
// but still then, you're not aware how busy the threadpool is.
Thread.Sleep(1000);

waitEvent.Set();

Task.WaitAll(task1, task2);

答案 1 :(得分:0)

所以似乎没有更好的解决方案,然后我的联想同步的想法,所以我将其实现为可重用的类,并添加了启动顺序的随机化,以确保启动顺序的平等机会:

awaitSpace

此实施中的同步精度约为 130 ns ,并且每项行动赢得比赛的概率非常接近50%。

我找到了一种通过在高优先级前台线程上调度这些任务来进一步微调同步精度的方法,但我认为这对我来说太过分了。如果有人发现它有用,仍然分享:

<OrderBatch>
  <ShopDetails>
        <ShopName>ShopALot</ShopName>
        <depoNumberID>121</depoNumberID>
        <totalOrderID>2424</totalOrderID>
        <AmountOfOrdersToday>2</AmountOfOrdersToday>
  </ShopDetails>
  <OrderInfo>
        <OrderID>1</OrderID>
        <ItemsInTheOrder>3</ItemsInTheOrder>
        <OrderDate>05/08/2017</OrderDate>
        <depoPostCode>N4 5TS</depoPostCode>
  </OrderInfo>
  <OrderInfo>
        <OrderID>2</OrderID>
        <ItemsInTheOrder>1</ItemsInTheOrder>
        <OrderDate>05/08/2017</OrderDate>
        <depoPostCode>N4 5TS</depoPostCode>
  </OrderInfo>

这使我可以将同步精度优化为 ~100 ns

public class Operations
{
    private static int _runId = 0;

    public static void ExecuteSimultaneously(Action action1, Action action2)
    {
        Action slightlyEarlierStartingAction;
        Action slightlyLaterStartingAction;

        if (Interlocked.Increment(ref _runId) % 2 == 0)
        {
            slightlyEarlierStartingAction = action1;
            slightlyLaterStartingAction = action2;
        }
        else
        {
            slightlyEarlierStartingAction = action2;
            slightlyLaterStartingAction = action1;
        }

        int sync = 0;

        var taskA = new Task(() =>
        {
            Interlocked.Increment(ref sync);
            while (Interlocked.CompareExchange(ref sync, 3, 2) != 2) ;

            slightlyLaterStartingAction();
        });

        var taskB = new Task(() =>
        {
            while (Interlocked.CompareExchange(ref sync, 2, 1) != 1) ;

            slightlyEarlierStartingAction();
        });

        taskA.Start();
        taskB.Start();

        Task.WaitAll(taskA, taskB);
    }
}

警告:使用优先级最高的线程可能会限制计算机的响应速度,尤其是在没有免费处理器核心的情况下。