Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm new to asynchronous programming in C# and I'm still confused about a few things. I've read that after .NET 4.5, the APM and EAP are no longer recommended for new development since the TAP is supposed to replace them (source).

I think I understood how async/await works and I'd be able to use them for performing IO operations that have async methods. For example, I could write an async method that awaits for an HttpWebClient's GetStringAsync result, since it's declared as an async method. That's great.

My question is: what if we have an IO operation that happens in a method that is not declared as async? Like this: suppose I have an API that has a method

string GetResultFromWeb()

which queries something from the Web. And I have lots of different queries to do and I must use this method to do so. And then I need to process each query result. I understand that I'd do this if that was an async method:

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

But since it's not, I cannot await for it -- it tells me string is not awaitable. So my question is: is there any way of performing these IO operations asynchronously without having to create one thread for each query? If I could, I'd like to create as less threads as possible, without having to block any of the threads.

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). Like this:

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();
    }
}

Then I use an AsyncEnumerator and begin (BeginQuery) several queries and process the results as each one of them finishes (using yield return / EndQuery). This seems to work well. But after reading so much that APM is obsolete, I was wondering how could I do this using TAP. Also, is there any problem with this APM approach?

Thanks!

like image 631
Derek Patton Avatar asked Jan 10 '23 03:01

Derek Patton


2 Answers

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

In this case, the I/O operation is blocking. In other words, GetResultFromWeb blocks the calling thread. Keep that in mind as we go through the rest...

I must use this method to do so.

By this I infer that you cannot write a GetResultFromWebAsync method which is asynchronous. So any thread doing the web requests must be blocked.

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

The most natural approach is to write a GetResultFromWebAsync method. Since that isn't possible, your options are: block the calling thread, or block some other thread (i.e., a thread pool thread). Blocking a thread pool thread is a technique I call "fake asynchrony" - since it appears asynchronous (i.e., not blocking the UI thread) but it's really not (i.e., it just blocks a thread pool thread instead).

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

That's not possible given the constraints. If you must use the GetResultFromWeb method, and that method blocks the calling thread, then a thread must be blocked.

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).

In this case, your code is exposing an asynchronous API (begin/end), but in the implementation it's just calling GetResultFromWeb on a thread pool thread. I.e., it is fake asynchrony.

This seems to work well.

It works, but it's not truly asynchronous.

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

As others have noted, there's a much easier way to schedule work to the thread pool: Task.Run.

True asynchrony isn't possible, because you have a blocking method that you must use. So, all you can do is a workaround - fake asynchrony, a.k.a. blocking a thread pool thread. The easiest way to do that is:

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);

(much cleaner code than APM and AsyncEnumerator)

Note that I do not recommend creating a GetResultFromWebAsync method that is implemented using fake asynchrony. The Task-returning, Async-suffix methods are supposed to follow the Task-based Asynchronous Pattern guidelines, which imply true asynchrony.

In other words, as I describe in more detail on my blog, use Task.Run to invoke a method, not to implement a method.

like image 194
Stephen Cleary Avatar answered Jan 14 '23 14:01

Stephen Cleary


Your API is asynchronous using the older Begin/End model. This fits into TPL via

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

which returns a Task<string> which you can await.

like image 43
Ben Voigt Avatar answered Jan 14 '23 14:01

Ben Voigt