Asynchronous network programming with async/await

2019-03-04 15:21发布

问题:

For the past few years, I've developed client/server software using the Asynchronous Programming Model and sockets. This example on MSDN, although overcomplicated with synchronisation mechanisms such as ManualResetEvents, illustrates the concepts: you use a BeginXXX() and EndXXX() method pair for the connection and stream operations.

This has the advantage of letting the thread pool assign threads as needed (e.g. when data is received) rather than having a dedicated thread per connection, which doesn't scale.

Someone recently mentioned that this approach can also be achieved using the async/await model introduced in .NET 4.5, thus using Tasks and making the APM unnecessary for this scenario. How is this done?

回答1:

The Task Asynchronous Pattern and its used keywords async-await lets you take use of async I/O (as you said) but in a "cleaner" fashion.

Instead of using BeginXXX and EndXXX methods and passing callbacks between each other, you can simply await on an async method which returns an awaitable (A Task is an example of one).

For example, lets make an example using HttpClient:

public async Task DoGetWebRequestAsync()
{
    var httpClient = new HttpClient();
    await httpClient.GetAsync(url);
}

What this will do is once the method hits the await keyword, it will yield control back to the calling method, creating a state-machine behind the scenes (which will be in charge of executing the continuation, propagating exceptions, etc..) and resume via an IOCP (IO Completion port, allocated by the ThreadPool) once the IO work is complete.

The TAP also offers a way to turn the old APM pattern into the TAP using TaskFactory.FromAsync:

FileInfo fi = new FileInfo(path);
byte[] data = null;
data = new byte[fi.Length];

FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read,
                               FileShare.Read, data.Length, true);

//Task<int> returns the number of bytes read
Task<int> task = Task<int>.Factory.FromAsync(
        fs.BeginRead, fs.EndRead, data, 0, data.Length, null);

Edit:

How to use async is simple. For example, with FileStream:

public async Task<byte[]> ReadBufferAsync(string path)
{
    FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read,
                                FileShare.Read, data.Length, true);

    // Read some bytes, not all, just for the example.
    byte[] buffer = new byte[2048];
    await fs.ReadAsync(buffer, 0, buffer.Length);

    return buffer;
}

You mark you method async so you can use the await keyword inside it, and then simply await on the method which reads.



回答2:

Actually, the old APM supported by the Socket class used a special thread pool, the IOCP ("I/O Completion Port") thread pool, and rather than assigning threads "when data is received", actually assigned threads as I/O operations are initiated, but in a way that allows a single thread (or small number of threads…e.g. as many as there are cores on the system) to handle a large number of socket objects and operations.

As far as how to use the new async/await style API, unfortunately the Socket class didn't get any direct async love. However, all of the wrappers did. The most straight-forward replacement is to use the NetworkStream class, and use ReadAsync() and WriteAsync(), which are awaitable methods.

If you want to still use the Socket API (essentially) directly, you'll have to wrap it yourself with an awaitable implementation. IMHO, a very good resource explaining one possible approach for doing this is here: Awaiting Socket Operations