目前,所有现有的生产CLR宿主实现一对一的托管到非托管线程映射。这在Windows桌面操作系统系列中尤为明显,其中运行您的传统COM对象。
基于此,您可以使用TPL的Task.Run
代替经典线程API,并通过p/invoke调用QueueUserAPC
以释放您的COM对象,使其从可改变的等待状态中退出,当取消令牌被触发时。
下面的代码展示了如何做到这一点。需要注意的一点是,所有ThreadPool
线程(包括由Task.Run
启动的线程)都隐式地运行在COM MTA公寓下。因此,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);
}
}
}
Thread.Interrupt()
,则可能会在不恰当的时间中断您的托管代码以破坏状态。另请注意,根据COM调用正在执行的操作,您可能无法在那个时刻实际上中断线程。它很容易直到COM调用完成才抛出异常。 - Peter DunihoThread.Interrupt
以非常特定的方式运作;它会“等待”线程处于 WaitSleepJoin 状态,所以不用担心代码会被中断,除非它处于“休眠”状态(参见 MSDN)。 - Lorenzo Dematté