3

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 P/Invoke in my particular case below (see example code).

This MSDN article on Managed and Unmanaged Threading in Windows states:

"An operating-system ThreadId has no fixed relationship to a managed thread, because an unmanaged host can control the relationship between managed and unmanaged threads. Specifically, a sophisticated host can use the Fiber API to schedule many managed threads against the same operating system thread, or to move a managed thread among different operating system threads."

First of all, who or what is the unmanaged host this article describes? If you use marshaling like in the example code I give below, then who or what is the unmanaged host there?

Also, this StackOverflow question's accepted answer states:

"It's perfectly legal from a CLR perspective for a single managed thread to be backed by several different native threads during it's lifetime. This means the result of GetCurrentThreadId can (and will) change throughout the course of a thread's lifetime."

So, does this mean my APC will be queued in a native thread, or delegated directly to my managed thread because of the marshaling layer?

Here is the example. Let's say I use the following class to P/Invoke the NotifyServiceStatusChange function in managed code to check when a service is stopped:

using System;
using System.Runtime.InteropServices;

namespace ServiceStatusChecking
{
    class QueryServiceStatus
    {

        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public class SERVICE_NOTIFY
        {
            public uint dwVersion;
            public IntPtr pfnNotifyCallback;
            public IntPtr pContext;
            public uint dwNotificationStatus;
            public SERVICE_STATUS_PROCESS ServiceStatus;
            public uint dwNotificationTriggered;
            public IntPtr pszServiceNames;
        };

        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct SERVICE_STATUS_PROCESS
        {
            public uint dwServiceType;
            public uint dwCurrentState;
            public uint dwControlsAccepted;
            public uint dwWin32ExitCode;
            public uint dwServiceSpecificExitCode;
            public uint dwCheckPoint;
            public uint dwWaitHint;
            public uint dwProcessId;
            public uint dwServiceFlags;
        };

        [DllImport("advapi32.dll")]
        static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);

        [DllImport("advapi32.dll")]
        static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);

        [DllImport("advapi32.dll")]
        static extern uint NotifyServiceStatusChange(IntPtr hService, uint dwNotifyMask, IntPtr pNotifyBuffer);

        [DllImportAttribute("kernel32.dll")]
        static extern uint SleepEx(uint dwMilliseconds, bool bAlertable);

        [DllImport("advapi32.dll")]
        static extern bool CloseServiceHandle(IntPtr hSCObject);

        delegate void StatusChangedCallbackDelegate(IntPtr parameter);

        /// <summary> 
        /// Block until a service stops or is found to be already dead.
        /// </summary> 
        /// <param name="serviceName">The name of the service you would like to wait for.</param>
        public static void WaitForServiceToStop(string serviceName)
        {
            IntPtr hSCM = OpenSCManager(null, null, (uint)0xF003F);
            if (hSCM != IntPtr.Zero)
            {
                IntPtr hService = OpenService(hSCM, serviceName, (uint)0xF003F);
                if (hService != IntPtr.Zero)
                {
                    StatusChangedCallbackDelegate changeDelegate = ReceivedStatusChangedEvent;
                    SERVICE_NOTIFY notify = new SERVICE_NOTIFY();
                    notify.dwVersion = 2;
                    notify.pfnNotifyCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);
                    notify.ServiceStatus = new SERVICE_STATUS_PROCESS();
                    GCHandle notifyHandle = GCHandle.Alloc(notify, GCHandleType.Pinned);
                    IntPtr pinnedNotifyStructure = notifyHandle.AddrOfPinnedObject();
                    NotifyServiceStatusChange(hService, (uint)0x00000001, pinnedNotifyStructure);
                    SleepEx(uint.MaxValue, true);
                    notifyHandle.Free();
                    CloseServiceHandle(hService);
                }
                CloseServiceHandle(hSCM);
            }
        }

        public static void ReceivedStatusChangedEvent(IntPtr parameter)
        {

        }
    }
}

Is the APC queued onto whichever native thread was hosting my managed thread, or is the APC delegated directly to my managed thread? I thought the delegate was there to handle exactly this case, so that we don't need to worry about how managed threads are handled natively, but I could be wrong!

