簡體   English   中英

如何從C#中的同步方法調用異步方法?

[英]How to call asynchronous method from synchronous method in C#?

我有一個public async void Foo()方法,我想從同步方法調用它。 到目前為止,我從 MSDN 文檔中看到的都是通過異步方法調用異步方法,但我的整個程序並不是使用異步方法構建的。

這可能嗎?

下面是從異步方法調用這些方法的一個示例:
演練:使用 Async 和 Await(C# 和 Visual Basic)訪問 Web

現在我正在研究從同步方法調用這些異步方法。

異步編程確實通過代碼庫“增長”。 它被比作僵屍病毒 最好的解決方案是讓它增長,但有時這是不可能的。

我在Nito.AsyncEx庫中編寫了一些類型來處理部分異步代碼庫。 但是,沒有在所有情況下都有效的解決方案。

方案一

如果您有一個不需要同步回其上下文的簡單異步方法,那么您可以使用Task.WaitAndUnwrapException

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

希望使用Task.WaitTask.Result因為包裝在異常AggregateException

此解決方案僅適用於MyAsyncMethod不同步回其上下文的情況。 換句話說, 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方法使用await而沒有ConfigureAwait
  4. 在這種情況下, Task無法完成,因為它只有在async方法完成時才完成; async方法無法完成,因為它試圖將其延續安排到SynchronizationContext ,並且 WinForms/WPF/SL/ASP.NET 將不允許繼續運行,因為同步方法已經在該上下文中運行。

這就是為什么在每個async方法中盡可能多地使用ConfigureAwait(false)是個好主意的原因之一。

方案C

AsyncContext.RunTask不會在每種情況下都有效。 例如,如果async方法等待一些需要 UI 事件才能完成的事情,那么即使使用嵌套上下文,您也會死鎖。 在這種情況下,您可以在線程池上啟動async方法:

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

但是,此解決方案需要一個將在線程池上下文中工作的MyAsyncMethod 因此它無法更新 UI 元素或訪問 ASP.NET 請求上下文。 在這種情況下,您也可以將ConfigureAwait(false)添加到其await語句中,並使用解決方案 A。

更新,2019 年 5 月 1 日:當前的“最壞做法”在MSDN 文章中

添加一個最終解決我的問題的解決方案,希望可以節省某人的時間。

首先閱讀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;
    }
}

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

這不是和調用 Task.Run(async ()=> await AsyncFunc()).Result 一樣嗎? AFAIK,微軟現在不鼓勵調用 TaskFactory.StartNew,因為它們都是等價的,並且一個比另一個更易讀。

絕對不。

簡單的答案是

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

首先關閉

Task.Result 和 .GetAwaiter.GetResult() 一樣嗎?

其次.Unwrap()使 Task 的設置不會阻塞包裝的任務。

這應該導致任何人問

這不是和調用 Task.Run(async ()=> await AsyncFunc()).GetAwaiter().GetResult() 一樣嗎

這將是一個It Depends

關於 Task.Start() 、 Task.Run() 和 Task.Factory.StartNew() 的使用

摘抄:

Task.Run 使用 TaskCreationOptions.DenyChildAttach 這意味着子任務不能附加到父級,它使用 TaskScheduler.Default 這意味着在線程池上運行任務的將始終用於運行任務。

Task.Factory.StartNew 使用 TaskScheduler.Current 表示當前線程的調度程序,它可能是TaskScheduler.Default 但不總是

補充閱讀:

指定同步上下文

ASP.NET Core 同步上下文

為了額外的安全,像這樣調用它不是更好AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false)); 通過這種方式,我們告訴“內部”方法“請不要嘗試同步到上層上下文並解除鎖定”

非常重要的一點,正如大多數對象架構問題一樣,這取決於

作為一個擴展方法你想使用的功能配置給力,對於絕對每一個電話,或者你讓程序員認為自己的異步調用? 我可以看到調用三個場景的用例; 它很可能不是您在 WPF 中想要的東西,在大多數情況下當然是有意義的,但是考慮到 ASP.Net Core 中沒有Context,如果您可以保證它是 ASP.Net Core 的內部,那么這無關緊要.

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-主題訂閱

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 方法。 最大的區別是邏輯流不會被中斷,這使得讀寫變得更加容易。

