如何从C#中的同步方法调用异步方法?

问题描述 投票:652回答:14

我有一个public async void Foo()方法,我想从同步方法调用。到目前为止,我从MSDN文档中看到的是通过异步方法调用异步方法,但我的整个程序不是使用异步方法构建的。

这甚至可能吗?

以下是从异步方法调用这些方法的一个示例:http://msdn.microsoft.com/en-us/library/hh300224(v=vs.110).aspx

现在我正在研究从同步方法调用这些异步方法。

c# async-await
14个回答
562
投票

异步编程通过代码库“增长”。它一直是compared to a zombie virus。最好的解决方案是让它成长,但有时这是不可能的。

我在Nito.AsyncEx库中编写了一些类型来处理部分异步代码库。但是,没有适用于所有情况的解决方案。

解决方案A.

如果你有一个简单的异步方法,不需要同步回其上下文,那么你可以使用Task.WaitAndUnwrapException

var task = MyAsyncMethod();
var result = task.WaitAndUnwrapException();

您不想使用Task.WaitTask.Result,因为它们在AggregateException中包含异常。

只有当MyAsyncMethod不同步回其上下文时,此解决方案才适用。换句话说,await中的每个MyAsyncMethod都应以ConfigureAwait(false)结束。这意味着它无法更新任何UI元素或访问ASP.NET请求上下文。

解决方案B.

如果MyAsyncMethod确实需要同步回其上下文,那么您可以使用AsyncContext.RunTask来提供嵌套上下文:

var result = AsyncContext.RunTask(MyAsyncMethod).Result;

* 2014年4月14日更新:在更新版本的库中,API如下:

var result = AsyncContext.Run(MyAsyncMethod);

(在这个例子中使用Task.Result是可以的,因为RunTask将传播Task异常)。

您可能需要AsyncContext.RunTask而不是Task.WaitAndUnwrapException的原因是因为在WinForms / WPF / SL / ASP.NET上发生了相当微妙的死锁可能性:

  1. 同步方法调用异步方法,获取Task
  2. 同步方法在Task上执行阻塞等待。
  3. async方法使用没有awaitConfigureAwait
  4. Task无法在这种情况下完成,因为它只在async方法完成时完成; async方法无法完成,因为它正在尝试将其继续安排到SynchronizationContext,而WinForms / WPF / SL / ASP.NET将不允许继续运行,因为同步方法已在该上下文中运行。

这就是为什么尽可能在每个ConfigureAwait(false)方法中使用async的好主意之一。

解决方案C.

AsyncContext.RunTask不会在每种情况下都有效。例如,如果async方法等待需要UI事件完成的内容,那么即使使用嵌套上下文也会出现死锁。在这种情况下,您可以在线程池上启动async方法:

var task = TaskEx.RunEx(async () => await MyAsyncMethod());
var result = task.WaitAndUnwrapException();

但是,此解决方案需要一个可在线程池上下文中使用的MyAsyncMethod。因此它无法更新UI元素或访问ASP.NET请求上下文。在这种情况下,您也可以将ConfigureAwait(false)添加到其await语句中,并使用解决方案A.

更新,2019-05-01:当前“最差的最差做法”是在MSDN article here


2
投票
var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);

OpenIdConnectConfiguration config = result.GetAwaiter().GetResult();

或者用这个:

var result=result.GetAwaiter().GetResult().AccessToken

2
投票

我知道我已经很晚了。但是,如果像我这样的人想要以简洁明了的方式解决这个问题,而不依赖于另一个库。

我从piece of code找到了以下Ryan

public static class AsyncHelpers
{
    private static readonly TaskFactory taskFactory = new
        TaskFactory(CancellationToken.None,
            TaskCreationOptions.None,
            TaskContinuationOptions.None,
            TaskScheduler.Default);

