8

Some background: my C# code calls into some unmanaged code (C++) that does a blocking wait. The blocking wait, however, is alertable (like Thread.Sleep - I suppose it calls WaitForSingleObjectEx with bAlertable TRUE under the cover); I know for sure it is alertable, as it can be "waked up" by QueueUserAPC.

If I could simply use managed Threads, I would just call the blocking method, and then use Thread.Interrupt to "wake" the thread when I need it to exit; something like this:

void ThreadFunc() {
    try {
           Message message;
           comObject.GetMessage(out message);
           //....
     }
     catch (ThreadInterruptedException) {
        // We need to exit
        return;
     }
}

var t - new Thread(ThreadFunc);
//....
t.Interrupt();

(NOTE: I am not using this code, but it is something that, to the top of my knowledge, could work for this peculiar situation (alertable wait in unmanaged code out of my control). What I'm looking for is the best equivalent (or a better alternative!) to this in TPL).

But I have to use the TPL (Tasks instead of managed Threads), and the unmanaged method is out of my control (I cannot modify it to call WaitForMultipleObjectEx and make it return when I signal en Event, for example).

I am looking for a Thread.Interrupt equivalent for Tasks (something that will post an APC on the underlying thread). AFAIK, CancellationTokens require the code to be "Task aware", and do not use this technique, but I'm not sure: what happens, I wonder, if a task does a Thread.Sleep (I know there is a Task.Wait, but it's just for having an example of a non-task wait which is alertable), can it be cancelled?

Is my assumption wrong (I mean, could I just use a CT and everything will work? But how?).

If there is no such method... I'm open to suggestions. I'd really like to avoid to mix Threads and Tasks, or use P/Invoke, but if there is no other way, I would still like to do it in the "cleanest" way possible (which means: no rude aborts, and something "Tasky" :) )

Edit:

For those who are curious, I have "confirmed" that Thread.Interrupt could work in my case because it calls QueueUserAPC. It calls InterruptInternal, then Thread::UserInterrupt, then Alert, which queues the APC. It is actually quite clever, as it allows you to sleep/wait and then wake a thread without the need to use another synchronization primitive.

I just need to find a TPL primitive that follows the same flow

Community
  • 1
  • 1
Lorenzo Dematté
  • 7,638
  • 3
  • 37
  • 77
  • Your code example _might_ be "safe enough", depending on the elided parts. But in particular, if you don't have a way to guarantee that you call `Thread.Interrupt()` during the COM call, you could wind up corrupting state, by interrupting your managed code at an inopportune time. Note also that depending on what the COM call is doing, you may or may not be able to actually interrupt the thread at that very moment. It could easily not throw the exception until the COM call has completed. – Peter Duniho Jan 24 '15 at 18:36
  • Oh, that's not a problem. `Thread.Interrupt` acts in a very particular way; it "waits" for the thread to be in a WaitSleepJoin state, so there is no worry that the code will be interrupted, unless it is in a "sleep" state (see [MSDN](https://msdn.microsoft.com/en-us/library/system.threading.thread.interrupt.aspx)) – Lorenzo Dematté Jan 24 '15 at 18:50
  • Mind you, I agree that using Sleep/Interrupt blindly is a dangerous pattern, but if one knows what is going on is not bad, per se; it seems to me that it is implemented in a quite robust way – Lorenzo Dematté Jan 24 '15 at 18:52
  • 1
    Yes, that's true. It's just that there's not enough detail in your example to know when that might happen, and whether it is actually safe at all such points in the code to be interrupted. It all depends on what the code is doing, that's all I mean. – Peter Duniho Jan 24 '15 at 18:52

2 Answers2

4

