I am wondering if it is possible to fall victim to issues around the management of managed threads in the native world when you marshal a callback delegate to a DLL through
Yes, it is possible to fall victim to these issues. In this particular case it is difficult. The host can't switch the managed thread to a different OS thread while a native frame is on the managed thread's stack, and since you immediately p/invoke SleepEx, the window for the host to switch the managed thread is between the two p/invoke calls. Still, it is sometimes a disagreeable possibility when you need to p/invoke on the same OS thread and Thread.BeginThreadAffinity()
exists to cover this scenario.
Now for the APC question. Remember that the OS knows nothing about managed threads. So the APC will be delivered into the original native thread when it does something alertable. I don't know how the CLR host creates managed contexts in these cases, but if managed threads are one-to-one with OS threads the callback will probably use the managed thread's context.
UPDATE Your new code is much safer now, but you went a bit too far in the other direction:
1) There is no need to wrap the whole code with thread affinity. Wrap just the two p/invokes that do need to run on the same OS thread (Notify and Sleep). It does not matter whether you use a finite timeout, because the problem you're solving with thread affinity is a managed-to-OS thread migration between the two p/invokes. The callback should not assume it is running on any particular managed thread anyway, because there is little it can safely do, and little it should do: interlocked operations, setting events and completing TaskCompletionSources
is about it.
2) GCHandle
is a simple, IntPtr
-sized struct, and can be compared for equality. Instead of using GCHandle?
, use plain GCHandle
and compare to default(GCHandle)
. Besides, GCHandle?
looks fishy to me on general principles.
3) Notification stops when you close the service handle. The SCM handle can stay open, you might want to keep it around for the next check.
// Thread.BeginThreadAffinity();
// GCHandle? notifyHandle = null;
var hSCM = OpenSCManager(null, null, (uint)0xF003F);
if (hSCM != IntPtr.Zero)
{
StatusChangedCallbackDelegate changeDelegate = ReceivedStatusChangedEvent;
var notifyHandle = default(GCHandle);
var hService = OpenService(hSCM, serviceName, (uint)0xF003F);
if (hService != IntPtr.Zero)
{
...
notifyHandle = GCHandle.Alloc(notify, GCHandleType.Pinned);
var addr = notifyHandle.AddrOfPinnedObject();
Thread.BeginThreadAffinity();
NotifyServiceStatusChange(hService, (uint)0x00000001, addr);
SleepEx(timeout, true);
Thread.EndThreadAffinity();
CloseServiceHandle(hService);
}
GC.KeepAlive(changeDelegate);
if (notifyHandle != default(GCHandle))
notifyHandle.Free();
CloseServiceHandle(hSCM);
}
Also, to be as safe as possible, if your code is going to run for a long time, or if you're writing a library, you must use constrained regions and/or SafeHandles to ensure your cleanup routines run even if the thread is aborted. Look at all the hoops BCL code jumps through in, e.g., System.Threading.Mutex (use Reflector or the CLR source). At the very least, use SafeHandles and try/finally to free the GCHandle and end thread affinity.
As for callback-related problems, these are just a bad case of normal multi-threading sort of problems: deadlocks, livelocks, priority inversion etc. The worst thing about this sort of APC callback is that (unless you block the whole thread yourself until it happens, in which case it's easier just to block in native code) you don't control when it happens: your thread might be deep inside BCL waiting for I/O, for an event to be signaled, etc., and it is very difficult to reason about the state the program might be in.