To Overlap or Not to Overlap
It is tempting to overlap all external calls, but there are at least five reasons why one shouldn't (or can't) do this:
Overlapped Call Overhead
Overlapped calls do have some overhead relative to standard external call. This is because of context switch time; at least two thread context switches must be made for a threaded call to complete, typically four. These are necessary for the call to be initiated, and then for notification when the call completes and completion of the call in synchronization with the main VM thread. Bearing this in mind, the overhead is surprisingly low, with the simplest overlapped calls having only about 40% overhead. This is not likely to be an issue for longer running calls where the overhead of the call itself is insignificant in relation to the duration of the call, but would be for calls such as the CRT sin() function.
When the first overlapped call is made from a Smalltalk Process object there is significant additional overhead, because it requires allocation and initialisation of an operating system thread. This should only be an issue if your application starts a lot of short lived Processes that only perform one or two overlapped calls.
C++ virtual function calls (the means by which COM call-outs are implemented) cannot be overlapped. This may change in a future release, but currently the compiler does not allow both 'virtual' and 'overlap' attributes to be applied to the same function call, and there is no primitive support for overlapped virtual calls either.
"Idle Panic" is that state the VM enters when it finds that it has no processes ready to run. Under normal conditions the Idle process is always ready to run, and prevents the VM panicking about what to do next by putting the VM thread to sleep (it calls one of the MsgWaitForMultipleObjects() family of functions, depending on the host OS). If any of the external calls the idle process makes are modified to be overlapped, or the idle loop itself is modified to perform other overlapped calls, then the idle Process will be blocked, and enter a wait state. At this point if no other process is ready the VM will send an "idle panic" interrupt into the image, the default handling of which (see InputState>>idlePanic) is to terminate the existing idle process and start a new one. If the new idle process persists in making overlapped calls, then the system will start to thrash, continuously starting and terminating idle processes.
This situation can easily be avoided by not modifying the idle loop, and by not marking any of the functions called by that loop as overlapped.
Some functions generate immediate callbacks into the image, either explicitly as External Callbacks, or implicitly through the Windows message queue (e.g. DestroyWindow). The latter are always synchronised and directed to the VMs main thread by Windows, but the former may generated callbacks on other threads. The VM intercepts these callbacks and has to redirect them to the main VM thread so that they can be processed safely in the same manner as Windows, i.e. by synchronising them through the message queue. In order that such callbacks can be processed, the message queue has to be serviced, which requires that the main UI process return to the message loop. It should be apparent that this will cause a deadlock in the case of such synchronous callbacks, because main UI process is blocked waiting for the original function to return, and therefore cannot service the message queue.
By way of example, if we mark GDILibrary>>enumFonts: lpFaceName: lpFontFunc:lParam: as overlapped, and evaluate an expression such as:
Canvas forDesktop fonts: 'Arial' do: [
:lplf :lptm :dwType| Transcript print: lplf; cr]
Then Dolphin will simply lock up, although it can be broken into with Ctrl+Break.
To avoid this problem, do not overlap calls that generate synchronous callbacks.
Smalltalk was originally designed to be a co-operative multithreaded system, rather than a pre-emptive multi-threading system. Smalltalk Processes can be pre-empted by higher priority processes, but not by Processes at the same or lower priority, no matter how long they run without yielding. This assumption permeates code in certain Smalltalk libraries, although we have addressed this in the Dolphin class library and indeed the Dolphin VM does multi-task more aggressively than normal Smalltalk VMs. In particular it may switch between processes at the same priority when any process synchronisation primitives is executed, regardless of whether that primitive would block the active Process.
The only code in the Dolphin library that expects to run to completion to the exclusion of all other processes is the startup code, which is run at a high priority. This special case is necessary allow the system to reach a stable state before normal application processing begins. If the startup code is interrupted, then the system might not be in a stable state when other processes start to run.
Recall that issuing an overlapped call automatically blocks the calling process and allows other processes to run. Consequently one must not issue any overlapped external calls from SessionManager startup code, i.e. not in any of the primary, secondary or tertiary startup phases (see the 'operations-startup' category of methods under SessionManager). Once the #main method has been entered, it is safe to use overlapped calls.
It should be noted that normal MessageBoxes are created by an overlapped call, and so if these must be used during startup then either delay the MessageBox's creation until #main is activated, or mark the MessageBox as #taskModal (in which case it will be opened using a normal, non-overlapped, external call).
If overlapped calls are inadvertently used during session startup, then one's deployed applications may exhibit any of the following symptoms:
|•||Errors due to uninitialized subsystems|
|•||Startup code running twice|
|•||Applications windows failing to open, or conversely multiple copies being opened.|
|•||Failure to shut down.|
The easiest way to avoid all these problems is to do all application specific session startup, for example opening the main application window, in the #main method.
Session startup processing is a special case, and one should try and avoid creating other cases where blocking a process due to issuing an overlapped external call will result in instability or incorrect operation. In other words don't ever assume that non-interruptible behaviour can be guaranteed, even by boosting process priority or turning off interrupts, but instead use process synchronisation objects such as Mutex or Semaphore to protect critical sections.