While Hurricane Sandy hit the East Coast and gave me a full week of vacation without electricity and internet, I gathered enough motivation to read through the very good book – concurrent programming on Windows by Joe Duffy. So here, I came across the Asynchronous Procedure Calls and what they mean. So this post is just a journal about me trying to do something clever (actually stupid) and sharing my madness with others.
From what I learnt, an asynchronous procedure call (APC) is just a procedure that you can queue and request the OS that this procedure be performed on a particular thread. But that particular thread may be executing something right? So the question is when will the APC be executed? The answer for that is “it will be executed on the thread when it enters an ‘alertable wait’ state”. So what is an alertable wait?
Consider the code below.
void IExecuteOnAThread(){
lock(syncLock){
while(conditionIsNotMet)
Monitor.Wait(syncLock);
}
}
In the above snippet, the thread is actually waiting on the syncLock. When some other thread that makes the conditionIsNotMet as false and does a Pulse() on the syncLock, this current thread shall wake up and proceed with its execution.
Now back to “alertable wait” –> If a thread enters an alertable wait, then it can wake up properly (like in the case above) or can wake up for some other reason. This is how APC gets executed. When an APC gets queued on a thread, the OS will execute this APC the moment it finds that thread in an “alertable wait”.
So while I think about this concept, it stuck to me (given that I am not that smart) to run a simple experiment. Usually, we queue work items into ThreadPool to get executed. Now the execution can happen in parallel so there is no order guarantee. In some cases, I would like to execute some asynchronous operations “in-order”; as queued; in the background (for whatever reason). In these cases, we typically use some kind of “executor” classes where a queue is almost always involved and some thread/threads wait for the work to be queued and execute them one by one – classic Producer/Consumer style.
So what I wanted to try was to take advantage of APC and get away from not having to write any code to implement executors.
Note: For those who cannot wait till the end, do not do this – it is not worth it. I am just doing it because Sandy hit my head pretty hard.
public class StupidExecutor
{
delegate void ApcProc(UIntPtr dwParam);
[DllImport("kernel32.dll")]
static extern uint QueueUserAPC(ApcProc pfnApc, IntPtr hThread, UIntPtr dwData);
[DllImport("kernel32.dll")]
static extern IntPtr GetCurrentThread();
private readonly AutoResetEvent _waiter = new AutoResetEvent(false);
private IntPtr _threadId = IntPtr.Zero;
public StupidExecutor()
{
new Thread(WaitForApc).Start(null);
}
public void Stop()
{
_waiter.WaitOne();
}
private void WaitForApc(object none)
{
_threadId = GetCurrentThread();
_waiter.WaitOne();
}
public void QueueWork(Action action)
{
//while (Thread.VolatileRead(ref _threadId) == IntPtr.Zero)
// Thread.Sleep(0);
var localAction = action;
ApcProc apcProc = ((z) => localAction());
QueueUserAPC(apcProc, _threadId, UIntPtr.Zero);
}
}
When the StupidExecutor gets created, a thread is launched which when started waits for a signal on a Auto Reset Event. This will make the thread wait because it is non-signalled and will remain so until Stop is executed on the executor. The executor has one method “QueueWork” which takes a delegate.
So the usage of the stupid executor is as shown:
static void Main(string[] args)
{
var executor = new StupidExecutor();
double[] avg = {0.0};
int size = 1000;
for (int j = 0; j < size; j++)
{
var queued = DateTime.UtcNow;
executor.QueueWork(() => avg[0] += (DateTime.UtcNow - queued).TotalMilliseconds);
}
Console.ReadLine();
Console.WriteLine("Average Latency : "+avg[0]/size);
}
So the intention was to measure the latency (again not really the most efficient way). And I see an average latency of around 2 ms. So this must be a great way! Not really! First of all, for a lot of reasons like state corruption among other things this should not be done. Moreover, you are passing managed delegates to native code. So GC can always kick in. So to prove it, I am changing the sample size from 1000 to 100000.
Firstly, it wouldn’t work and you will be greeted with a nice dialog.
So run it in debug mode and you will get a nice little MDA called “CallbackOnCollectedDelegate" shall be raised. This simply says that the delegate you passed to native code is GC’d already.
Or even this:
So the experiment which was doomed to fail did fail. Long story short – don’t mess with your threads.