执行后在异步中调用 PInvoke 不会 return 到主线程

Calling a PInvoke in an async doesn't return to the main thread after execution

我正在使用一个非托管库,该库要求在同一线程上对其 API 的所有调用都是 运行。我们想使用 Reactive 扩展的 EventLoopScheduler 来促进这一点,因为我们将使用 Observable 做其他事情。

我正在使用类似于下面代码示例中的 Run 方法的方法来执行调度程序中的代码,该代码将始终 运行 在同一线程上。当我使用托管代码时,它按预期工作,并且所有调用都是 运行 在事件循环管理的线程上,异步调用之前/之后是主线程。

但是,当我调用 P/Invoke(代码示例中的那个只是一个例子,我并没有真正在我的代码中调用这个,但行为是相同的),线程确实运行 在事件循环线程上,但之后的一切也是如此!

我试过添加 ConfigureAwait(true)(和 false),但没有任何改变。我真的对这种行为感到困惑,为什么调用 P/Invoke 会改变等待 !! 之后继续的线程?

这是要重现的代码:

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

public static Task Run(Action action, IScheduler scheduler)
{
    return Observable.Start(action, scheduler).SingleAsync().ToTask();
}

public static string ThreadInfo() =>
    $"\"{Thread.CurrentThread.Name}\" ({Thread.CurrentThread.ManagedThreadId})";

private static async Task Main(string[] args)
{
    var scheduler = new EventLoopScheduler();

    Console.WriteLine($"Before managed call on thread {ThreadInfo()}");

    await Run(() => Console.WriteLine($"Managed call on thread {ThreadInfo()}"), scheduler);

    Console.WriteLine($"After managed call on thread {ThreadInfo()}");

    Console.WriteLine($"Before PInvoke on thread {ThreadInfo()}");

    await Run(() => MessageBox(IntPtr.Zero, $"Running on thread {ThreadInfo()}", "Attention", 0), scheduler);

    Console.WriteLine($"After PInvoke on thread {ThreadInfo()}");
}

执行returns是这样的:

Before managed call on thread "" (1)
Managed call on thread "Event Loop 1" (6)
After managed call on thread "" (1)
Before PInvoke on thread "" (1)
Message box displayed with text: Running on thread "Event Loop 1" (6)
After PInvoke on thread "Event Loop 1" (6)

我预期的地方

Before managed call on thread "" (1)
Managed call on thread "Event Loop 1" (6)
After managed call on thread "" (1)
Before PInvoke on thread "" (1)
Message box displayed with text: Running on thread "Event Loop 1" (6)
After PInvoke on thread "" (1)

任务

A Task 或承诺只是回调的抽象。 async/await 只是任务的语法糖。

因为它是一个回调抽象,await 不会阻塞线程。 为什么它看起来 像是在阻塞?那是因为 await 将您的代码重写到一个状态机中,当等待的任务完成时,该状态机会推进其状态。

大致改写成这样:

switch (state)
{
    case 0:
        Console.WriteLine($"Before managed call on thread {ThreadInfo()}");
        Await(Run(() => Console.WriteLine($"Managed call on thread {ThreadInfo()}"), scheduler));
        return;
    case 1:

        Console.WriteLine($"After managed call on thread {ThreadInfo()}");
        Console.WriteLine($"Before PInvoke on thread {ThreadInfo()}");
        Await(Run(() => MessageBox(IntPtr.Zero, $"Running on thread {ThreadInfo()}", "Attention", 0), scheduler));
        return;
    case 2:
        Console.WriteLine($"After PInvoke on thread {ThreadInfo()}");
        return;
}

实际重写使用 goto 而不是 switch,但概念是相同的。因此,当一个任务完成时,它会在同一个线程上下文中使用 state += 1 调用这个状态机。当您使用任务计划程序时,您只会看到任务池线程。

抽象中的漏洞

您看到此特定行为的原因的解释:

After managed call on thread "" (1)

相当复杂。它与预定的 thunk 是否立即完成有关。如果您在第一个托管调用中添加 Thread.Sleep,您会注意到事件循环线程上的延续 运行。

这是由于调度优化更喜欢只排队 if something is currently running。当您调用 ToTask() 时,您使用的是默认调度程序,即当前线程调度程序。

当前的线程调度器是这样工作的:

免费? 运行 立即。

忙吗?排队工作。

运行 立即行为是您在主线程上看到日志 运行ning 的原因。 如果你只是添加一个

var scheduler = new EventLoopScheduler();
scheduler.Schedule(() => Thread.Sleep(1000));

一开始,您让事件循环变得忙碌,导致一切都进入排队状态,因​​此您随后会在事件循环线程中看到所有日志。所以这与 P/Invoke.

无关

需要明确的是,这与指定用于观察的调度程序无关,而是与订阅有关。当您将 Observables 转换为其他抽象,如 Tasks、Enumerables、Blocking Joins 等时,一些内部复杂性可能会泄漏。