【问题标题】:Why this order of events in a lambda expression?为什么在 lambda 表达式中出现这种事件顺序?
【发布时间】:2013-09-18 04:00:12
【问题描述】:

这是我正在运行的代码:

    [TestMethod]
    public async Task HelloTest()
    {
        List<int> hello = new List<int>();
       //await Task.WhenAll(Enumerable.Range(0, 2).Select(async x => await Say(hello)));
        await Say(hello);
        await Say(hello);
    }

    private static async Task Say(List<int> hello)
    {
        await Task.Delay(100);
        var rep = new Random().Next();
        hello.Add(rep);
    }

为什么按原样运行此代码会按预期工作并产生两个随机数,但使用注释代码总是会产生两个完全相同的数字?

【问题讨论】:

  • GotANumber 是做什么的?我在这里没有看到太多的语义差异
  • @olydis 只返回一个int
  • 为什么不向我们展示 GotANumber 实现?
  • 除非我遗漏了什么,否则async 的用法在这里只会令人困惑。 Say 方法是完全同步的,所以当它被等待的时候,它总是会返回一个完成的Task,所以调用的 awaiter 会自己继续同步。所以,如果这是正确的,那么你应该能够在没有async/await 的情况下重写问题并遇到同样的问题。
  • 保持问题简短只有在它也完整的情况下才有价值。

标签: c# async-await c#-5.0


【解决方案1】:

所以你这里有几个问题。

首先,为什么您看到两次相同的值。那是最容易的。当您创建 Random 实例时,它使用当前时间播种,但它使用的当前时间的精度相当低。如果您在大约 16 毫秒左右(对于计算机来说这是一个真的长时间)获得两个新的 Random 实例,您会从它们中看到相同的值。这就是你正在发生的事情。

通常解决方法是共享一个 Random 实例,但问题是您的随机实例没有从同一个线程访问(可能假设您没有 SynchronizationContext指定),并且Random 不是线程安全的。您可以使用类似的方法来获取随机数:

public static class MyRandom
{
    private static object key = new object();
    private static Random random = new Random();
    public static int Next()
    {
        lock (key)
        {
            return random.Next();
        }
    }

    //TODO add other methods for other `Random` methods as needed
}

使用它,它会解决眼前的问题。

您遇到的另一个问题,虽然它目前似乎并没有困扰您,但您正在从两个不同的任务修改您的List,可能在不同的线程中执行。你不应该那样做。在单线程环境中使用这样的方法已经够糟糕的做法了(因为您依赖副作用来完成工作),但在多线程环境中,这非常有问题,不仅仅是概念上的原因。相反,您应该让每个线程返回一个值,然后将所有这些值拉入调用方的集合中,如下所示:

public async Task HelloTest()
{
    var data = await Task.WhenAll(Say(), Say());
}

private static async Task<int> Say()
{
    await Task.Delay(100);
    return MyRandom.Next();
}

至于为什么两个 Say 调用是并行而不是顺序运行的,这与在您的第二个代码 sn-p 中您实际上并没有在开始之前等待一个任务完成的事实有关下一个。

传递给Select 的方法是启动任务的方法,它不会阻塞,直到该任务完成后再开始下一个任务。你在这里的代码:

await Task.WhenAll(Enumerable.Range(0, 2).Select(async x => await Say(hello)));

与简单地拥有没有什么不同:

await Task.WhenAll(Enumerable.Range(0, 2).Select(x => Say(hello)));

拥有一个除了等待一个方法调用之外什么都不做的async 方法实际上与只有一个方法调用没有什么不同。这里发生的情况是Select 正在调用Say,盯着任务,继续,统计下一个任务,然后WhenAll 正在(异步)等待两个任务完成,然后继续。

【讨论】:

  • 关于 Random 真的很有趣(随时间播种而不是线程安全的)。这对我来说是个新闻。
  • @MattSmith 在 SO 上的大多数日子里,问题都是由于第一个问题而发布的。它们很少被绑定到多线程应用程序中,但仍然不是闻所未闻。考虑到您遇到的问题数量,我真的很惊讶您以前从未遇到过它。
【解决方案2】:

注意:这是对原始问题的回答(问题已被更改)。

它们的操作方式相同。我运行了以下控制台应用程序并得到了两个版本的结果 0 1:

class Program
{
    static int m_nextNumber = 0;

    static void Main(string[] args)
    {
        var t1 = Version1();

        m_nextNumber = 0;
        var t2 = Version2();

        Task.WaitAll(t1, t2);
        Console.ReadKey();
    }

    static async Task Version1()
    {
        List<int> hello = new List<int>();
        await Say(hello);
        await Say(hello);
        PrintHello(hello);
    }

    static async Task Version2()
    {
        List<int> hello = new List<int>();
        await Task.WhenAll(Enumerable.Range(0, 2).Select(async x => await Say(hello)));
        PrintHello(hello);
    }

    static void PrintHello(List<int> hello)
    {
        foreach (var i in hello)
            Console.WriteLine(i);
    }

    static int GotANumber()
    {
        return m_nextNumber++;
    }

    static async Task Say(List<int> hello)
    {
        var rep = GotANumber();
        hello.Add(rep);
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-22
    • 2015-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-17
    • 1970-01-01
    相关资源
    最近更新 更多