我不是 100% 確定,但我相信本博客中描述的技術應該適用於許多情況:

因此,如果您想直接調用此傳播邏輯,則可以使用task.GetAwaiter().GetResult()

然而,有一個很好的解決方案(幾乎:見評論)適用於每種情況:一個特別的消息泵(SynchronizationContext)。

調用線程將按預期被阻塞,同時仍確保從異步函數調用的所有延續不會死鎖,因為它們將被編組到在調用線程上運行的臨時 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(...));

此處提供了異步泵的更詳細說明。

對於任何關注這個問題的人......

如果您查看Microsoft.VisualStudio.Services.WebApi則有一個名為TaskExtensions的類。 在該類中,您將看到靜態擴展方法Task.SyncResult() ,它就像完全阻塞線程直到任務返回。

它在內部調用task.GetAwaiter().GetResult()這非常簡單,但是它被重載以處理任何返回TaskTask<T>Task<HttpResponseMessage> async方法......語法糖,寶貝......爸爸的愛吃甜食。

看起來...GetAwaiter().GetResult()是在阻塞上下文中執行異步代碼的 MS 官方方法。 對於我的用例來說似乎工作得很好。

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

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

或者使用這個:

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

您可以從同步代碼中調用任何異步方法,也就是說,直到您需要await它們為止,在這種情況下,它們也必須標記為async

正如很多人在這里建議的那樣,您可以在同步方法中對結果任務調用Wait()或 Result ,但最終會在該方法中進行阻塞調用,這有點違背了異步的目的。

如果你真的不能讓你的方法async並且你不想鎖定同步方法,那么你將不得不通過將它作為參數傳遞給任務上的ContinueWith()方法來使用回調方法。

受其他一些答案的啟發,我創建了以下簡單的輔助方法:

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

它們可以按如下方式調用(取決於您是否返回值):

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

請注意,原始問題public async void Foo()中的簽名不正確。 它應該是public async Task Foo()因為你應該為不返回值的異步方法返回 Task not void (是的,有一些罕見的例外)。

Stephen Cleary 的回答;

該方法不應導致死鎖(假設 ProblemMethodAsync 不會將更新發送到 UI 線程或類似的東西)。 它確實假定可以在線程池線程上調用 ProblemMethodAsync,但情況並非總是如此。

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

這是方法;

線程池 Hack 與 Blocking Hack 類似的方法是將異步工作卸載到線程池,然后阻塞生成的任務。 使用此 hack 的代碼類似於圖 7 中所示的代碼。

圖 7 線程池 Hack 的代碼

C#

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return Task.Run(() => GetAsync(id)).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "https://www.example.com/api/values/" + id);
  }
}

對 Task.Run 的調用在線程池線程上執行異步方法。 在這里它將在沒有上下文的情況下運行,從而避免了死鎖。 這種方法的問題之一是異步方法不能依賴於在特定上下文中執行。 因此,它不能使用 UI 元素或 ASP.NET HttpContext.Current。

我知道我來晚了。 但是,如果像我這樣的人想要以一種整潔,簡單的方式解決此問題,而又不必依賴其他庫。

我從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>());

經過數小時嘗試不同的方法,或多或少的成功,這就是我的結局。 在獲得結果時它不會以死鎖結束,它還會獲取並拋出原始異常而不是包裝的異常。

private ReturnType RunSync()
{
  var task = Task.Run(async () => await myMethodAsync(agency));
  if (task.IsFaulted && task.Exception != null)
  {
    throw task.Exception;
  }

  return task.Result;
}

這是最簡單的解決方案。 我在網上的某個地方看到過,不記得在哪里了,但我一直在成功使用它。 它不會死鎖調用線程。

    void Synchronous Function()
    {
        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方法。

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

希望這可以幫助!

   //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;

如果你想運行它同步

MethodAsync().RunSynchronously()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM