CountdownEvent和Barrier的多线程用途有什么区别?

4

看一下 Barrier 类,它允许 n 个线程在同一时间点上进行会合:

static Barrier _barrier = new Barrier(3);
static void Main()
{
    new Thread(Speak).Start();
    new Thread(Speak).Start();
    new Thread(Speak).Start();
}
static void Speak()
{
    for (int i = 0; i < 5; i++)
    {
        Console.Write(i + " ");
        _barrier.SignalAndWait();
    }
}
//OUTPUT: 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4

但是 CountdownEvent 类也一样:
static CountdownEvent _countdown = new CountdownEvent(3);
static void Main()
{
    new Thread(SaySomething).Start("I am thread 1");
    new Thread(SaySomething).Start("I am thread 2");
    new Thread(SaySomething).Start("I am thread 3");
    _countdown.Wait(); // Blocks until Signal has been called 3 times
    Console.WriteLine("All threads have finished speaking!");
}
static void SaySomething(object thing)
{
    Thread.Sleep(1000);
    Console.WriteLine(thing);
    _countdown.Signal();
}


// output : 
I am thread 3
I am thread 1
I am thread 2
All threads have finished speaking!

似乎Barrier会阻塞直到n个线程相遇,而CountdownEvent也会阻塞直到有n个线程发出信号。
这对我来说有点混乱,我不知道在什么情况下应该使用哪一个。
问题:
在哪些(现实生活)场景中应该选择使用Barrier而不是CountdownEvent(反之亦然)?

第二个程序在 _countdown.Signal() 处抛出了 InvalidOperationException 异常。 - Keyur Ramoliya
1
@KeyurRamoliya 你应该杀死之前正在执行的线程。这样就不会出现异常(我刚测试过)。请看这里:https://i.imgur.com/3Ib2i5Z.jpg - Royi Namir
1
完美。这是我的错误。 - Keyur Ramoliya
1
有人可以发布展示区别的示例代码。但如果这样的示例已经在问题中给出,那么这样做就没有太大意义了。只需要尝试使用CountDownEvent实现第一个示例的行为,那么差异将更加明显。 - Hans Passant
1个回答

5
有关这两个类的一些有趣的事情需要注意:
  • CountdownEvent没有明确的后续操作; Barrier则有。
  • 一个只有一个阶段的CountdownEvent和一个Barrier大致相等,因此可以互换使用。
  • Barrier可以有多个阶段。当每个阶段完成时,将执行后续阶段操作;当该操作完成时,下一个阶段开始。

在SO上有一个类似的问题讨论了这种行为,但是针对的是C#类的Java等效类。答案给出了几个例子,这些例子也适用于C#的等效类。

也就是说,考虑一个真实的场景:检查三个信贷来源是否适合潜在购房者的贷款。假设您不想在收到所有3个信用评分并对其进行评估之前作出决定。你可以使用CountdownEvent(在Wait()之后的代码中检查分数)或具有单个阶段的Barrier来实现。

这里,Barrier是更好的选择:假设贷款官员还想检查购房者的SO声誉分数(因为专家用户获得更好的信用!)以及其他两个社交分数,但仅在检索到信用评分后才进行检查(因为嘿,如果我们不必要,就不要检查社交媒体)。Barrier的好处在于您可以在单个方法调用中通过各个阶段,这使逻辑紧凑且整洁:

var barrier = new Barrier(participantCount: 3, b => LogScoreAndPossiblyEvaluate(b));
var credit = new int[3];
var social = new int[3];

void LogScoreAndPossiblyEvaluate(Barrier b)
{
    Log.Info("Got scores for {b.CurrentPhaseNumber == 1 ? "credit" : "social"} phase");
     ...
    if (b.CurrentPhaseNumber == 2 && SomeComplexCalculationWithSixScores() == LoanResult.Approved)
        LoanMoney(); 
}

...

for (int i=0; i<3; i++)
    Task.Run(() => {
        credit[i] = GetCreditScore(CreditSource(i);
        barrier.SignalAndWait();

        social[i] = GetSocialScore(SocialSource(i);
        barrier.SignalAndWait();

        // all phases done at this point 
    });

一个CountdownEvent没有与之关联的显式后续操作;而Barrier有。---- 但这就是main函数中最后一个console.write所做的事情。(在倒计时事件示例中) - Royi Namir
我的意思是Barrier接受一个在每个阶段结束时调用的操作。CountdownEvent不接受操作,你必须在Wait之后执行你的操作。 - Kit
请问您能否澄清一下关于贷款示例的问题 - 为什么我不能简单地使用Task.WhenAll?在这种情况下使用Barrier/CountDownEvent的好处是什么? - BornToCode
1
@BornToCode,对于这个例子,你可以这样做,这会更简单。如果你“只想在所有任务都完成后才继续”,那么这是有意义的;如果你“想要在完成5个任务中的4个时继续”,那么Task.WhenAll就不适用了。这完全取决于你的情况。 - Kit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接