【问题标题】:Is it possible to await an IO operation that is not declared as async? If not, what should I do?是否可以等待未声明为异步的 IO 操作?如果没有,我该怎么办?
【发布时间】:2015-01-06 22:55:31
【问题描述】:

我是 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 方法有什么问题吗?

谢谢!

【问题讨论】:

    标签: c# multithreading asynchronous task


    【解决方案1】:

    如果我们的 IO 操作发生在未声明为异步的方法中怎么办?

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

    我必须使用这种方法。

    由此我推断您不能编写异步的GetResultFromWebAsync 方法。因此,任何执行 Web 请求的线程必须被阻止。

    有没有什么方法可以异步执行这些 IO 操作而不必为每个查询创建一个线程?

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

    如果可以的话,我想创建尽可能少的线程,而不必阻塞任何线程。

    鉴于限制,这是不可能的。如果您必须使用GetResultFromWeb方法,并且该方法阻塞调用线程,那么线程必须被阻塞。

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

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

    这似乎运作良好。

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

    但是在阅读了这么多 APM 已经过时的文章后,我想知道如何使用 TAP 来做到这一点。

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

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

    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-returning, Async-suffix 方法应该遵循Task-based Asynchronous Pattern guidelines,这意味着 true 异步。

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

    【讨论】:

    • 很好的答案!谢谢!
    【解决方案2】:

    您的 API 使用旧的 Begin/End 模型是异步的。这通过

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

    它返回一个Task&lt;string&gt;,你可以await。

    【讨论】:

    • 除了使代码更简单之外,这样做有什么好处(性能)而不是仅仅坚持旧的 Begin/End 方法?
    • @DerekPatton:与其他异步调用交错更容易。
    • @DerekPatton:另外,我不知道你为什么认为会有性能优势。包装器永远不会比它所依赖的基础更快。
    • 您说我的 API 使用的是 Begin/End 模型。它不是。请注意,我自己创建了 Begin/End 方法。这是否意味着我必须创建 Begin/End 方法,然后每次我想等待非异步方法时使用这个 Task.Factory.FromAsync 调用?另外,这是否会在每次调用时创建一个新线程?
    • @DerekPatton:不,您不会仅仅为了包装它们而编写 Begin/End 方法。但是有很多现有的 API 使用 Begin/End。要获得良好的性能,您需要重写 GetResultFromWeb 以使用某种异步 API 进行网络 I/O。无论是 Begin/End、TPL async 还是其他任何东西,它们都可以变成 async。在您的特定情况下,使用异步 API 代理请求可能会更好,并使用立即响应的本地 URL 调用第三方 API。
    【解决方案3】:

    一种更简单的方法是使用Task 类调用该方法。在您的情况下,它看起来像这样:

    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 选项一样创建另一个线程,但这大大简化了流程。

    【讨论】:

    • 很好的答案,但是不建议使用仅委托给 Task.Run 的 async 方法,因为 async 方法通常不会启动新线程。
    • 很好,但这不是为每次调用 GetResultFromWebAsync 创建一个新线程吗?我认为 APM 会更高效,因为它不会为每个调用创建不同的线程,而是将任务排入队列并为这些任务交替使用更少数量的线程,从而消除创建新线程的开销。我错了吗?
    • @DerekPatton 我相信Task 类在内部使用了ThreadPool,如果我记得我读的是正确的话。
    • @NedStoyanov 那么实现这一点的更好方法是什么?这是我目前所知道的将非异步方法用作 acync 方法的最佳方法,但我一直在寻求改进。
    • 我认为建议不要添加以xxxxAsync结尾的方法,直接使用Task.Run
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-06
    • 2019-06-02
    • 2020-09-24
    • 1970-01-01
    • 1970-01-01
    • 2013-08-28
    • 2022-07-25
    相关资源
    最近更新 更多