Task<T>.Result 和字符串拼接

11

当我在使用 async / await 时,遇到了以下问题:

class C
{
    private static string str;

    private static async Task<int> FooAsync()
    {
        str += "2";
        await Task.Delay(100);
        str += "4";
        return 5;
    }

    private static void Main(string[] args)
    {
        str = "1";
        var t = FooAsync();
        str += "3";

        str += t.Result; // Line X

        Console.WriteLine(str);
    }
}

我原本期望的结果是“12345”,但实际上却是“1235”。不知何故,数字“4”被忽略了。

如果我将X行拆分为:

int i = t.Result;
str += i;

然后预期的结果是"12345"。

为什么会这样?(使用VS2012)


为什么这样做确实很有趣,但需要注意的是,您不应该这样做 - 共享对变量的访问,而这些操作不是原子操作,并且没有锁定或其他线程逻辑,这肯定会使您陷入不确定的情况。 - Chris Moschini
3个回答

10
为什么会这样?(使用 VS2012)
您正在运行控制台应用程序,这意味着没有当前的同步上下文。
因此,在 await 之后的 FooAsync() 方法部分在单独的线程中运行。 当您执行 str += t.Result 时,实际上是在 += 4 调用和 += t.Result 之间创建了竞争条件。 这是因为 string + = 不是原子操作。
如果您在Windows Forms或WPF应用程序中运行相同的代码,则会捕获并使用同步上下文来进行 + =“4”,这意味着所有内容都在同一线程上运行,您将不会看到此问题。

如果在“FooAsync”代码中发生str += 4之后才执行return 5,那么怎么可能会出现竞争条件呢? - Ilya Kogan
2
@IlyaKogan 因为+=不是原子操作。它被分解成几个子操作,这些操作可以(在这种情况下)交织在一起生成未定义的结果。 - Servy
1
@IlyaKogan 竞态条件发生的原因是 str += t.Result 实际上是 str = str + t.Result,且在 +=4 完成之前获取了“str”。 - Reed Copsey

7

C#语句形式x += y;在编译时会被扩展成x = x + y;

str += t.Result;会被扩展成str = str + t.Result;,其中str在获取t.Result之前被读取。此时,str的值是"123"。当FooAsync中的延续运行时,它修改了str并返回了5,因此str现在的值是"1234"。然而,在FooAsync的延续运行之前(即str仍为"123"),str的值与5相连以将结果赋给str,结果为"1235"

当你把它分成两个语句int i = t.Result; str += i;时,这种行为就不会发生。


行为仍然可能发生,即使分成两个语句。只是不太可能。请参见里德的答案。 - Stephen Cleary

6
这是一种竞态条件。在两个执行线程之间,您没有正确同步共享变量的访问。 您的代码可能像这样做:
  • 将字符串设置为“1”
  • 调用FooAsync
  • 追加2
  • 当调用await时,主方法继续执行,FooAsync中的回调将在线程池中运行;从现在开始,事情变得不确定。
  • 主线程将3附加到字符串
然后我们来到有趣的那一行:
str += t.Result;

这里将它分成了几个较小的操作。首先获取str的当前值。此时异步方法(很可能)尚未完成,因此它将是"123"。然后等待任务完成(因为Result强制执行阻塞等待),并将任务结果添加到字符串末尾,本例中为5
异步回调将在主线程已经获取str的当前值之后抓取并重新设置str,然后在主线程即将覆盖它之前覆盖str,而它从未被读取。

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