I wonder, if a task does a Thread.Sleep (I know there is a Task.Wait, but it's just for having an example of a non-task wait which is alertable), can it be cancelled?

No, it cannot. Cancellation of tasks is defined by the user. It is cooperative cancellation which requires the user to explicitly check the state of the CancellationToken

Note that there is an overload of Task.Wait which takes a CancellationToken:

/// <summary>
/// Waits for the task to complete, for a timeout to occur, 
/// or for cancellation to be requested.
/// The method first spins and then falls back to blocking on a new event.
/// </summary>
/// <param name="millisecondsTimeout">The timeout.</param>
/// <param name="cancellationToken">The token.</param>
/// <returns>true if the task is completed; otherwise, false.</returns>
private bool SpinThenBlockingWait(int millisecondsTimeout, 
                                  CancellationToken cancellationToken)
{
    bool infiniteWait = millisecondsTimeout == Timeout.Infinite;
    uint startTimeTicks = infiniteWait ? 0 : (uint)Environment.TickCount;
    bool returnValue = SpinWait(millisecondsTimeout);
    if (!returnValue)
    {
        var mres = new SetOnInvokeMres();
        try
        {
            AddCompletionAction(mres, addBeforeOthers: true);
            if (infiniteWait)
            {
                returnValue = mres.Wait(Timeout.Infinite,
                                        cancellationToken);
            }
            else
            {
                uint elapsedTimeTicks = ((uint)Environment.TickCount) -
                                               startTimeTicks;
                if (elapsedTimeTicks < millisecondsTimeout)
                {
                    returnValue = mres.Wait((int)(millisecondsTimeout -
                                             elapsedTimeTicks), cancellationToken);
                }
            }
        }
        finally
        {
            if (!IsCompleted) RemoveContinuation(mres);
            // Don't Dispose of the MRES, because the continuation off
            // of this task may still be running.  
            // This is ok, however, as we never access the MRES' WaitHandle,
            // and thus no finalizable resources are actually allocated.
        }
    }
    return returnValue;
}

It will attempt to spin the thread on certain condition. If that isn't enough, it will end up calling Monitor.Wait which actually blocks:

/*========================================================================
** Waits for notification from the object (via a Pulse/PulseAll). 
** timeout indicates how long to wait before the method returns.
** This method acquires the monitor waithandle for the object 
** If this thread holds the monitor lock for the object, it releases it. 
** On exit from the method, it obtains the monitor lock back. 
** If exitContext is true then the synchronization domain for the context 
** (if in a synchronized context) is exited before the wait and reacquired 
**
** Exceptions: ArgumentNullException if object is null.
========================================================================*/
[System.Security.SecurityCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern bool ObjWait(bool exitContext, int millisecondsTimeout, Object obj);
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • That's what I suspected... Thank you for confirming it – Lorenzo Dematté Jan 24 '15 at 18:28
  • @PeterDuniho This is what `Task.Wait` calls internally. – Yuval Itzchakov Jan 24 '15 at 18:34
  • So it does a busy waiting.. I'm a bit disappointed to hear that, and certainly confirms my suspicion: no "real wait" here. – Lorenzo Dematté Jan 24 '15 at 18:37
  • @Lorenzo - If the spin isn't enough, it does end up calling `Monitor.Wait`, which is a blocking call – Yuval Itzchakov Jan 24 '15 at 18:38
  • Oh, Ok! I missed that.. much better, but it does not help me a lot :) Unless ObjWait calls into the runtime following the same path of Sleep, into a alertable wait.. I will try to find the sources. And then if nobody comes up with an answer in the meantime, just try it myself. – Lorenzo Dematté Jan 24 '15 at 18:43
  • @Lorenzo Guess not :/ Note that this should work though if you pass a `CancellationToken`, you should only know that it will spin wait before blocking. – Yuval Itzchakov Jan 24 '15 at 18:45
  • your answer pointed me in the right direction. It seems it *may* work, as ObjWait (after a series of calls) ends up calling Thread::Block, which does an alertable wait `WaitMode_Alertable` - at least, according to the [SSCLI](https://github.com/gbarnett/shared-source-cli-2.0/blob/d63349c09c2e93e4bfc4c8b147ff0805f36cec68/clr/src/vm/threads.cpp#L3669). So if a CT can wake up a `Task::Wait` that is doing an ObjWait, it probably uses APCs as well. I will try to figure out how tomorrow. – Lorenzo Dematté Jan 24 '15 at 19:57
  • @Lorenzo Note that the cancellation token doesn't drill all the way down to the `Monitor.Wait`. Eventually, only a time out is passed. – Yuval Itzchakov Jan 24 '15 at 20:04
  • @Lorenzo Read [this method](http://referencesource.microsoft.com/#mscorlib/system/threading/ManualResetEventSlim.cs,bbdade44882603cf) to see how it goes down – Yuval Itzchakov Jan 24 '15 at 20:07
4

Currently, all existing production CLR hosts implement one-to-one managed-to-unmanaged thread mapping. This is particularly true about Windows Desktop OS family where your legacy COM object runs.

In this light, you can use TPL's Task.Run instead of classic threading APIs and still call QueueUserAPC via p/invoke to release your COM object from alterable wait state, when the cancellation token has been triggered.

The code below shows how to do that. One thing to note, all ThreadPool threads (including those started by Task.Run) implicitly run under COM MTA apartment. Thus, the COM object needs to support the MTA model without implicit COM marshaling. If it isn't the case, you'll probably need a custom task scheduler (like StaTaskScheduler) to be used instead of Task.Run.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        static int ComGetMessage()
        {
            NativeMethods.SleepEx(2000, true);
            return 42;
        }

        static int GetMessage(CancellationToken token)
        {
            var apcWasCalled = false;
            var gcHandle = default(GCHandle);
            var apcCallback = new NativeMethods.APCProc(target => 
            {
                apcWasCalled = true;
                gcHandle.Free();
            });

            var hCurThread = NativeMethods.GetCurrentThread();
            var hCurProcess = NativeMethods.GetCurrentProcess();
            IntPtr hThread;
            if (!NativeMethods.DuplicateHandle(
                hCurProcess, hCurThread, hCurProcess, out hThread,
                0, false, NativeMethods.DUPLICATE_SAME_ACCESS))
            {
                throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
            }
            try
            {
                int result;
                using (token.Register(() => 
                    {
                        gcHandle = GCHandle.Alloc(apcCallback);
                        NativeMethods.QueueUserAPC(apcCallback, hThread, UIntPtr.Zero);
                    },
                    useSynchronizationContext: false))
                {
                    result = ComGetMessage();
                }
                Trace.WriteLine(new { apcWasCalled });
                token.ThrowIfCancellationRequested();
                return result;
            }
            finally
            {
                NativeMethods.CloseHandle(hThread);
            }
        }

        static async Task TestAsync(int delay)
        {
            var cts = new CancellationTokenSource(delay);
            try
            {
                var result = await Task.Run(() => GetMessage(cts.Token));
                Console.WriteLine(new { result });
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Cancelled.");
            }
        }

        static void Main(string[] args)
        {
            TestAsync(3000).Wait();
            TestAsync(1000).Wait();
        }

        static class NativeMethods
        {
            public delegate void APCProc(UIntPtr dwParam);

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern uint SleepEx(uint dwMilliseconds, bool bAlertable);

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern uint QueueUserAPC(APCProc pfnAPC, IntPtr hThread, UIntPtr dwData);

            [DllImport("kernel32.dll")]
            public static extern IntPtr GetCurrentThread();

            [DllImport("kernel32.dll")]
            public static extern IntPtr GetCurrentProcess();

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool CloseHandle(IntPtr handle);

            public const uint DUPLICATE_SAME_ACCESS = 2;

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle,
               IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle,
               uint dwDesiredAccess, bool bInheritHandle, uint dwOptions);
        }
    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 2
    Very nice answer, I'll go with this approach as it seems there is no "implicit" APC sent by the cancellation token by itself. Only one observation: even if your premise is true, I would be extra-safe and call [BeginThreadAffinity](https://msdn.microsoft.com/en-us/library/system.threading.thread.beginthreadaffinity%28v=vs.110%29.aspx) and EndThreadAffinity – Lorenzo Dematté Jan 26 '15 at 16:17
  • @LorenzoDematté, surely Begin/EndThreadAffinity wouldn't hurt here. – noseratio Jan 26 '15 at 20:51