繁体   English   中英

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

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

我正在使用一个非托管库,该库要求对其 API 的所有调用都在同一线程上运行。 我们想使用响应式扩展的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()}");
}

执行返回如下内容:

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)

任务

Task或 promise 只是回调的抽象。 而 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 ,您会注意到继续在事件循环线程上运行。

这是由于调度优化倾向于仅在当前正在运行的情况下排队。 当您调用ToTask()时,您使用的是默认调度程序,即当前线程调度程序。

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

自由的? 立即运行。

忙碌的? 排队工作。

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

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

一开始,您使事件循环忙碌,导致 go 的所有内容都进入队列,因此您随后会在事件循环线程中看到所有日志。 所以这与 P/Invoke 无关。

需要明确的是,这不是关于指定用于观察的调度程序,而是订阅。 当您将 Observable 转换为其他抽象,如任务、枚举、阻塞连接等时,可能会泄漏一些内部复杂性。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM