是否可以等待未声明为异步的 IO 操作?如果没有,我该怎么办?

Is it possible to await an IO operation that is not declared as async? If not, what should I do?

我是 C# 异步编程的新手,但我仍然对一些事情感到困惑。 我读到在 .NET 4.5 之后,不再建议将 APM 和 EAP 用于新开发,因为 TAP 应该取代它们 (source)。

我想我了解 async/await 的工作原理并且我能够使用它们来执行具有异步方法的 IO 操作。例如,我可以编写等待 HttpWebClient 的 GetStringAsync 结果的异步方法,因为它被声明为异步方法。太棒了

我的问题是:如果我们有一个 IO 操作发生在未声明为异步的方法中怎么办?像这样:假设我有一个 API 有一个方法

string GetResultFromWeb()

从 Web 查询内容。我有很多不同的查询要做,我 必须 使用此方法来完成。然后我需要处理每个查询结果。我知道如果那是一个异步方法我会这样做:

Task<string> getResultTask = GetResultFromWeb(myUrl); 
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

但由于它不是,我不能等待它 -- 它告诉我字符串不可等待。所以我的问题是:是否有任何方法可以异步执行这些 IO 操作,而不必为每个查询创建一个线程?如果可以的话,我想创建尽可能少的线程,而不必阻塞任何线程。

我发现这样做的一种方法是实施 APM,遵循 Jeffrey Richter 的 this article,然后在我的 Begin 方法中调用 ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult)。像这样:

public class A {
    private void DoQuery(Object ar){
        AsyncResult<string> asyncResult = (AsyncResult<string>) ar;
        string result = GetResultFromWeb();
        asyncResult.SetAsCompleted(result, false);
    }

    public IAsyncResult BeginQuery(AsyncCallback){
        AsyncResult<string> asyncResult = new AsyncResult<string>(callback, this);
        ThreadPool.QueueUserWorkItem(DoQuery, asyncResult);
        return asyncResult;
    }

    public string EndQuery(IAsyncResult ar){
        AsyncResult<string> asyncResult = (AsyncResult<string>)ar;
        return asyncResult.EndInvoke();
    }
}

然后我使用 AsyncEnumerator 并开始 (BeginQuery) 多个查询并在每个查询完成时处理结果(使用 yield return / EndQuery)。这似乎运作良好。但是在阅读了这么多 APM 已过时之后,我想知道如何使用 TAP 来做到这一点。另外,这种APM方法有什么问题吗?

谢谢!

一种更简单的方法来完成您正在寻找的事情是使用 Task class 调用该方法。在您的情况下,它看起来像这样:

Task<string> getResultTask = Task.Run<string>(()=>GetResultFromWeb(myUrl));
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

虽然这将像您的 IAsyncResult 选项那样创建另一个线程,但这大大简化了过程。

您的 API 是异步的,使用旧的 Begin/End 模型。这符合 TPL via

Task.Factory.FromAsync<string>(BeginQuery, EndQuery)

哪个returns一个Task<string>哪个你可以await

what if we have an IO operation that happens in a method that is not declared as async?

在这种情况下,I/O 操作是阻塞的。换句话说,GetResultFromWeb 阻塞调用线程。在我们完成其余部分时请记住这一点...

I must use this method to do so.

据此我推断您不能编写异步的 GetResultFromWebAsync 方法。所以任何执行网络请求的线程必须被阻塞。

is there any way of performing these IO operations asynchronously without having to create one thread for each query?

最自然的做法是写一个GetResultFromWebAsync方法。由于这是不可能的,您的选择是:阻止调用线程,或阻止其他线程(即线程池线程)。阻塞线程池线程是一种我称之为 "fake asynchrony" 的技术——因为它看起来是异步的(即不阻塞 UI 线程)但实际上不是(即它只是阻塞线程池线程)。

If I could, I'd like to create as less threads as possible, without having to block any of the threads.

鉴于限制,这是不可能的。如果您必须使用GetResultFromWeb方法,而该方法阻塞调用线程,则线程必须 被屏蔽了。

One way I found to do so was by implementing APM, following this article from Jeffrey Richter and then, in my Begin method, I call ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult).

在这种情况下,您的代码公开了一个异步 API (begin/end),但在实现中它只是在线程池线程上调用 GetResultFromWeb。即,它是伪异步。

This seems to work well.

它可以工作,但不是真正的异步。

But after reading so much that APM is obsolete, I was wondering how could I do this using TAP.

正如其他人所指出的,有一种更简单的方法可以将工作安排到线程池:Task.Run

真正的异步是不可能的,因为您必须使用 阻塞 方法。因此,您所能做的就是解决方法 - 假异步,a.k.a。阻塞线程池线程。最简单的方法是:

Task<string> getResultTask = Task.Run(() => GetResultFromWeb(myUrl)); 
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

(比 APM 和 AsyncEnumerator 更简洁的代码)

请注意,我 建议创建一个 GetResultFromWebAsync 使用伪异步实现的方法。返回任务的异步后缀方法应该遵循 Task-based Asynchronous Pattern guidelines,这意味着 true 异步。

换句话说,正如我在我的博客上更详细地描述的那样,use Task.Run to invoke a method, not to implement a method