2009-07-24 »
Asynchronizing the Synchronous
We've been working on some further speed optimizations for our EQL Data plugin for Microsoft Access. This involves rewriting some parts (which were previously in VBA) into C++.
Now, to do this, I decided it would be nicest if we could write a simple command-line tool first, and then afterwards, adapt it into a GUI plugin. That makes testing easier and makes the code nice and linear, at least at first. So that's what I did.
The next steps was to convert it to something more appropriate for a GUI. What's the #1 concern of GUI code? It has to be asynchronous: you're not allowed to block for too long, or else the system seems unresponsive. Usually this means either a) using threads, or b) rewriting your program to use a state machine so you can give up your time slice occasionally, or more realistically c) both, because using a thread gives the best interactive performance but you still have to be able to abort them safely.
So I figured we'd go with (b) and then (c). Then, I thought, do I really have to actually change the code at all? I should be able to make my synchronous function effectively asynchronous without any changes, using a trick like this:
#include "wvcont.h"
typedef void LogFunc(const char *msg);
static void *long_running_function(LogFunc *log)
{
char msg[100];
for (int i = 1; i <= 5; i++)
{
sprintf(msg, "Hello world! %d\n", i);
log(msg);
}
return (void *)1;
}
static void do_log(const char *msg)
{
fprintf(stderr, "%s", msg);
WvCont::yield(0);
}
int main()
{
WvCont c(wv::bind(long_running_function, do_log));
while (!c())
fprintf(stderr, "If you see this, yielding worked.\n");
return 0;
}
Now, using this trick here isn't really necessary; I have total control over long_running_func() and I could just break it into more pieces, or make a class, or whatever. But the result is actually pretty elegant.
Not always a waste of time
In fact, however, when I first used this trick, it was the only way to get the results I wanted. That was a few years ago, when I was working on the Nitix ExchangeIt plugin for Outlook. (I can't link to that, because it effectively no longer exists now that IBM bought my company. If you google for ExchangeIt, you'll mostly find that the name has been recycled by other people. The one on Google Code is definitely not it.)
The problem we ran into in ExchangeIt was with the ITnef::ExtractProps function (if I remember right). This function is kind of neat: you give it an IStream (an object implementing a simple streaming read interface) and it automatically reads the stream, decodes the results, turns them into a MAPI message, and returns when it's done.
Great, right? You just wrap whatever input stream you have (in our case, it was an auto-zlib-decompressing, SSL-enabled TCP socket) in an IStream, pass the IStream to ExtractProps, and off it goes ... except it's synchronous. While you wait for ExtractProps to finish, your thread is stuck. And because it's a network connection, it might be stuck for a long time.
To resolve the problem, we used the same trick as above: our IStream implementation actually called WvCont::yield() whenever ExtractProps asked it to Read(). The program's mainloop, then, could resume the WvCont only when data was ready on the stream; the rest of the time, the MAPI function was just waiting patiently to be resumed.
All this with no threads, and thus no locking, no race conditions, and no kernel-level task switching overhead.
(WvCont is part of the WvStreams library that I helped to write. Apologies go to mag and drheld, who tried to use a predecessor to WvCont when writing WvFuse/FunFS, before I had managed to make it awesome.)
::nv
Why would you follow me on twitter? Use RSS.