我有一个公共异步无效Foo()方法,我想从同步方法调用。到目前为止,我从MSDN文档中看到的都是通过异步方法调用异步方法,但我的整个程序并不是用异步方法构建的。

这可能吗?

下面是一个从异步方法调用这些方法的例子: 演练:使用Async和Await访问Web (c#和Visual Basic)

现在我正在研究从sync方法调用这些async方法。


当前回答

异步编程的确是通过代码库“增长”的。它被比作僵尸病毒。最好的解决办法是让它成长,但有时这是不可能的。

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

解决方案一

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

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

您不想使用Task。等待或任务。结果,因为它们在AggregateException中包装了异常。

该解决方案仅适用于MyAsyncMethod不同步回上下文的情况。换句话说,MyAsyncMethod中的每个await都应该以ConfigureAwait(false)结束。这意味着它不能更新任何UI元素或访问ASP。NET请求上下文。

解决方案B

如果MyAsyncMethod需要同步回它的上下文,那么你可以使用AsyncContext。RunTask提供一个嵌套的上下文:

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

*更新4/14/2014:在库的最新版本中,API如下:

var result = AsyncContext.Run(MyAsyncMethod);

(可以使用Task。因为RunTask将传播Task异常)。

你可能需要AsyncContext的原因。RunTask而不是Task。WaitAndUnwrapException是由于在WinForms/WPF/SL/ASP上可能会发生相当微妙的死锁。NET:

A synchronous method calls an async method, obtaining a Task. The synchronous method does a blocking wait on the Task. The async method uses await without ConfigureAwait. 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.

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

解决方案C

AsyncContext。RunTask并不适用于所有场景。例如,如果异步方法等待需要UI事件才能完成的事情,那么即使嵌套上下文也会死锁。在这种情况下,你可以在线程池上启动async方法:

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

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

更新日期:2019-05-01:当前的“最不坏的做法”见MSDN的一篇文章。

其他回答

微软构建了一个AsyncHelper(内部)类来作为同步运行Async。源代码如下所示:

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

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

这与调用Task不一样吗?运行(async ()=> await AsyncFunc()).Result?说真的,微软现在不鼓励调用TaskFactory。StartNew,因为它们都是等效的,一个比另一个更可读。

绝对不是。

答案很简单

.Unwrap().GetAwaiter().GetResult() != .Result

首先

是任务。结果与.GetAwaiter.GetResult()?

其次,unwrap()会导致Task的设置不阻塞被包装的任务。

谁会问这个问题呢

这与调用Task不一样吗?运行(async ()=> await AsyncFunc()).GetAwaiter().GetResult()

那就得看情况了。

关于Task.Start(), Task.Run()和Task.Factory.StartNew()的使用

摘录:

的任务。Run使用taskcreateoptions。DenyChildAttach意思是子任务不能附加到父任务它使用TaskScheduler。默认值,这意味着在线程池中运行任务的线程将始终用于运行任务。 startnew使用TaskScheduler。Current表示当前线程的调度程序,它可能是TaskScheduler。违约但并非总是如此。

更多阅读:

指定同步上下文

ASP。NET Core SynchronizationContext

为了更加安全,像这样调用AsyncHelper不是更好吗?RunSync(async () => await AsyncMethod().ConfigureAwait(false));通过这种方式,我们告诉"内部"方法"请不要尝试同步到上层上下文并取消锁"

非常好的观点,就像大多数对象架构问题一样,这要视情况而定。

作为一个扩展方法,你想强迫绝对每个调用,还是让程序员使用该函数配置他们自己的异步调用?我可以看到call 3场景的用例;它很可能不是你在WPF中想要的,当然在大多数情况下是有意义的,但考虑到在ASP中没有上下文。Net Core如果你能保证它是ASP内部的。Net Core,那就无所谓了。

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方法。最大的区别是逻辑流没有中断,这使得读写更容易。

然而,有一个很好的解决方案可以在(几乎:参见评论)任何情况下工作:一个特别的消息泵(SynchronizationContext)。

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

临时消息泵帮助器的代码:

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

异步泵的更详细的描述可在这里。

这是最简单的解决办法。我在网上的某个地方看到它,我不记得在哪里了,但我一直在成功地使用它。它不会死锁调用线程。

    void SynchronousFunction()
    {
        Task.Run(Foo).Wait();
    }

    string SynchronousFunctionReturnsString()
    {
        return Task.Run(Foo).Result;
    }

    string SynchronousFunctionReturnsStringWithParam(int id)
    {
        return Task.Run(() => Foo(id)).Result;
    }

现在,您可以使用源生成器使用同步方法生成器库(nuget)创建方法的同步版本。

使用方法如下:

[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async void FooAsync()

它会生成Foo方法,你可以同步调用。