.net - IPC - "queue" the oldest process' work to fire first

167 views Asked by At

I have a .Net 2.0 application that processes data, generates Crystal Reports, and then sends the rendered output to a printer. This application is most-of-the-time fired from a Win32 application multiple times. The time it takes to actually send the rendered report to the printer depends on the size of the data and/or the complexity of the Crystal Report.
My quandary is that I need to have the print jobs queued in the same order as the the process, regardless of whether or not the report is ready to be printed. So, for instance:

    myapp.exe fired (process 1) - begins chewing on data (large dataset)  
    myapp.exe fired again (process 2) - chews on data  
      process 2 done chewing on data (small report) - sends report to printer  
    myapp.exe fired a third time (process 3) - chews on its data  
      process 3 done (also small) - sends report to printer  
      process 1 is finally done (slacker!) - sends report to printer

In the above example, I need process 1 to print 1st, then process 2, then process 3. However, only the printing of the report needs to happen in sequence - all the "chewing" can be done simultaneously...

I've been toying with Mutex and Semaphore, and I've got it to a point in a test app where the first process will "print" first, but the second and third (fourth, fifth, etc) will "print" depending on when their "WaitOne" was issued.

Am I going the wrong route here?
I think what i need is some kind of mechanism like a "Queued WaitHandle" for IPC...

1

There are 1 answers

4
Jim Mischel On

If the number of processes is relatively small, you could create a named EventWaitHandle for each one, and the order in which the wait handles are created would define the order in which processes are allowed to print. (See the "Added later" section below, which describes a race condition in the presented code and discusses a fix.)

When a process starts up, it tries to create a named event with the name Process0. If it succeeds, it goes on. If it fails, it tries to create a named event Process1, then Process2, etc. You end up with one named event for every process that starts up. There is an EventWaitHandle constructor that will tell you if the handle was created new. So the code looks something like:

List<EventWaitHandle> WaitHandles = new List<EventWaitHandle>();

// at program startup, try to create Process0, with an initial value of set
bool createdNew;
EventWaitHandle wh = new EventWaitHandle(true, EventResetMode.ManualReset, "Process0", out createdNew);
WaitHandles.Add(wh);
int procNum = 1;
while (!createdNew)
{
    // Create wait handles with increasing process numbers until one is created new
    string whName = "Process" + procNum.ToString();
    wh = new EventWaitHandle(false, EventResetMode.ManualReset, whName, out createdNew);
    WaitHandles.Add(wh);
    ++procNum;
}

A process now knows which wait handle it needs to wait on--the last one in the WaitHandles list. So when it's done with its "chewing" phase, it can wait on that handle:

WaitHandles[WaitHandles.Count - 1].WaitOne();

// print

// Now notify the next wait handle, but only if it exists.
try
{
    string whName = "Process" + WaitHandles.Count.ToString();
    WaitHandle wh = EventWaitHandle.OpenExisting(whName);
    wh.Set();
    wh.Dispose();
}
catch (WaitHandleCannotBeOpenedException)
{
    // wait handle doesn't exist
}

You'll probably want to be sure to call Dispose on each item in the WaitHandles list before your program exits, although Windows will clean up your references if you forget.

This should work if you don't have too many processes going. As long as any one of these processes is running, the "Process0" and all other named wait handles up to the last one will remain in the system. So if "Process0" finishes printing before "Process9" even starts, there won't be a problem with a job printing out of order or handing forever waiting for notification.

This method also makes it possible to write an application that can start up and get all of the handles along with their signaled state. Such an application could be used to signal the next process in line if one of the previous processes crashed without signaling.

Added later:

It occurs to me that there's a possible race condition here. Say there are three wait handles ("Process0", "Process1", and "Process2"). Process2 finishes its printing and attempts to notify Process3, which doesn't exist. Then Process3 starts up before Process2 exits, meaning that Process3 will get a wait handle named "Process3" that will never be signaled.

The way around this would be to have Process0 allocate its own wait handle and also a wait handle for Process1 -- that is, the wait handle that it will have to notify when it's done printing. All other processes do the same thing. Rather than allocating their own wait handle (except for Process0, which allocates its own wait handle AND the next one), they allocate the wait handle for the next process. Doing it this way should eliminate the race condition.