Edit: I guess this is a more agreeable answer.

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace ServiceAssistant
{
    class ServiceHelper
    {

        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public class SERVICE_NOTIFY
        {
            public uint dwVersion;
            public IntPtr pfnNotifyCallback;
            public IntPtr pContext;
            public uint dwNotificationStatus;
            public SERVICE_STATUS_PROCESS ServiceStatus;
            public uint dwNotificationTriggered;
            public IntPtr pszServiceNames;
        };

        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct SERVICE_STATUS_PROCESS
        {
            public uint dwServiceType;
            public uint dwCurrentState;
            public uint dwControlsAccepted;
            public uint dwWin32ExitCode;
            public uint dwServiceSpecificExitCode;
            public uint dwCheckPoint;
            public uint dwWaitHint;
            public uint dwProcessId;
            public uint dwServiceFlags;
        };

        [DllImport("advapi32.dll")]
        static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);

        [DllImport("advapi32.dll")]
        static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);

        [DllImport("advapi32.dll")]
        static extern uint NotifyServiceStatusChange(IntPtr hService, uint dwNotifyMask, IntPtr pNotifyBuffer);

        [DllImportAttribute("kernel32.dll")]
        static extern uint SleepEx(uint dwMilliseconds, bool bAlertable);

        [DllImport("advapi32.dll")]
        static extern bool CloseServiceHandle(IntPtr hSCObject);

        delegate void StatusChangedCallbackDelegate(IntPtr parameter);

        /// <summary> 
        /// Block until a service stops or is found to be already dead.
        /// </summary> 
        /// <param name="serviceName">The name of the service you would like to wait for.</param>
        /// <param name="timeout">An amount of time you would like to wait for. uint.MaxValue is the default, and it will force this thread to wait indefinitely.</param>
        public static void WaitForServiceToStop(string serviceName, uint timeout = uint.MaxValue)
        {
            Thread.BeginThreadAffinity();
            GCHandle? notifyHandle = null;
            StatusChangedCallbackDelegate changeDelegate = ReceivedStatusChangedEvent;
            IntPtr hSCM = OpenSCManager(null, null, (uint)0xF003F);
            if (hSCM != IntPtr.Zero)
            {
                IntPtr hService = OpenService(hSCM, serviceName, (uint)0xF003F);
                if (hService != IntPtr.Zero)
                {
                    SERVICE_NOTIFY notify = new SERVICE_NOTIFY();
                    notify.dwVersion = 2;
                    notify.pfnNotifyCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);
                    notify.ServiceStatus = new SERVICE_STATUS_PROCESS();
                    notifyHandle = GCHandle.Alloc(notify, GCHandleType.Pinned);
                    IntPtr pinnedNotifyStructure = ((GCHandle)notifyHandle).AddrOfPinnedObject();
                    NotifyServiceStatusChange(hService, (uint)0x00000001, pinnedNotifyStructure);
                    SleepEx(timeout, true);
                    CloseServiceHandle(hService);
                }
                CloseServiceHandle(hSCM);
            }
            GC.KeepAlive(changeDelegate);
            if (notifyHandle != null)
            {
                ((GCHandle)notifyHandle).Free();
            }
            Thread.EndThreadAffinity();
        }

        public static void ReceivedStatusChangedEvent(IntPtr parameter)
        {

        }
    }
}

