如何在不阻塞主线程的情况下将异步函数调用变为同步?

3

我编写了一个简单的控制台应用程序,测试ConfigureAwait(false)修复在同步方式下调用异步函数的问题。这是我模拟事件并尝试以同步方式调用异步函数的方式。

 public static class Test2
    {
        private static EventHandler<int> _somethingEvent;

        public static void Run()
        {
            _somethingEvent += OnSomething;
            _somethingEvent(null, 10);
            Console.WriteLine("fetch some other resources");
            Console.ReadLine();
        }

        private static void OnSomething(object sender, int e)
        {
            Console.WriteLine($"passed value : {e}");
            FakeAsync().Wait();
        }

        private static async Task FakeAsync()
        {
            await Task.Delay(2000).ConfigureAwait(false);
            Console.WriteLine($"task completed");
        }
    }

代码的输出结果如下:

passed value : 10
task completed
fetch some other resources

当我将OnSomething函数更改为异步无返回值时,如下所示:

private static async void OnSomething(object sender, int e)
{
    Console.WriteLine($"passed value : {e}");
    await FakeAsync();
}

输出将变为如下内容:
passed value : 10
fetch some other resources
task completed

你可以看到在任务完成部分之前发生了“获取其他资源”的操作,这是我想要的结果。我的问题是如何通过调用异步函数使其与我为OnSomething函数编写的第一个签名同步?我应该如何使用“ConfigureAwait(false)”帮助我解决这个问题?或者我没有正确地使用它。

我不确定是否完全理解,只是简单地评论:您可以等待一定数量的任务完成后再继续执行其他代码。您可以通过创建任务,然后等待Task.WhenAll(task 1, task 2, ...)来实现这一点。 - Jonast92
1
是的,我知道。但我的问题不是那样的。我的事件会在任何时候发生,而我不知道什么时候会发生。我只想让我的同步调用像异步调用一样运行。https://www.youtube.com/watch?v=avhLfcgIwbg - Milad Bonakdar
2
你说你想让同步等待的行为像异步等待一样。同步和异步是相反的。要使等待的行为像异步等待一样,就必须制作一个异步等待,而不是同步等待。 - Eric Lippert
2
这样想。你把面包放进烤面包机里。你有两个选择。你可以一直站在烤面包机旁边,什么也不做,直到面包弹出来——这是同步等待。或者你可以去检查你的信箱是否有包裹到达,当你检查完毕后,再去看看烤面包机是否已经弹出了面包。这就是异步等待。请描述一下您在这个类比中所说的同步等待如何表现为异步。了解人们真正想要的功能有助于我设计更好的功能。 - Eric Lippert
我在思考如果我使用 *ConfigureAwait(false)*,下面的代码将由后台线程运行,主线程可以自由返回并继续执行它原本要做的事情。 - Milad Bonakdar
2个回答

4
我写了一个简单的控制台应用程序来测试ConfigureAwait(false),以解决在同步方式下调用异步函数的问题。
这样做存在一些问题。
  1. ConfigureAwait(false) 不是解决同步调用异步函数的“问题”的“修复”方法。它是 一种可能的hack,在某些情况下起作用,但通常不建议使用,因为您必须在该函数的整个传递闭包中(包括二级和三级调用)每个地方使用 ConfigureAwait(false),如果您只错过了一个,仍然存在死锁的可能性。解决从同步函数调用异步函数的问题的最佳解决方案是将调用函数更改为异步(“完全异步”)。有时这是不可能的,您需要一个hack,但没有一个hack适用于所有情况。
  2. 您试图避免的死锁问题具有两个要素:在任务上阻塞线程和单线程上下文。控制台应用程序没有单线程上下文,因此在控制台应用程序中,就好像您的所有代码都在各处使用 ConfigureAwait(false)
我的问题是如何通过调用异步函数来实现与我为OnSomething函数编写的第一个签名相同的结果,使其同步?
调用代码将在异步代码上阻塞(例如Wait),或者不会(例如await)。您不能在异步代码上阻塞并继续执行。调用线程必须继续执行或阻塞;它不能同时执行两个操作。

为了解决同步调用异步函数的问题,是否可以安排一个定时器来定期检查任务状态,并在任务完成时执行继续操作?积极监视Task.IsCompleted似乎可以解决这个问题。请参见Eric's answer以了解我得到这个想法的地方。 - Luca Cremonesi
@LucaCremonesi:你的代码要么阻塞线程,要么不阻塞。如果线程被阻塞,它可能会死锁。如果线程没有被阻塞,那么await比定时器更简单。 - Stephen Cleary
如果我的代码可能会阻塞线程,我可以在任务完成后安排一个定时器。实际上,根据Eric Lippert的说法,“如果您已经知道任务已完成,则同步等待结果是安全的。”此时,我们可以调用task.Result而不会导致任何可能的死锁。这正确吗? - Luca Cremonesi
是的,从技术上讲是正确的。你可以在后台线程上启动一个任务,并启动一个定时器来轮询该任务的完成情况。但更简单的方法是只需执行await Task.Run(..); - 这将在后台线程上启动一个任务,然后安排一个继续运行以在任务完成时立即运行。 - Stephen Cleary
谢谢你的解释,Stephen。所以,基本上你是在提到你有关在同步方法中调用异步函数的“方案C”答案。这比启动计时器并手动检查任务状态要容易得多。如果你的异步函数在线程池上下文中正确工作,那应该是解决问题的最佳方案。 - Luca Cremonesi
1
@LucaCremonesi:不要忘记最好的解决方案始终是全部采用异步方式。仅在必须时阻塞。 - Stephen Cleary

1

当您调用_somethingEvent(null, 10);时,OnSomething没有被等待,因此在执行await FakeAsync();后,下面的Console.WriteLine("fetch some other resources");将被允许直接运行。

如果您不需要在继续之前等待OnSomething完成,只需进行fire and forget:

private static void OnSomething(object sender, int e)
{
    Console.WriteLine($"passed value : {e}");
    Task.Run(() => FakeAsync());
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接