繁体   English   中英

尝试在另一个线程上运行异步方法时,为什么会收到警告?

[英]Why do I get a warning when trying to run an async method on another thread?

我有一个异步方法,该方法调用了另一个异步方法,但是,我希望它在单独的线程上并行运行:

public async Task<Page> ServePage() {
  Task.Run(() => DoThings(10));   // warning here

  // ... other code
  return new Page();
}

public async Task DoThings(int foo) {
  // stuff
}

警告状态:

因为不等待此调用,所以在调用完成之前将继续执行当前方法。 考虑将“ await”运算符应用于调用结果。

实际上,这就是我正在尝试做的事情。 为什么会收到编译器警告? Task.Run的语法不正确吗?

TL; DR

您收到警告的原因是

 Task.Run(() => DoThings(10));   // warning here

返回一个Task,并且由于您的ServePage方法被标记为异步,因此编译器认为您应该等待Task的结果

详情

您正在混合两种截然不同的范例,它们恰好都涉及Task ,即:

  • Task.Run() ,通常通过利用多个可用内核来并行化CPU绑定的工作
  • async / await ,对等待I / O绑定的操作完成而没有阻塞(浪费)线程很有用。

因此,例如,如果您想同时执行3 x CPU绑定的操作,并且由于Task.Run返回Task ,那么您可以做的是:

public Page ServePage() // If we are CPU bound, there's no point decorating this as async
{
    var taskX = Task.Run(() => CalculateMeaningOfLife()); // Start taskX
    var taskY = Task.Run(() => CalculateJonSkeetsIQ()); // Start taskY
    var z = DoMoreHeavyLiftingOnCurrentThread();
    Task.WaitAll(taskX, taskY); // Wait for X and Y - the Task equivalent of `Thread.Join`

    // Return a final object comprising data from the work done on all three tasks
    return new Page(taskX.Result, taskY.Result, z);
}

上面可能需要三个线程,如果有足够的核心可以同时执行CPU绑定工作。

这与async / await相反,后者通常用于在等待I / O绑定调用完成时释放线程。 假设DoThings确实受I / O约束,并且看起来像

public async Task<string> DoThings(int foo) {
   var result = await SomeAsyncIo(foo);
   return "done!";
}

您可以并行执行但仍然可以异步执行以下操作:

public async Task<Page> ServePage() {
   var task1 = DoThings(123); // Kick off Task 1
   var task2 = DoThings(234); // Kick off Task 2 in parallel with task 1
   await Task.WhenAll(task1, task2); // Wait for both tasks to finish, while releasing this thread
   return new Page(task1.Result, task2.Result); // Return a result with data from both tasks
}

如果I / O绑定工作花费了合理的时间,那么很有可能在await Task.WhenAll期间, await Task.WhenAll零线程正在运行时,这await Task.WhenAll -请参见Stephen Cleary的文章

有第三个但非常危险的选择,那就是失火。 由于方法DoThings已被标记为async ,它已经返回了Task ,因此根本不需要使用Task.Run 忘却了,看起来如下:

public Page ServePage() // No async
{
  #pragma warning disable 4014 //   warning is suppresed by the Pragma
  DoThings(10);   // Kick off DoThings but don't wait for it to complete.
  #pragma warning enable 4014

  // ... other code
  return new Page();
}

根据@JohnWu的评论,“开火即忘记”方法很危险,通常会指示出设计气味。 这里这里更多

编辑

回覆:

有一点细微的差别使我一遍又一遍地逃避,例如调用异步方法从同步方法返回Task的情况被触发并忘记了该方法的执行。 (这是最后一个代码示例。)我是否正确理解?

这有点难以解释,但是无论是否使用await关键字调用,在第一次等待之前调用的async方法中的任何同步代码都将在调用者的线程上执行,除非我们采用Task.Run类的Task.Run

也许此示例可能有助于理解(请注意,我们故意使用同步Thread.Sleep而不是await Task.Delay来模拟CPU绑定工作并引入可以观察到的延迟)