    /// <summary>
    /// Executes an async Task method which has a void return value synchronously
    /// USAGE: AsyncUtil.RunSync(() => AsyncMethod());
    /// </summary>
    /// <param name="task">Task method to execute</param>
    public static void RunSync(Func<Task> task)
        => taskFactory
            .StartNew(task)
            .Unwrap()
            .GetAwaiter()
            .GetResult();

    /// <summary>
    /// Executes an async Task<T> method which has a T return type synchronously
    /// USAGE: T result = AsyncUtil.RunSync(() => AsyncMethod<T>());
    /// </summary>
    /// <typeparam name="TResult">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static TResult RunSync<TResult>(Func<Task<TResult>> task)
        => taskFactory
            .StartNew(task)
            .Unwrap()
            .GetAwaiter()
            .GetResult();
}

然后你就可以这样称呼它

var t = AsyncUtil.RunSync<T>(() => AsyncMethod<T>());

-1
投票

如果你想运行它同步

MethodAsync().RunSynchronously()

-3
投票

那些windows异步方法有一个很棒的小方法,叫做AsTask()。您可以使用此方法将该方法作为任务返回,以便您可以手动调用Wait()。

例如,在Windows Phone 8 Silverlight应用程序上,您可以执行以下操作:

private void DeleteSynchronous(string path)
{
    StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
    Task t = localFolder.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask();
    t.Wait();
}

private void FunctionThatNeedsToBeSynchronous()
{
    // Do some work here
    // ....

    // Delete something in storage synchronously
    DeleteSynchronous("pathGoesHere");

    // Do other work here 
    // .....
}

希望这可以帮助!


-4
投票
   //Example from non UI thread -    
   private void SaveAssetAsDraft()
    {
        SaveAssetDataAsDraft();
    }
    private async Task<bool> SaveAssetDataAsDraft()
    {
       var id = await _assetServiceManager.SavePendingAssetAsDraft();
       return true;   
    }
   //UI Thread - 
   var result = Task.Run(() => SaveAssetDataAsDraft().Result).Result;

223
投票

添加一个最终解决了我的问题的解决方案,希望能节省一些人的时间。

首先阅读几篇Stephen Cleary文章:

从“不要阻止异步代码”中的“两个最佳实践”开始,第一个不适用于我,第二个不适用(基本上如果我可以使用await,我做!)。

所以这是我的解决方法:将调用包装在Task.Run<>(async () => await FunctionAsync());中,希望不再有死锁。

这是我的代码:

public class LogReader
{
    ILogger _logger;

    public LogReader(ILogger logger)
    {
        _logger = logger;
    }

    public LogEntity GetLog()
    {
        Task<LogEntity> task = Task.Run<LogEntity>(async () => await GetLogAsync());
        return task.Result;
    }

    public async Task<LogEntity> GetLogAsync()
    {
        var result = await _logger.GetAsync();
        // more code here...
        return result as LogEntity;
    }
}

176
投票

Microsoft构建了一个AsyncHelper(内部)类来将Async作为Sync运行。来源看起来像:

internal static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new 
      TaskFactory(CancellationToken.None, 
                  TaskCreationOptions.None, 
                  TaskContinuationOptions.None, 
                  TaskScheduler.Default);

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return AsyncHelper._myTaskFactory
          .StartNew<Task<TResult>>(func)
          .Unwrap<TResult>()
          .GetAwaiter()
          .GetResult();
    }

    public static void RunSync(Func<Task> func)
    {
        AsyncHelper._myTaskFactory
          .StartNew<Task>(func)
          .Unwrap()
          .GetAwaiter()
          .GetResult();
    }
}

Microsoft.AspNet.Identity基类只有Async方法,为了将它们称为Sync,有些类的扩展方法看起来像(示例用法):

public static TUser FindById<TUser, TKey>(this UserManager<TUser, TKey> manager, TKey userId) where TUser : class, IUser<TKey> where TKey : IEquatable<TKey>
{
    if (manager == null)
    {
        throw new ArgumentNullException("manager");
    }
    return AsyncHelper.RunSync<TUser>(() => manager.FindByIdAsync(userId));
}

