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

时间:2019-04-25 17:58:05

标签: c# async-await

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

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的语法不正确吗?

4 个答案:

答案 0 :(得分:3)

TL; DR

收到警告的原因是

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

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

的结果

详细信息

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

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

例如,如果您想同时执行3个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相反,DoThings通常用于在等待I / O绑定调用完成时释放线程。假设public async Task<string> DoThings(int foo) { var result = await SomeAsyncIo(foo); return "done!"; } 确实受I / O约束,并且看起来像

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
}

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

await Task.WhenAll

如果I / O绑定工作花费合理的时间,则很有可能在DoThings期间实际运行零线程-参见Stephen Cleary's article

有第三个但非常危险的选择,那就是失火。由于方法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(); } 。忘却了,看起来像这样:

await

根据@JohnWu的评论,“火灾后遗忘”方法很危险,通常表示有设计气味。有关herehere

的更多信息

修改

回复:

  

有一个细微的差别使我一遍又一遍地逃脱,例如调用异步方法从同步方法返回Task的行为被抛弃了。 (这是最后一个代码示例。)我是否正确理解?

很难解释,但是无论在第一次等待之前,在调用的async方法中是否使用同步关键字Task.Run进行调用,无论是否使用Thread.Sleep关键字除非我们求助于await Task.Delay之类的锤子,否则它将在调用者的线程上执行。

也许此示例可能有助于理解(请注意,我们故意使用同步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来模拟CPU受限的工作并引入可以观察到的延迟)

String

还有另一点需要注意-在大多数情况下,不能保证在等待之后与streamsConfig.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG, "com.company.data.kstreams.Processor.MediaTimestampExtractor"); 之前的同一线程上执行继续代码。继续代码将由编译器重新写入Task中,并且继续任务将在线程池中进行调度。

答案 1 :(得分:1)

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

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

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

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

换句话说:

  

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

例如:

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

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

答案 2 :(得分:1)

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

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

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

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

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

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

答案 3 :(得分:0)

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

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的返回值来执行即发即弃的任务。如果这样做,编译器将发出警告,因为它很少是有意的。