【问题标题】:How does C# Task.WaitAll() combine object states into one?C# Task.WaitAll() 如何将对象状态合并为一个?
【发布时间】:2016-03-19 19:09:57
【问题描述】:

以一个简单的酒店实体为例:

class Hotel
{
    public int NumberOfRooms { get; set; }
    public int StarRating { get; set; }
}

请考虑 C# 5.0 中的以下代码:

public void Run()
{
    var hotel = new Hotel();
    var tasks = new List<Task> { SetRooms(hotel), SetStars(hotel) };
    Task.WaitAll(tasks.ToArray());
    Debug.Assert(hotel.NumberOfRooms.Equals(200));
    Debug.Assert(hotel.StarRating.Equals(5));
}

public async Task SetRooms(Hotel hotel)
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    hotel.NumberOfRooms = 200;
}

public async Task SetStars(Hotel hotel)
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    hotel.StarRating = 5;
}

对 Debug.Assert() 的两次调用都成功通过。我不明白两个任务完成后,Hotel 的实例如何包含并行运行的两个方法的分配。

我认为当调用await 时(在SetRooms() 和SetStars() 中),会创建酒店实例的“快照”(将NumberOfRooms 和StarRating 都设置为0)。所以我的期望是这两个任务之间会有一个竞争条件,最后一个运行的将是复制回hotel 的那个,在两个属性之一中产生一个 0。

显然我错了。你能解释一下我在哪里误解了 await 的工作原理吗?

【问题讨论】:

  • 你从哪里得到关于“快照”的想法? :)
  • 从编译器创建的状态机的描述我猜...? :(

标签: c# asynchronous async-await task-parallel-library task


【解决方案1】:

我认为当调用 await 时(在 SetRooms() 和 SetStars()),创建酒店实例的“快照”

您的Hotel 类是一个引用类型。当您使用 async-await 时,您的方法将转换为状态机,并且该状态机将 reference 提升到您的变量上。这意味着创建的两个状态机都指向相同的Hotel 实例。您的Hotel 没有“快照”或深层副本,编译器不会这样做。

如果你想看看实际发生了什么,you can have a look at what the compiler emits 一旦它转换了你的异步方法:

[AsyncStateMachine(typeof(C.<SetRooms>d__1))]
public Task SetRooms(Hotel hotel)
{
    C.<SetRooms>d__1 <SetRooms>d__;
    <SetRooms>d__.hotel = hotel;
    <SetRooms>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    <SetRooms>d__.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = <SetRooms>d__.<>t__builder;
    <>t__builder.Start<C.<SetRooms>d__1>(ref <SetRooms>d__);
    return <SetRooms>d__.<>t__builder.Task;
}
[AsyncStateMachine(typeof(C.<SetStars>d__2))]
public Task SetStars(Hotel hotel)
{
    C.<SetStars>d__2 <SetStars>d__;
    <SetStars>d__.hotel = hotel;
    <SetStars>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    <SetStars>d__.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = <SetStars>d__.<>t__builder;
    <>t__builder.Start<C.<SetStars>d__2>(ref <SetStars>d__);
    return <SetStars>d__.<>t__builder.Task;
}

您可以看到这两种方法都将hotel 变量提升到它们的状态机中。

所以我的期望是在 两项任务,最后一项运行将是复制回酒店的一项 在两个属性之一中产生 0。

现在您看到了编译器的实际作用,您可以理解实际上不存在竞争条件。修改的是Hotel的同一个实例,每个方法设置不同的变量。


旁注

也许您编写此代码只是作为示例来解释您的问题,但如果您已经在创建异步方法,我建议使用Task.WhenAll 而不是阻塞Task.WaitAll。这意味着将Run 的签名更改为async Task 而不是void:

public async Task RunAsync()
{
    var hotel = new Hotel();
    await Task.WhenAll(SetRooms(hotel), SetStars(hotel));
    Debug.Assert(hotel.NumberOfRooms.Equals(200));
    Debug.Assert(hotel.StarRating.Equals(5));
}

【讨论】:

  • 谢谢@yuval-itzchakov。使用Task.WhenAll() 比使用Task.WaitAll() 有什么优势?
  • Task.WhenAll 是非阻塞的,因此异步将控制权交给调用方法,而不是同步阻塞的Task.WaitAll。此外,异常处理略有不同。有关更多信息,请参阅this 问题。
  • 看来我会在Task.WhenAll()返回的Task`上立即调用Wait(),优势是否仍然存在?
  • @YuvalItzchakov,这就是为什么我说“如果你从 UI 线程做”;)
  • @urig 你不需要用Wait 阻止。让您的操作返回Task&lt;T&gt;,您也可以在控制器操作中等待
猜你喜欢
  • 1970-01-01
  • 2019-02-07
  • 2020-01-17
  • 2018-12-31
  • 1970-01-01
  • 2018-02-09
  • 2021-07-27
  • 2021-06-26
  • 1970-01-01
相关资源
最近更新 更多