As I mentioned the other day, I can’t quite let go of the concurrency bug, so the 2nd of my 3 add-on posts to the concurrency series.
One thing I didn’t talk about explicitly in the original concurrency series is the Win32 API set.
I implicated the concurrency issues when I mentioned that all DLLs need to be multi-thread safe, and the Win32 API set is no exception, but that really doesn’t express the full state of the concurrency “issues”` w.r.t. the Win32 APIs.
In general, you can assume that all the Win32 APIs can be called from separate threads (please note, I’m NOT including the “User”, “GDI” or “Shell” APIs in this list – I’m just talking about the functions in Kernel32.dll and Advapi32.dll – the “Windows” APIs have different threading requirements that I’ll talk about tomorrow).
But each family of APIs handle concurrency slightly differently (the lack of consistancy is simply a reflection of the underlying architecture of each API).
For instance, the file I/O APIs handle concurrency issues by simply ignoring concurrency issues. There are internal locks to protect the data structures but if you attempt to perform multiple I/Os on an ordinary file handle, the results are “undefined” – all bets are off. You CAN use file handles from multiple threads simultaneously, but the system makes no guarantees of working correctly. If you do want to do I/O on multiple threads, you need to tell the I/O manager that you’re going to do that by specifying the FILE_FLAG_OVERLAPPED flag in the CreateFile call. When you make this call, you’re telling the I/O system that you’re prepared to deal with the consequences of accessing files from multiple threads. For example, when you specify overlapped I/O, the SetFilePointer API ceases to function in a reasonable fashion – the API won’t fail, but it also won’t necessarily set the current file offset – that’s because files opened for overlapped I/O effectively don’t have a current file offset, you need to specify the file offset on each read or write operation.
Another example is the NT heap (which I mentioned before). The NT heap manager (by default) protects its data structures by acquiring a lock across all heap accesses. The only option you have for modifying the heap serialization behavior is to set the HEAP_NO_SERIALIZE flag, which tells the heap manager that you’re prepared to own the serialization for that particular heap. In that situation, the heap manager disables its internal locks and hands the responsibility for concurrency to your application.
The Windows MME APIs (waveOutXxx, waveInXxx, mixerXxx, etc) also protect their internal data structures. But they also support callback functions, and those callback functions are called with internal locks held (this has to happen to protect the integrity of the internal data structures in the winmm DLL). As a result, there are some huge caveats on what you can do in the callback function (waveOutProc). For the MME APIs, the reality is that the waveOutProc is (IMHO) sufficiently limited that the other callback options (CALLBACK_EVENT, CALLBACK_THREAD, or CALLBACK_WINDOW) are vastly better – they allow you to control the context in which the APIs called.
The good news is that MSDN does a pretty good job of laying out the concurrency issues associated with the particular APIs. For example, the documentation for StartService calls out the fact that there’s a lock held by the service control manager during service startup, and thus that a service cannot start another service in its startup routine before it’s reported that it’s started to the service controller.
For COM, the concurrency contract for the COM objects is explicitly called out in the objects threading model. I’ve written about this here, but here’s a really quick summary: Apartment threaded objects can only be called from the thread which created the object, “free” threaded objects can be called from any thread (and thus internally deal with concurrency issues), and “both” objects can be called from either apartment model threads.