public static bool IsInRole<TUser, TKey>(this UserManager<TUser, TKey> manager, TKey userId, string role) where TUser : class, IUser<TKey> where TKey : IEquatable<TKey>
{
    if (manager == null)
    {
        throw new ArgumentNullException("manager");
    }
    return AsyncHelper.RunSync<bool>(() => manager.IsInRoleAsync(userId, role));
}

对于那些关注代码许可条款的人来说,这里有一个链接到非常相似的代码(只是在线程上增加了对文化的支持),这些代码有注释表明它是由MIT许可的。 https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs


114
投票

async Main现在是C#7.2的一部分,可以在项目高级构建设置中启用。

对于C#<7.2,正确的方法是:

static void Main(string[] args)
{
   MainAsync().GetAwaiter().GetResult();
}


static async Task MainAsync()
{
   /*await stuff here*/
}

您会在许多Microsoft文档中看到这种情况,例如:https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions


47
投票
public async Task<string> StartMyTask()
{
    await Foo()
    // code to execute once foo is done
}

static void Main()
{
     var myTask = StartMyTask(); // call your method which will return control once it hits await
     // now you can continue executing code here
     string result = myTask.Result; // wait for the task to complete to continue
     // use result

}

您将'await'关键字读作“启动此长时间运行的任务,然后将控制权返回给调用方法”。一旦长时间运行的任务完成,它就会执行后面的代码。 await之后的代码类似于以前的CallBack方法。逻辑流程的最大区别不在于中断,这使得编写和读取更加容易。


31
投票

我不是百分百肯定,但我相信this blog中描述的技术应该适用于许多情况:

因此,如果要直接调用此传播逻辑,可以使用task.GetAwaiter().GetResult()


23
投票

最常被接受的答案并不完全正确。有一种适用于所有情况的解决方案:ad-hoc消息泵(SynchronizationContext)。

调用线程将按预期被阻塞,同时仍然确保从异步函数调用的所有continuation都不会死锁,因为它们将被封送到在调用线程上运行的ad-hoc SynchronizationContext(消息泵)。