public async Task<Page> ServePage()
{
  // Launched from this same thread, 
  // returns after ~2 seconds (i.e. hits both sleeps) 
  // continuation printed.
  await DoThings(10);

  #pragma warning disable 4014
  // Launched from this same thread, 
  // returns after ~1 second (i.e. hits first sleep only) 
  // continuation not yet printed
  DoThings(10);   

  // Task likely to be scheduled on a second thread
  // will return within few milliseconds (i.e. not blocked by any sleeps)
  Task.Run(() => DoThings(10));   

  // Task likely to be scheduled on a second thread
  // will return after 2 seconds, although caller's thread will be released during the await
  // Generally a waste of a thread unless also doing CPU bound work on current thread, or unless we want to release the calling thread.
  await Task.Run(() => DoThings());   

  // Redundant state machine, returns after 2 seconds
  // see return Task vs async return await Task https://stackoverflow.com/questions/19098143
  await Task.Run(async () => await DoThings());   
}

public async Task<string> DoThings(int foo) {
   Thread.Sleep(1000); 
   var result = await SomeAsyncIo(foo);
   Trace.WriteLine("Continuation!"); 
   Thread.Sleep(1000); 
   return "done!";
}

还有一点要注意-在大多数情况下,不能保证await之后的继续代码将与await之前的线程在同一线程上执行。 编译器将延续代码重写为一个Task,并且该延续任务将在线程池中进行调度。

这个

Task.Run(() => DoThings(10));

将“并行”运行您的单独任务,这意味着另一个线程将运行此任务。 输入该方法的线程将继续执行下一条语句。

允许您在这里做什么。 这就是为什么这是一个警告。 (我假设该方法的其余部分(未显示)返回Page 。)

该消息警告您,由于另一个任务正在另一个线程上执行,因此它可以在方法中的另一个代码之前,之后或同时执行。 此方法不再“知道”任务在做什么或何时完成。

换句话说,就是:

不要以为是因为这行代码会在要执行的方法中的其他代码行之前出现。 如果此任务完成后需要执行某些操作,请在执行下一步之前等待它。

举个例子:

public int DoSomething()
{
    var x = 1;
    Task.Run(() => x++);
    return x;        
}

这会返回什么? 这取决于。 它可以返回1或2。它可以在x递增之前或之后返回。 如果您关心x是否已递增,那么这很不好。 如果您的任务执行的操作与方法的其余部分无关,而您根本不在乎任务是否在方法的其余部分之前或之后完成,则此警告对您而言无关紧要。 如果您确实在乎,那么这很重要,并且您要等待任务。

当您调用返回任务的异步方法时,您可以做两件事:

  1. 等待任务,任务立即将控制权返回给调用者,并在任务完成时继续执行(有或没有返回值)。 您还可以在这里捕获执行任务时可能发生的任何异常。

  2. 火与忘了。 这是当您启动任务并将其完全删除(包括捕获异常的功能)时。 最重要的是,除非等待,否则控制执行将超出调用范围,并且可能导致意外的状态损坏问题。

您在代码中排名第二。 尽管在技术上允许,但由于上述原因,编译器警告您。

不过,您要问的是-如果您真的只是想开枪而忘记了,为什么还要创建该显式任务? 您可以直接调用DoThings(10),不是吗? 除非我在代码中缺少我看不见的东西。 所以-您不能做到这一点吗?

public async Task<Page> ServePage() {
  DoThings(10); 
}

以下所有版本均有效,无警告,或多或少等效:

public Task ServePage1()
{
    return Task.Run(async () => await DoThings(10));
}

public async Task ServePage2()
{
    await Task.Run(async () => await DoThings(10));
}

public Task ServePage3()
{
    return Task.Run(() => DoThings(10));
}

public async Task ServePage4()
{
    await Task.Run(() => DoThings(10));
}

通常,您不应该通过忽略Task.Run的返回值来Task.Run 如果这样做,编译器将发出警告,因为它很少是有意的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM