理解异步 - 我可以等待同步方法吗?

5

试图理解C#中的async-await,但可能陷入了一个“先有鸡还是先有蛋”的问题。

一个async方法需要调用另一个async方法才能变成异步吗?

作为一个高级示例,我想尝试对文件系统进行简单的写操作,但是不确定如何使此任务可等待,如果有可能的话。

public Task<FileActionStatus> SaveAsync(path, data)
{
    // Do some stuff, then...

    File.WriteAllBytes(path, data); // <-- Allow this to yield control?

    // ... then return result
}

那行代码在我试图使方法异步的情况下被调用。因此,当文件正在被写入时,我希望将控制权交给应用程序,但不太确定该如何做。

有人能否用一个非常高级的示例来启发我,告诉我如何以async方式向文件系统写入文件?


3
关于“异步方法是否需要调用另一个异步方法才能变成异步”的问题,我想提出一个评论 - 这个问题是相反的。在我看来,更好的做法是首先考虑操作本身(例如,写入文件),询问它是否自然地是异步的(即,如果以同步方式实现会阻塞线程),找到适当的异步 API(例如,没有WriteAllBytesAsync但你可以自己创建),最后使调用方的方法变为异步,这样就可以使用await调用API了。 - Stephen Cleary
@StephenCleary 绝佳的建议,这是一个非常好的思考方式。谢谢! - trnelson
2个回答

7

异步方法需要调用另一个异步方法才能成为异步吗?

通常情况下是这样的,因为异步一直到调用堆栈的底部,并在实际执行IO操作的最低位置结束。在您的情况下,您正在使用File.WriteAllBytes,它是一个阻塞同步调用。您不能通过魔法使其异步。

有人能给我举一个非常高级的例子,说明如何以异步方式将文件写入文件系统吗?

要做到这一点,您需要使用公开异步API的机制,例如FileStream

public async Task<FileActionStatus> SaveAsync(string path, byte[] data) 
{
    using (FileStream sourceStream = new FileStream(path,
    FileMode.Append, FileAccess.Write, FileShare.None,
    bufferSize: 4096, useAsync: true))
    {
        await sourceStream.WriteAsync(data, 0, data.Length);
    }
    // return some result.
}

请注意,虽然异步方法应该调用其他异步方法,但这些其他方法可以通过使用.NET 4.0风格来实现而不必使用async关键字。例如,它们可以使用Task.Factory.StartNewContinueWith方法来实现。您仍然可以使用await来异步等待这些异步方法的完成。 - Yacoub Massad
1
@YacoubMassad 是的,但使用 Task.Run 不是一种自然的异步操作。如果你使用它,你只是在使用异步 over 同步反模式,而不是真正地使用异步 IO。 - Yuval Itzchakov
@YuvalItzchakov,我同意你的观点,但我的意思是即使不使用“async”关键字,你仍然可以拥有异步方法。例如,你可以编写一个返回Task并通过TaskCompletionSource包装某些EAP操作的方法。 - Yacoub Massad
@YacoubMassad 嗯,使用 TaskCompletionSource 作为 EAP 的包装器与使用 Task.Run 是不同的。前者实际上只会将自然异步操作包装成可等待对象。 - Yuval Itzchakov
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Yacoub Massad

-1
除了Yuval的答案之外:如果你的类没有任何异步方法,你可以使用Task.Run在后台线程上运行该方法,这样你的代码可能会像这样:
public Task<FileActionStatus> SaveAsync(path, data)
{
  return Task.Run<FileActionStatus>(() =>
    {
      File.WriteAllBytes(path, data);
      // don't forget to return your FileActionStatus
    });
}

更新:线程会占用资源。在使用它们之前,请三思而后行。异步包装同步操作可以至少在 UI 响应和并行性方面带来好处。在选择一种方案之前,请测试不同的场景。在其他情况下,请使用单个线程。


2
那不是真的。你没有使用Task.Run来生成异步操作。你只是将一个同步操作排队,并在线程池线程上执行它。实际上,这是适得其反的,因为异步是为了在执行IO时释放线程,而这正好相反。这被称为异步超同步反模式,我不建议任何人使用它。 - Yuval Itzchakov
@svick 当然有地方。但是OP特别询问如何异步写入文件。虽然这可能是回答“在仅由同步方法公开的IO操作期间如何保持UI响应性”的问题的好选择,但这不是它。此外,人们确实会错误地查看此类答案并认为“好的,这就是您执行异步操作的方式”。因此,如果您选择这种路线,请确保解释采用该方式的影响。 - Yuval Itzchakov
@YuvalItzchakov 问问题的人并不总是使用正确的术语。问题的核心似乎是:“在文件被写入时,我想将控制权交给应用程序”,我认为这个答案回答得很好。(但我不清楚 OP 为什么要使用异步。) - svick
1
@svick,我想我们对问题的解释有所不同:)。我通常不同意这个答案说“你仍然可以通过使用Task.Run自己生成它”。不,你不能使用Task.Run来生成自然异步操作,但如果需要保持UI响应,则可以使用此操作。术语很重要。 - Yuval Itzchakov
@YuvalItzchakov 为什么不呢?在 async/await 时代之前,你是如何处理长时间操作的?你是在主线程上运行它吗?我希望不是。async/await 对这种方法有什么改变? - vzayko
显示剩余4条评论

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