Edit again! I guess THIS is an even MORE agreeable answer:

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace ServiceAssistant
{
    class ServiceHelper
    {

        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public class SERVICE_NOTIFY
        {
            public uint dwVersion;
            public IntPtr pfnNotifyCallback;
            public IntPtr pContext;
            public uint dwNotificationStatus;
            public SERVICE_STATUS_PROCESS ServiceStatus;
            public uint dwNotificationTriggered;
            public IntPtr pszServiceNames;
        };

        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct SERVICE_STATUS_PROCESS
        {
            public uint dwServiceType;
            public uint dwCurrentState;
            public uint dwControlsAccepted;
            public uint dwWin32ExitCode;
            public uint dwServiceSpecificExitCode;
            public uint dwCheckPoint;
            public uint dwWaitHint;
            public uint dwProcessId;
            public uint dwServiceFlags;
        };

        [DllImport("advapi32.dll")]
        static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);

        [DllImport("advapi32.dll")]
        static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);

        [DllImport("advapi32.dll")]
        static extern uint NotifyServiceStatusChange(IntPtr hService, uint dwNotifyMask, IntPtr pNotifyBuffer);

        [DllImportAttribute("kernel32.dll")]
        static extern uint SleepEx(uint dwMilliseconds, bool bAlertable);

        [DllImport("advapi32.dll")]
        static extern bool CloseServiceHandle(IntPtr hSCObject);

        delegate void StatusChangedCallbackDelegate(IntPtr parameter);

        /// <summary> 
        /// Block until a service stops, is killed, or is found to be already dead.
        /// </summary> 
        /// <param name="serviceName">The name of the service you would like to wait for.</param>
        /// <param name="timeout">An amount of time you would like to wait for. uint.MaxValue is the default, and it will force this thread to wait indefinitely.</param>
        public static void WaitForServiceToStop(string serviceName, uint timeout = uint.MaxValue)
        {
            // Ensure that this thread's identity is mapped, 1-to-1, with a native OS thread.
            Thread.BeginThreadAffinity();
            GCHandle notifyHandle = default(GCHandle);
            StatusChangedCallbackDelegate changeDelegate = ReceivedStatusChangedEvent;
            IntPtr hSCM = IntPtr.Zero;
            IntPtr hService = IntPtr.Zero;
            try
            {
                hSCM = OpenSCManager(null, null, (uint)0xF003F);
                if (hSCM != IntPtr.Zero)
                {
                    hService = OpenService(hSCM, serviceName, (uint)0xF003F);
                    if (hService != IntPtr.Zero)
                    {
                        SERVICE_NOTIFY notify = new SERVICE_NOTIFY();
                        notify.dwVersion = 2;
                        notify.pfnNotifyCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);
                        notify.ServiceStatus = new SERVICE_STATUS_PROCESS();
                        notifyHandle = GCHandle.Alloc(notify, GCHandleType.Pinned);
                        IntPtr pinnedNotifyStructure = notifyHandle.AddrOfPinnedObject();
                        NotifyServiceStatusChange(hService, (uint)0x00000001, pinnedNotifyStructure);
                        SleepEx(timeout, true);
                    }
                }
            }
            finally
            {
                // Clean up at the end of our operation, or if this thread is aborted.
                if (hService != IntPtr.Zero)
                {
                    CloseServiceHandle(hService);
                }
                if (hSCM != IntPtr.Zero)
                {
                    CloseServiceHandle(hSCM);
                }
                GC.KeepAlive(changeDelegate);
                if (notifyHandle != default(GCHandle))
                {
                    notifyHandle.Free();
                }
                Thread.EndThreadAffinity();
            }
        }

        public static void ReceivedStatusChangedEvent(IntPtr parameter)
        {

        }
    }
}
Community
  • 1
  • 1
Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • This can't work robustly from .NET - you need to implement that part in a native DLL which runs its own native thread and do some IPC between that DLL and your .NET code... – Yahia Nov 20 '13 at 18:29
  • 1
    Why not? CLR does this kind of thing all the time. The callback just must be prepared to run in a different managed thread. – Anton Tykhyy Nov 20 '13 at 18:31
  • @Alexandru BTW your code is buggy: 1) you must not free `notifyHandle` until you have stopped the notification with `CloseServiceHandle`. 2) The delegate can be garbage-collected while you are sleeping. You need to call `GC.KeepAlive (changeDelegate)` after you stop the notification. Also, be more careful about leaking `GCHandle`s in exceptional circumstances. – Anton Tykhyy Nov 20 '13 at 18:35
  • 2
    This came about because of a SQL Server project at .NET 2.0. It was an abysmal failure and this wasn't tried again by anybody. A .NET thread always matches an operating system thread. You did rewrite ServiceController.WaitForStatus(), don't do that. – Hans Passant Nov 20 '13 at 18:35
  • @HansPassant Are you saying there is a 1-to-1 mapping of managed to unmanaged threads? – Alexandru Nov 20 '13 at 18:37
  • @HansPassant nice to know that! Could you also throw a light on where does the managed context come from when an APC calls a managed delegate? – Anton Tykhyy Nov 20 '13 at 18:39
  • @AntonTykhyy 1. You might be right, I need to check. 2. The struct is already pinned. The delegate is pointed to as part of the struct. Its reference should already stick around because of this. I've done extensive testing to force GC.Collect() between each line in the above code. This question actually came up because of the answer I posted on my other question, http://stackoverflow.com/questions/20061459/why-wont-my-solution-work-to-p-invoke-notifyservicestatuschange-in-c/20071707#comment29949602_20071707 – Alexandru Nov 20 '13 at 18:40
  • @Alexandru 2. No. The IntPtr you get from GetFunctionPointerForDelegate is not a GC root, MSDN on this method says so explicitly. You might just have been lucky, or you tested in debug mode where GC does not collect references until they go out of scope. Have you tested in release mode? – Anton Tykhyy Nov 20 '13 at 18:45
  • @AntonTykhyy Yup. Works in release. – Alexandru Nov 20 '13 at 18:48
  • 1
    Then you're just lucky. I got bitten in production in similar circumstances. Put in GC.KeepAlive, it doesn't cost you anything. **UPD** Actually, I know why it works for you. It's because your callback method is static. The C# compiler caches delegates created from static methods in a hidden static variable (you can see it yourself in the IL) and that static variable is what keeps your delegate alive. Once you create the delegate from an instance method or manually using `CreateDelegate`, BAM! – Anton Tykhyy Nov 20 '13 at 18:52
  • @AntonTykhyy Did you try this out? – Alexandru Nov 20 '13 at 19:25
  • @AntonTykhyy For point 1, the MSDN on this says, "This structure must remain valid until the callback function is invoked or the calling thread cancels the notification request." Well, I only free it after the callback function is invoked, which will come after the sleep, which is why it works in this case, as well. – Alexandru Nov 20 '13 at 20:11
  • Yes, as I said this thing bit me once in production. Besides, consider it logically: there's no way for the marshaler to know what you are going to use a particular `IntPtr` for. It's just an integer. Maybe it's a `GCHandle.ToIntPtr()`, or a delegate's function pointer, or the address of a pinned object, or a pointer-sized something Win32 gave you that even *you* don't know what it is, like an `LPARAM`. – Anton Tykhyy Nov 20 '13 at 20:13
  • Hm, you're right on the structure. I didn't notice you passed `INFINITE` as timeout. Still, it's good practice to put the call to `.Free()` where it should be if the timeout was finite. If you don't, you are just setting a subtle trap for the person who'll be changing the code to use a finite timeout. – Anton Tykhyy Nov 20 '13 at 20:18
  • @AntonTykhyy For point 2, I gotta try it out for myself then. Its not a trivial thing to create a non-static delegate like this as far as I can tell, and probably for this exact reason. Hey, when you ran this in production, did you create a delegate in such a way as you described, and not in the same way I've created it here? For point 1, I agree with you this might be a subtle trap in that case; great point. – Alexandru Nov 20 '13 at 20:32
  • @HansPassant Hans, why don't you post an answer to this question? You seem to know more on this subject than most of us and your answer would have the potential to help a lot of people, because right now I feel like there's a lot of inconsistencies on this topic and scarce documentation of it. – Alexandru Nov 20 '13 at 20:40
  • Because you're not listening. Use ServiceController.WaitForStatus(). You don't need my help to use it. – Hans Passant Nov 20 '13 at 20:43
  • @AntonTykhyy Help me get Hans in on this. :) – Alexandru Nov 20 '13 at 20:59
  • @Alexandru by all means try it. It's not really a complicated bug to set up, see http://pastebin.com/LKTBYxUq. – Anton Tykhyy Nov 20 '13 at 22:32
  • @AntonTykhyy Also, you said that, "The C# compiler caches delegates created from static methods in a hidden static variable (you can see it yourself in the IL)"...I've never done this sort of thing, you mean to disassemble the code? Do you know of any good tutorials on that? It would help me a lot in my debugging in the future! – Alexandru Nov 21 '13 at 02:27
  • @AntonTykhyy I edited the question with some updated code. Let me know what you think of this update if you could. Hey, and Anton, thanks so much for all of this. Words can't describe my appreciation and gratitude. I didn't know about a lot of this stuff until today. – Alexandru Nov 21 '13 at 02:39
  • @Alexandru No, don't know any tutorials. Grab a copy of the ECMA-335 spec, skim it once, then run ildasm and enjoy. There is also a free IL-level debugger, Dile. FWIW I find I don't often need to go to IL for debugging, unless (a) there is no source code or (b) the code is generated, which is really just a subcase of (a). Still it's good to know how things work under the hood, and I'm glad to find someone willing to learn. You can buy me a beer someday:) – Anton Tykhyy Nov 21 '13 at 08:34
  • @AntonTykhyy Absolutely, you're definitely up for one or more (>= 1) beers. Well deserved, indeed! I will check it out. I've always been interested in computer security, so the way .NET operates under the covers is pretty exciting to me. – Alexandru Nov 21 '13 at 13:55
  • @HansPassant Its not quite the same though, is it? *The WaitForStatus method waits approximately 250 milliseconds between each status check. WaitForStatus cannot detect the case of the observed service changing to the desiredStatus and then immediately to another status in that interval.* Its not instant. Whereas, my method gets notified if a service is stopped, becomes stopped, is killed (the underlying process is terminated), or if the call times out because you can give it a timeout...WaitForStatus just polls under the covers. – Alexandru Nov 21 '13 at 13:59

