How to call asynchronous method from synchronous method in C#?

后端 未结 15 1051
星月不相逢
星月不相逢 2020-11-21 06:27

I have a public async void Foo() method that I want to call from synchronous method. So far all I have seen from MSDN documentation is calling async methods via

相关标签:
15条回答
  • 2020-11-21 06:39

    I'm not 100% sure, but I believe the technique described in this blog should work in many circumstances:

    You can thus use task.GetAwaiter().GetResult() if you want to directly invoke this propagation logic.

    0 讨论(0)
  • 2020-11-21 06:39

    Inspired by some of the other answers, I created the following simple helper methods:

    public static TResult RunSync<TResult>(Func<Task<TResult>> method)
    {
        var task = method();
        return task.GetAwaiter().GetResult();
    }
    
    public static void RunSync(Func<Task> method)
    {
        var task = method();
        task.GetAwaiter().GetResult();
    }
    

    They can be called as follows (depending on whether you are returning a value or not):

    RunSync(() => Foo());
    var result = RunSync(() => FooWithResult());
    

    Note that the signature in the original question public async void Foo() is incorrect. It should be public async Task Foo() as you should return Task not void for async methods that don't return a value (yes, there are some rare exceptions).

    0 讨论(0)
  • 2020-11-21 06:43

    Adding a solution that finally solved my problem, hopefully saves somebody's time.

    Firstly read a couple articles of Stephen Cleary:

    • Async and Await
    • Don't Block on Async Code

    From the "two best practices" in "Don't Block on Async Code", the first one didn't work for me and the second one wasn't applicable (basically if I can use await, I do!).

    So here is my workaround: wrap the call inside a Task.Run<>(async () => await FunctionAsync()); and hopefully no deadlock anymore.

    Here is my code:

    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;
        }
    }
    
    0 讨论(0)
  • 2020-11-21 06:43

    There is, however, a good solution that works in (almost: see comments) every situation: an ad-hoc message pump (SynchronizationContext).

    The calling thread will be blocked as expected, while still ensuring that all continuations called from the async function don't deadlock as they'll be marshaled to the ad-hoc SynchronizationContext (message pump) running on the calling thread.

    The code of the ad-hoc message pump helper:

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

    Usage:

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

    More detailed description of the async pump is available here.

    0 讨论(0)
  • 2020-11-21 06:44

    If you want to run it Sync

    MethodAsync().RunSynchronously()
    
    0 讨论(0)
  • 2020-11-21 06:45

    Asynchronous programming does "grow" through the code base. It has been compared to a zombie virus. The best solution is to allow it to grow, but sometimes that's not possible.

    I have written a few types in my Nito.AsyncEx library for dealing with a partially-asynchronous code base. There's no solution that works in every situation, though.

    Solution A

    If you have a simple asynchronous method that doesn't need to synchronize back to its context, then you can use Task.WaitAndUnwrapException:

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

    You do not want to use Task.Wait or Task.Result because they wrap exceptions in AggregateException.

    This solution is only appropriate if MyAsyncMethod does not synchronize back to its context. In other words, every await in MyAsyncMethod should end with ConfigureAwait(false). This means it can't update any UI elements or access the ASP.NET request context.

    Solution B

    If MyAsyncMethod does need to synchronize back to its context, then you may be able to use AsyncContext.RunTask to provide a nested context:

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

    *Update 4/14/2014: In more recent versions of the library the API is as follows:

    var result = AsyncContext.Run(MyAsyncMethod);
    

    (It's OK to use Task.Result in this example because RunTask will propagate Task exceptions).

    The reason you may need AsyncContext.RunTask instead of Task.WaitAndUnwrapException is because of a rather subtle deadlock possibility that happens on WinForms/WPF/SL/ASP.NET:

    1. A synchronous method calls an async method, obtaining a Task.
    2. The synchronous method does a blocking wait on the Task.
    3. The async method uses await without ConfigureAwait.
    4. The Task cannot complete in this situation because it only completes when the async method is finished; the async method cannot complete because it is attempting to schedule its continuation to the SynchronizationContext, and WinForms/WPF/SL/ASP.NET will not allow the continuation to run because the synchronous method is already running in that context.

    This is one reason why it's a good idea to use ConfigureAwait(false) within every async method as much as possible.

    Solution C

    AsyncContext.RunTask won't work in every scenario. For example, if the async method awaits something that requires a UI event to complete, then you'll deadlock even with the nested context. In that case, you could start the async method on the thread pool:

    var task = Task.Run(async () => await MyAsyncMethod());
    var result = task.WaitAndUnwrapException();
    

    However, this solution requires a MyAsyncMethod that will work in the thread pool context. So it can't update UI elements or access the ASP.NET request context. And in that case, you may as well add ConfigureAwait(false) to its await statements, and use solution A.

    Update, 2019-05-01: The current "least-worst practices" are in an MSDN article here.

    0 讨论(0)
提交回复
热议问题