|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionNB: I removed comments and some details from the code for this article for brevity. To see the full code with comments download the sample project. In .NET, Microsoft had introduced so-called asynchronous design pattern. Suppose we have a method named The pattern consists of two additional methods for an asynchronous operation, an object that implements the The most important piece of the puzzle is the
As you can see, the if(!asyncResult.IsCompleted)
or if(!asyncResult.AsyncWaitHandle.WaitOne(0, false))
You may safely assume that both ways are pretty much the same. If you just want to wait until the operation is completed you should use code like this: asyncResult.AsyncWaitHandle.WaitOne();
Keep in mind that you should not use a loop that constantly queries the Another good thing is that if you perform a lot of similar asynchronous operations at once you may create an array of WaitHandle objects, fill it with values of The next piece of puzzle is the Now you see that The .NET class library provides us with many examples of asynchronous operations, the most notable is asynchronous delegates pattern that makes possible to call any method asynchronously. Most implementations of the pattern use Implementing the patternI distinguish two ways to implement the pattern. The first method is straightforward: the Executing a method on the background threadLet's code. Because my imagination is very limited I can't think of a useful example so I demonstrate the pattern with an oversimplified calculation task. Take a look at the AsyncMethod sample, my To turn the
That is actually it, nothing too complex. Let me summarize the rules of implementing the asynchronous pattern for a method:
IssuesFrom this sample you can see some drawbacks of the asynchronous design pattern. Let's review them. SynchronizationAlthough Microsoft states it in the documentation, I repeat it once more: the callback passed to a There's a trick that helps you to avoid expensive synchronization, I learned it from the weblog of Mike Taulty. He introduces a queue of Ability to cancel the pending operationTo my knowledge, no built-in classes allow you to cancel a pending operation. For instance, if you're sending a buffer with a Re-entranceIf you look at my Split cleanupThe asynchronous design pattern easily leads to hard to trace bugs if you forget to call All I can do is to repeat: don't forget to call MulticastsAs you know, all real delegates in .NET are multicasts. It means that a delegate invocation may end up as a series of methods calls. It is perfect for events so many objects can subscribe for a single event and a disaster for callbacks. So always check the callback delegates for being singlecasts: if(null != callback)
if(callback.GetInvocationList().Length > 1)
throw new InvalidOperationException("A callback must not be multicast.");
Exception propagationIt is unclear to me what happens in the BCL code if an exception occurs during the execution of an asynchronous method. It seems to me that the background operation just fails. However, it is clear that we can propagate the exception to the caller by adding a field of type Wrapping asynchronous calls chainsUsing the To help with the issue we should use a smarter approach. First, let's remember overlapped I/O. It is not very clear to me how it works, but from MSDN I gather that some services like file I/O and sockets queue requests for reading and writing and perform them at once in the context of a kernel thread. Obviously, it is faster and conserves system resources. Second, we can split long running tasks into atomic pieces and perform them step by step thus minimizing load on the thread pool. In this case you chain I/O calls using Let's write a sample program that downloads a file from Web servers using my favorite guinea pig, the hypertext transfer protocol (HTTP). Of course, the operation will be performed asynchronously. HTTP abstractsThe truth is, HTTP protocol is very simple so I won't use the standard A download of a single Web page in your browser's window consists of one or more download sessions, depending on the page's complexity. Each session downloads a single file or, strictly speaking, a resource. In HTTP 1.0 each session opens a connection to the server, in HTTP 1.1 many sessions may share a single connection. A session begins with a request. The request is a bunch of text lines. The first line always specifies the HTTP protocol method, the resource and the version of the protocol. The last line is empty. The rest of lines contain co-called HTTP headers, key-value pairs that describe optional parameters of the request. There are a lot of methods in HTTP protocol, but we are interested in the When the server receives the request, it sends back a response. The response consists of a header and a body. The response header, just like the request, is a bunch of text lines. The first line is the response success code, the last line is empty and the rest of the lines are also HTTP headers. The request body contains the data and can be either text or binary. HTTP 1.0 sends the response body in one piece, but HTTP 1.1 may split the data into chunks. Each chunk consists of a text line with the chunk length as a hexadecimal number and followed by the data. The last chunk has the length of zero and the whole data is followed by an empty line. HTTP GET implementation, common bitsBecause the protocol sometimes treats the bytes being sent as characters, it is very hard to use .NET-provided high-level I/O classes as All the processing is done in HTTP GET implementation, take oneThis implementation uses the asynchronous design pattern. The public class Getter: IHttpResponseHandler
{
public void Get(Uri uri, Stream output);
public IAsyncResult BeginGet(Uri uri, Stream output,
AsyncCallback callback, object state);
public void EndGet(IAsyncResult ar);
}
The funny thing is that the implementation of the public void Get(Uri uri, Stream output)
{
EndGet(BeginGet(uri, output, null, null));
}
Now let's implement the operation step by step. Resolve the name of the server: public IAsyncResult BeginGet(Uri uri, Stream output, AsyncCallback callback,
object state)
{
this.uri = uri;
this.output = output;
this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
this.asyncResult = new AsyncResult(callback, state);
this.recvBuffer = new byte[256];
Dns.BeginGetHostByName(uri.Host, new AsyncCallback(GetHostByNameDone),
null);
return asyncResult;
}
Connect to the server: void GetHostByNameDone(IAsyncResult ar)
{
try
{
IPHostEntry ihe = Dns.EndGetHostByName(ar);
IPAddress address = ihe.AddressList[0];
IPEndPoint trackerEndpoint = new IPEndPoint(address, uri.Port);
socket.BeginConnect(trackerEndpoint, new AsyncCallback(ConnectDone),
null);
}
catch(Exception)
{
Complete();
}
}
Send the request: void ConnectDone(IAsyncResult ar)
{
try
{
socket.EndConnect(ar);
MemoryStream output = new MemoryStream();
StreamWriter writer = new StreamWriter(output, Encoding.ASCII);
writer.Write("GET {0} HTTP/1.0\r\nHost: {0}\r\nUser-Agent:
AsyncHttpGet1\r\nConnection: close\r\n\r\n", uri, uri.Host);
writer.Flush();
byte[] toSend = output.ToArray();
socket.BeginSend(toSend, 0, toSend.Length, SocketFlags.None,
new AsyncCallback(SendDone), null);
}
catch(Exception)
{
Complete();
}
}
I send a bare minimum of the HTTP headers. The Host is required by the protocol, the User-Agent helps the Web site administrator to parse the server logs, and the Connection header notifies the server that we are not going to use "kept alive" connection and it can close it right after sending the response. Receive the response: void SendDone(IAsyncResult ar)
{
try
{
socket.EndSend(ar);
socket.Shutdown(SocketShutdown.Send);
Receive(new HttpResponseParser(this));
}
catch(Exception)
{
Complete();
}
}
void Receive(HttpResponseParser parser)
{
socket.BeginReceive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None,
new AsyncCallback(ReceiveDone), parser);
}
. . . . .
void ReceiveDone(IAsyncResult ar)
{
try
{
int received = socket.EndReceive(ar);
if(0 == received)
{
Complete();
return;
}
HttpResponseParser parser = (HttpResponseParser)ar.AsyncState;
bool cont = true;
for(int i = 0; i < received; i++)
{
cont = parser.ProcessByte(recvBuffer[i]);
if(!cont)
break;
}
if(cont)
Receive(parser);
else
Complete();
}
catch(Exception)
{
Complete();
}
}
I use a very small buffer for incoming data and kind of loop socket reading requests by using callbacks. In a real HTTP client it would be a good idea to allocate a buffer at least 8K bytes of size for start and then adjust it to chunk size. And, finally, the cleanup: public void EndGet(IAsyncResult ar)
{
if(null == ar)
throw new ArgumentNullException("ar");
AsyncResult r = ar as AsyncResult;
if((null == r) || !Object.ReferenceEquals(this.asyncResult, r))
throw new ArgumentException();
r.Validate();
r.AsyncWaitHandle.WaitOne();
r.Dispose();
asyncResult = null;
}
Now the program itself: static void Main(string[] args)
{
if(args.Length == 0)
{
Console.WriteLine("Usage: AsyncHttpGet1 <url> [<file_name>]");
return;
}
Uri inputUrl = new Uri(args[0]);
string outputFile;
if(args.Length < 2)
outputFile = Path.GetFileName(inputUrl.LocalPath);
else
outputFile = args[1];
using(Stream output = new FileStream(outputFile, FileMode.Create))
{
GetPoll(inputUrl, output);
GetCallback(inputUrl, output);
}
}
Note the conditional compilation directives that change the program behavior. When the POLL symbol is defined the main program polls the As you see, the asynchronous design pattern is easy to program and simple to use. ISynchronizeInvokeThis world would be a much better place if we kept all promises made. Unfortunately, we don't. For instance, while presenting the asynchronous design pattern, Microsoft broke it in one of the crucial places of the class library - in the I present here my own implementation of this interface, the Some time ago I published the article Another way to invoke UI from a Worker Thread. In the article I suggested to move the code dealing with the public class ExceptionEventArgs: EventArgs
{
readonly Exception exception;
public ExceptionEventArgs(Exception exception)
{
this.exception = exception;
}
public Exception Exception
{
get
{
return exception;
}
}
}
public delegate void ExceptionEventHandler(object sender,
ExceptionEventArgs e);
#if NET20
public static class ExceptionEvent
{
#else
public sealed class ExceptionEvent
{
private ExceptionEvent()
{}
#endif
public static void Raise(ExceptionEventHandler handler, object sender,
ExceptionEventArgs e)
{
if(null != handler)
{
foreach(
ExceptionEventHandler singleCast in handler.GetInvocationList())
{
ISynchronizeInvoke syncInvoke =
singleCast.Target as ISynchronizeInvoke;
try
{
if((null != syncInvoke) && (syncInvoke.InvokeRequired))
syncInvoke.Invoke(singleCast, new object[] {sender, e});
else
singleCast(sender, e);
}
catch(Exception exception)
{
throw new TargetInvocationException(exception);
}
}
}
}
}
I also declared a class named #if NET20
public static class Event
{
#else
public sealed class Event
{
private Event()
{}
#endif
public static void Raise(EventHandler handler, object sender, EventArgs e)
{
if(null != handler)
{
foreach(EventHandler singleCast in handler.GetInvocationList())
{
ISynchronizeInvoke syncInvoke =
singleCast.Target as ISynchronizeInvoke;
try
{
if((null != syncInvoke) && (syncInvoke.InvokeRequired))
syncInvoke.Invoke(singleCast, new object[] {sender, e});
else
singleCast(sender, e);
}
catch(Exception exception)
{
throw new TargetInvocationException(exception);
}
}
}
}
}
With such pattern an implementation of, for instance, protected virtual void OnCanceled(EventArgs e)
{
Event.Raise(Canceled, this, e);
}
Now get back to the business. Background workersAsynchronous methods are good for simple (atomic) background operations. But processes like downloading a file from the Internet may take hours to complete if you're downloading, for instance, a .NET Framework SDK installation file. For such long processes the Microsoft's pattern seems a bit awkward to me. Let's think a bit. What do we really need for a long running background operation? After it has started, we just need to know when in finishes or fails and we need a way to cancel it and get a notification that it is really cancelled to perform clean up. It would also be better if we could avoid or simplify the synchronization between workers and the main thread. Let's define an interface to generalize these requirements into an asynchronous operation: public interface IAsyncOp
{
event EventHandler Completed;
event EventHandler Canceled;
event ExceptionEventHandler Failed;
bool Cancel();
}
Now let's write another implementation of HTTP GET application, this time using my pattern. HTTP GET application, take twoFirst, derive the application class from the Synchronizer, add event handlers and the Main method: public class MainClass: Synchronizer
{
bool stop;
bool shouldWrite;
void Completed(object sender, EventArgs e)
{
Console.WriteLine("Complete!");
stop = true;
shouldWrite = true;
}
void Canceled(object sender, EventArgs e)
{
Console.WriteLine("Canceled.");
stop = true;
}
void Failed(object sender, ExceptionEventArgs e)
{
Console.WriteLine("Failed: {0}", e.Exception);
stop = true;
}
void Run(Uri uri, string outputFileName)
{
Getter getter = new Getter();
getter.Completed += new EventHandler(Completed);
getter.Canceled += new EventHandler(Canceled);
getter.Failed += new ExceptionEventHandler(Failed);
getter.Start(uri);
stop = false;
while(!stop)
Poll();
if(shouldWrite)
using(Stream output = new FileStream(outputFileName,
FileMode.Create))
output.Write(getter.Result, 0, getter.Result.Length);
}
static void Main(string[] args)
{
new MainClass().Run(new Uri(args[0]), args[1]);
}
}
Now write the new version of the Getter class that implements our public class Getter: IAsyncOp, IHttpResponseHandler
{
Uri uri;
Stream output;
Socket socket;
volatile bool cancel;
. . . . .
public event EventHandler Completed;
public event EventHandler Canceled;
public event ExceptionEventHandler Failed;
public void Start(Uri uri, Stream output)
{
this.uri = uri;
this.output = output;
this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
Dns.BeginGetHostByName(uri.Host, new AsyncCallback(GetHostByNameDone),
null);
}
void GetHostByNameDone(IAsyncResult ar)
{
if(cancel)
{
OnCanceled(EventArgs.Empty);
Stop();
}
try
{
IPHostEntry ihe = Dns.EndGetHostByName(ar);
IPAddress address = ihe.AddressList[0];
IPEndPoint trackerEndpoint = new IPEndPoint(address, uri.Port);
socket.BeginConnect(trackerEndpoint,
new AsyncCallback(ConnectDone), null);
}
catch(Exception e)
{
Fail(e);
}
}
. . . . .
void Complete()
{
OnCompleted(EventArgs.Empty);
Stop();
}
void Fail(Exception e)
{
OnFailed(new ExceptionEventArgs(e));
Stop();
}
void Stop()
{
if(null != socket)
{
socket.Close();
socket = null;
}
}
public bool Cancel()
{
cancel = true;
return true;
}
. . . . .
}
I do not use threads here, only asynchronous I/O. The standard event system is used to notify the caller. The only peculiar detail is the ConclusionThe asynchronous design pattern provided by Microsoft is powerful, elegant and simple to implement. However, it is not universal and sometimes you need to rely on other means like a bit more general asynchronous operation pattern. LicenseAll the code in Compiling sample applicationsAs usual, I provide a SharpDevelop 1.x combine and a NAnt 0.85.x build file. The project consists of three console applications: AsyncMethod, AsyncHttpGet1 and AsyncHttpGet2 and of a library Shared that contains common code. SharpDevelop combine has four subprojects, all of them define two build targets, Debug and Release. For Debug targets I define following symbols: DEBUG, TRACE and NET11. For Release targets I define symbols: TRACE and NET11. If you change NET11 to NET20 some code becomes compatible with .Net 2.x features (mostly static classes). Also for AsyncHttpGet1 subproject you can add either POLL or CALLBACK symbols or it won't compile. NAnt build files are bit simpler. Each subproject is in in it's respective folder and has it's own build file. The main build file calls each subproject build file for the specific target. The main file also specifies the third target, Clean that removes all intermediate and target files. To all VS users - I'm sorry, no solution file so far, I have no VS installed around. But I hope you can reconstruct the solution with my description of the sample project. History
Links
|
|||||||||||||||||||||||||||||||||||||||||||||||