异步并等待使用Task的同步方法

时间:2016-12-06 12:51:41

标签: c# .net async-await uwp task

我对这个异步/等待概念非常新,所以我为任何明显的问题道歉。

我需要发送电子邮件,新的API要求我使用 async 并等待。问题是我的很多方法需要调用这个"发送电子邮件"的同步即可。

所以,我创建了一个同步包装器方法:

private Task SendEmailAsync(string email, string content)
{
   ...
   ...
   ...
}

private void SendEmail(string email, string content)
{
    Task tsk = Task.Factory.StartNew(async () => await SendEmailAsync(email, content));
    try { tsk.Wait(); }
    catch (AggregateException aex)
    {
        throw aex.Flatten();
    }
}

但出于某种原因, tsk.Wait()不会等待等待SendEmailAsync(...)完成。所以,我需要添加ManualResetEvent。像这样的东西

private void SendEmail(string email, string content)
{
    ManualResetEvent mre = new ManualResetEvent(false);
    Task tsk = Task.Factory.StartNew(async () =>
    {
        mre.Reset();
        try { await SendEmailAsync(email, content); }
        finally { mre.Set(); }
    });

    mre.WaitOne();
    try { tsk.Wait(); }
    catch (AggregateException aex)
    {
        throw aex.Flatten();
    }
}

tsk.Wait()不会捕获 SendEmailAsync(...)引发的任何异常。我的问题是:

  1. 为什么 tsk.Wait()不等待 await SendEmailAsync(...)
  2. 如何捕获 await SendEmailAsync(...)
  3. 引发的异常

    谢谢!

3 个答案:

答案 0 :(得分:1)

我首先应该说明你应该遵循@ChrFin的评论并尝试重构你的代码以使其异步,而不是试图同步运行现有的异步库(你甚至会注意到一个改进在应用程序的表现中)。

要完成您所寻求的目标,您应该使用SendEmailAsync(email, content).GetAwaiter().GetResult()

为什么不SendEmailAsync(email, content).Wait()你可能会问。

嗯,有一个微妙的区别。 Wait()会将任何运行时异常包装在AggregateException中,如果你试图捕获原始异常,这将使你的生活更加艰难。

GetAwaiter().GetResult()只会抛出原始异常。

您的代码如下所示:

private void SendEmail(string email, string content)
{
    try
    {
        SendEmailAsync(email, content).GetAwaiter().GetResult();
    }
    catch(Exception ex)
    {
        // ex is the original exception
        // Handle ex or rethrow or don't even catch
    }
}

答案 1 :(得分:1)

您可以使用以下扩展名以同步方式运行异步代码。

https://stackoverflow.com/a/5097066/5062791

public static class AsyncHelpers
{
    /// <summary>
    /// Execute's an async Task<T> method which has a void return value synchronously
    /// </summary>
    /// <param name="task">Task<T> method to execute</param>
    public static void RunSync(Func<Task> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(async _ =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();

        SynchronizationContext.SetSynchronizationContext(oldContext);
    }

    /// <summary>
    /// Execute's an async Task<T> method which has a T return type synchronously
    /// </summary>
    /// <typeparam name="T">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static T RunSync<T>(Func<Task<T>> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        T ret = default(T);
        synch.Post(async _ =>
        {
            try
            {
                ret = await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();
        SynchronizationContext.SetSynchronizationContext(oldContext);
        return ret;
    }

    private class ExclusiveSynchronizationContext : SynchronizationContext
    {
        private bool done;
        public Exception InnerException { get; set; }
        readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
        readonly Queue<Tuple<SendOrPostCallback, object>> items =
            new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotSupportedException("We cannot send to our same thread");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            lock (items)
            {
                items.Enqueue(Tuple.Create(d, state));
            }
            workItemsWaiting.Set();
        }

        public void EndMessageLoop()
        {
            Post(_ => done = true, null);
        }

        public void BeginMessageLoop()
        {
            while (!done)
            {
                Tuple<SendOrPostCallback, object> task = null;
                lock (items)
                {
                    if (items.Count > 0)
                    {
                        task = items.Dequeue();
                    }
                }
                if (task != null)
                {
                    task.Item1(task.Item2);
                    if (InnerException != null) // the method threw an exeption
                    {
                        throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
                    }
                }
                else
                {
                    workItemsWaiting.WaitOne();
                }
            }
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    }
}

答案 2 :(得分:1)

  

我需要发送电子邮件,新的API要求我使用async和await。问题是我的很多方法需要同步调用这个“发送电子邮件”。

最佳解决方案是删除“同步调用者”要求。相反,您应该允许asyncawait在代码库中自然增长。

  

由于某种原因,tsk.Wait()不等待等待SendEmailAsync(...)完成。

那是因为您使用的是Task.Factory.StartNew dangerous API

  

我曾尝试使用Task.Result和Task.GetAwaiter()。GetResult()但导致死锁。

我详细解释了这个死锁on my blog

  

我使用过ColinM发布的解决方案,但效果很好。

这是一个危险的解决方案。除非您完全了解 ,否则我不建议使用它。

如果绝对必须实现同步调用异步代码的反模式,那么你需要使用hack这样做。我在brownfield async的文章中介绍了各种黑客行为。在你的情况下,你可能只是使用线程池hack,这只是一行代码:

private void SendEmail(string email, string content) =>
    Task.Run(() => SendEmailAsync(email, content)).GetAwaiter().GetResult();

但是,正如我在本回答开头所述,理想解决方案只是允许async增长。像这样的黑客限制了Web服务器的可扩展性。