2 Answers2

3

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.

Anton Tykhyy
  • 19,370
  • 5
  • 54
  • 56
  • I saw you edited this answer, but you kept the first paragraph, which is of interest to me. So if mappings are 1-to-1 as Hans said, it is still possible to fall victim to these issues in my case...or will the *host* just P/Invoke a second time to the same unmanaged thread that is 1-to-1 mapped? – Alexandru Nov 20 '13 at 20:29
  • 1
    If it's 1-to-1, you can't stumble on OS thread affinity. Both calls will happen on the same OS thread. The APC will be delivered to the OS thread which called `NotifyServiceStatusChange`, and so the delegate will be called on the same managed thread. Again, to avoid setting traps I'd spell this out with a pair of calls to `Begin/EndThreadAffinity`. There remain problems, however, as somebody's already said, because the delegate might get invoked while the managed thread is doing something tricky, holding locks etc. – Anton Tykhyy Nov 20 '13 at 22:16
  • I really love this stuff. Could you explain the remaining problems you mention? I get most of what you're saying but I'm just trying to think of a case where this would be a problem. So, my managed thread has a lock, calls WaitForServiceToStop, it sleeps, gets signaled, returns...what happens to the lock? – Alexandru Nov 21 '13 at 02:03
  • Hey, how does GC.KeepAlive() work if, say, an exception is thrown in a thread keeping something alive, but if, say, it never hits the GC.KeepAlive() line of code? – Alexandru Nov 22 '13 at 02:05
  • Also, the reason why I went overboard with thread affinity is because...since I've introduced the timeout...I'm afraid of a case where my thread will awaken by hitting the timeout but (at the same time) the APC might get sent to the managed thread before I close the service handle because of a really tight race-condition there, although I could have still went a little more under-board with it I suppose. On the other hand, my thought was that I don't want to let the OS not ensure that mapping throughout the whole process anyways...I feel like if it switches threads it would be more costly . – Alexandru Nov 22 '13 at 02:30
  • I never knew about the default directive until now. Quite useful! – Alexandru Nov 22 '13 at 02:30
  • There seems to be one last shitty case, and that's...you can't abort a sleeping thread...I guess if people plan on aborting this thread...they need to give it some timeout, too. – Alexandru Nov 22 '13 at 02:53
  • I added another update to the question with the latest. Let me know what you think! – Alexandru Nov 22 '13 at 02:56
  • Also, I suppose it could be an issue if you already have thread affinity on before the call and expect it to stay on after the call. – Alexandru Nov 22 '13 at 15:29
1

Asynchronous procedure calls exist completely on the native side. APC's know nothing of managed threads nor of marshaling. NotifyServiceStatusChange would have to call (or use the equivalent of) QueueUserAPC to dispatch the APC, which only takes an native thread handle. So, the APC will be queued to the native thread that calls NotifyServiceStatusChange.

So this APC being queued and dispatched correctly rely on two things:

  1. The CLR keeps the native thread that it called NotifyServiceStatusChange.
  2. The CLR puts that native thread into an alterable wait.

You control neither of these two things.

shf301
  • 31,086
  • 2
  • 52
  • 86