首页 文章

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

提问于
浏览
543

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

这有可能吗?

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

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

11 回答

  • 29

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

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

    static void Main(string[] args)
    {
       MainAsync().GetAwaiter().GetResult();
    }
    
    
    static async Task MainAsync()
    {
       /*await stuff here*/
    }
    
  • 149

    最常被接受的答案并不完全正确 . 有一种适用于所有情况的解决方案: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 .

  • 164
    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方法 . 逻辑流程的最大区别不在于中断,这使得编写和读取更加容易 .

  • 42

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

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

  • 77

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

    首先阅读Stephen Cleary的几篇文章:

    从"two best practices"中"Don't Block on Async Code"开始,第一个没有适用(基本上如果我可以使用 await ,我会这样做!) .

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

    这是我的代码:

    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;
        }
    }
    
  • -3

    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));
    }
    

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

  • -3

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

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

    我真的无法使你的方法 async 并且你不必通过将其作为参数传递给任务上的ContinueWith方法来使用回调方法 .

  • 19
    //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;
    
  • 3

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

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

    Solution A

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

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

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

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

    Solution 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上发生了相当微妙的死锁可能性:

    • 同步方法调用异步方法,获取 Task .

    • 同步方法对 Task 执行阻塞等待 .

    • async 方法使用 await 而不使用 ConfigureAwait .

    • Task 无法在这种情况下完成,因为它只在 async 方法完成时才完成; async 方法无法完成,因为它正在尝试将其继续安排到 SynchronizationContext ,并且WinForms / WPF / SL / ASP.NET将不允许继续运行,因为同步方法已在该上下文中运行 .

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

    Solution 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.

  • 3
    var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);
    
    OpenIdConnectConfiguration config = result.GetAwaiter().GetResult();
    

    或者用这个:

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

    那些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 
        // .....
    }
    

    希望这可以帮助!

相关问题