ad-hoc消息泵助手的代码:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Threading
{
    /// <summary>Provides a pump that supports running asynchronous methods on the current thread.</summary>
    public static class AsyncPump
    {
        /// <summary>Runs the specified asynchronous method.</summary>
        /// <param name="asyncMethod">The asynchronous method to execute.</param>
        public static void Run(Action asyncMethod)
        {
            if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");

            var prevCtx = SynchronizationContext.Current;
            try
            {
                // Establish the new context
                var syncCtx = new SingleThreadSynchronizationContext(true);
                SynchronizationContext.SetSynchronizationContext(syncCtx);

                // Invoke the function
                syncCtx.OperationStarted();
                asyncMethod();
                syncCtx.OperationCompleted();

                // Pump continuations and propagate any exceptions
                syncCtx.RunOnCurrentThread();
            }
            finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
        }

        /// <summary>Runs the specified asynchronous method.</summary>
        /// <param name="asyncMethod">The asynchronous method to execute.</param>
        public static void Run(Func<Task> asyncMethod)
        {
            if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");

            var prevCtx = SynchronizationContext.Current;
            try
            {
                // Establish the new context
                var syncCtx = new SingleThreadSynchronizationContext(false);
                SynchronizationContext.SetSynchronizationContext(syncCtx);

                // Invoke the function and alert the context to when it completes
                var t = asyncMethod();
                if (t == null) throw new InvalidOperationException("No task provided.");
                t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);

                // Pump continuations and propagate any exceptions
                syncCtx.RunOnCurrentThread();
                t.GetAwaiter().GetResult();
            }
            finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
        }

        /// <summary>Runs the specified asynchronous method.</summary>
        /// <param name="asyncMethod">The asynchronous method to execute.</param>
        public static T Run<T>(Func<Task<T>> asyncMethod)
        {
            if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");

            var prevCtx = SynchronizationContext.Current;
            try
            {
                // Establish the new context
                var syncCtx = new SingleThreadSynchronizationContext(false);
                SynchronizationContext.SetSynchronizationContext(syncCtx);

                // Invoke the function and alert the context to when it completes
                var t = asyncMethod();
                if (t == null) throw new InvalidOperationException("No task provided.");
                t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);

                // Pump continuations and propagate any exceptions
                syncCtx.RunOnCurrentThread();
                return t.GetAwaiter().GetResult();
            }
            finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
        }

        /// <summary>Provides a SynchronizationContext that's single-threaded.</summary>
        private sealed class SingleThreadSynchronizationContext : SynchronizationContext
        {
            /// <summary>The queue of work items.</summary>
            private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> m_queue =
                new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
            /// <summary>The processing thread.</summary>
            private readonly Thread m_thread = Thread.CurrentThread;
            /// <summary>The number of outstanding operations.</summary>
            private int m_operationCount = 0;
            /// <summary>Whether to track operations m_operationCount.</summary>
            private readonly bool m_trackOperations;

            /// <summary>Initializes the context.</summary>
            /// <param name="trackOperations">Whether to track operation count.</param>
            internal SingleThreadSynchronizationContext(bool trackOperations)
            {
                m_trackOperations = trackOperations;
            }

            /// <summary>Dispatches an asynchronous message to the synchronization context.</summary>
            /// <param name="d">The System.Threading.SendOrPostCallback delegate to call.</param>
            /// <param name="state">The object passed to the delegate.</param>
            public override void Post(SendOrPostCallback d, object state)
            {
                if (d == null) throw new ArgumentNullException("d");
                m_queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
            }

            /// <summary>Not supported.</summary>
            public override void Send(SendOrPostCallback d, object state)
            {
                throw new NotSupportedException("Synchronously sending is not supported.");
            }

            /// <summary>Runs an loop to process all queued work items.</summary>
            public void RunOnCurrentThread()
            {
                foreach (var workItem in m_queue.GetConsumingEnumerable())
                    workItem.Key(workItem.Value);
            }

            /// <summary>Notifies the context that no more work will arrive.</summary>
            public void Complete() { m_queue.CompleteAdding(); }

            /// <summary>Invoked when an async operation is started.</summary>
            public override void OperationStarted()
            {
                if (m_trackOperations)
                    Interlocked.Increment(ref m_operationCount);
            }

            /// <summary>Invoked when an async operation is completed.</summary>
            public override void OperationCompleted()
            {
                if (m_trackOperations &&
                    Interlocked.Decrement(ref m_operationCount) == 0)
                    Complete();
            }
        }
    }
}

用法:

AsyncPump.Run(() => FooAsync(...));

有关异步泵的详细说明,请访问here


6
投票

对于任何关注这个问题的人来说......

如果你看看Microsoft.VisualStudio.Services.WebApi有一个名为TaskExtensions的课程。在该类中,您将看到静态扩展方法Task.SyncResult(),它完全阻止线程直到任务返回。

在内部,它调用task.GetAwaiter().GetResult()非常简单,但它超负荷工作任何async方法返回TaskTask<T>Task<HttpResponseMessage> ...语法糖,宝贝...爸爸有一颗甜食。

看起来...GetAwaiter().GetResult()是在阻塞上下文中执行异步代码的MS官方方式。似乎对我的用例非常好。


3
投票

您可以从同步代码调用任何异步方法,也就是说,直到您需要对它们进行await,在这种情况下,它们也必须标记为async

正如很多人在这里建议的那样,你可以在同步方法中调用结果任务上的Wait()或Result,但最后你会在该方法中调用阻塞调用,这会破坏异步的目的。

我真的无法使你的方法async并且你不想锁定同步方法,那么你将不得不使用一个回调方法,将它作为参数传递给任务上的ContinueWith方法。

© www.soinside.com 2019 - 2024. All rights reserved.