任务TPL的Thread.Interrupt等价物

时间:2015-01-24 18:17:48

标签: c# .net multithreading task-parallel-library

一些背景知识:我的C#代码调用了一些阻塞等待的非托管代码(C ++)。但是,阻止等待是可见的(例如Thread.Sleep - 我想它会在封面下调用WaitForSingleObjectEx bAlertable TRUE};我知道它确实是警觉的,因为它可以被唤醒" QueueUserAPC

如果我可以简单地使用托管线程,我只需要调用阻塞方法,然后使用Thread.Interrupt来唤醒"唤醒"当我需要它退出时的线程;像这样的东西:

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

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

(注意:我没有使用这个代码,但据我所知,它可能适用于这种特殊情况(在我的控制之外的非托管代码中可警告等待)。我是什么在TPL中寻找最好的等价物(或更好的替代品!)。

但是我必须使用TPL(任务代替托管的线程),并且非托管方法不受我的控制(我无法修改它以调用WaitForMultipleObjectEx并使其在我发出事件信号时返回,例子)。

我正在为任务寻找一个Thread.Interrupt等价物(会在底层线程上发布一个APC)。 AFAIK,CancellationTokens要求代码是"任务感知",并且不使用这种技术,但我不确定:我想知道,如果任务执行Thread.Sleep会发生什么(我知道有一个Task.Wait,但它只是有一个可以提醒的非任务等待的例子,可以取消吗?

我的假设是错的(我的意思是,我可以只使用CT,一切都会起作用吗?但是如何?)。

如果没有这样的方法......我愿意接受建议。我真的很想避免混合使用线程和任务,或者使用P / Invoke,但如果没有其他方法,我仍然希望在"最干净的"可能的方式(这意味着:没有粗鲁的中止,以及某些事情" Tasky" :))

编辑:

对于那些好奇的人,我已经确认了#34; Thread.Interrupt可以在我的情况下工作,因为它调用QueueUserAPC。 它会调用InterruptInternal,然后是Thread::UserInterrupt,然后是Alert,它会对APC进行排队。它实际上非常聪明,因为它允许您休眠/等待然后唤醒线程而无需使用其他同步原语。

我只需要找到一个遵循相同流程的TPL原语

2 个答案:

答案 0 :(得分:4)

  

我想知道,如果一个任务执行了Thread.Sleep(我知道有一个Task.Wait,但它只是为了有一个可以警告的非任务等待的例子),它可以被取消吗?

不,它不能。取消任务由用户定义。它是合作取消,要求用户明确检查CancellationToken

的状态

请注意,Task.Wait的重载需要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;
}

它会尝试在特定条件下旋转线程。如果这还不够,它最终会调用实际阻止的Monitor.Wait

/*========================================================================
** 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);

答案 1 :(得分:4)

目前,所有现有的生产CLR主机implement one-to-one managed-to-unmanaged thread mapping。对于运行旧COM对象的Windows桌面操作系统系列尤其如此。

有鉴于此,您可以使用TPL的Task.Run代替经典线程API,并仍然通过p / invoke调用QueueUserAPC,以便在取消时将COM对象从可变等待状态释放令牌已被触发。

下面的代码显示了如何执行此操作。需要注意的是,所有ThreadPool个主题(包括由Task.Run启动的主题)都隐含在COM MTA apartment下运行。因此,COM对象需要支持MTA模型而不需要隐式COM编组。如果不是这样,您可能需要使用自定义任务计划程序(如StaTaskScheduler)而不是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);
        }
    }
}