簡體   English   中英

HttpClient.GetAsync(...) 在使用 await/async 時永遠不會返回

[英]HttpClient.GetAsync(…) never returns when using await/async

編輯: 這個問題看起來可能是同樣的問題,但沒有回應......

編輯:在測試用例 5 中,任務似乎停留在WaitingForActivation狀態。

我在 .NET 4.5 中使用 System.Net.Http.HttpClient 遇到了一些奇怪的行為 - 其中“等待”調用(例如) httpClient.GetAsync(...)將永遠不會返回。

這僅在使用新的 async/await 語言功能和 Tasks API 時發生在某些情況下 - 代碼似乎總是在僅使用延續時工作。

這是一些重現問題的代碼 - 將其放入 Visual Studio 11 中的新“MVC 4 WebApi 項目”中以公開以下 GET 端點:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

這里的每個端點都返回相同的數據(來自 stackoverflow.com 的響應標頭),除了永遠不會完成的/api/test5

我是否在 HttpClient 類中遇到了錯誤,或者我是否以某種方式濫用了 API?

重現代碼:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}

您正在濫用 API。

情況是這樣的:在 ASP.NET 中,一次只有一個線程可以處理一個請求。 如有必要,您可以進行一些並行處理(從線程池中借用附加線程),但只有一個線程具有請求上下文(附加線程沒有請求上下文)。

這是由 ASP.NET SynchronizationContext管理的

默認情況下,當您await Task ,該方法會在捕獲的SynchronizationContext (或捕獲的TaskScheduler ,如果沒有SynchronizationContext )上恢復。 通常,這正是您想要的:異步控制器操作將await某些東西,當它恢復時,它會以請求上下文恢復。

所以,這就是test5失敗的原因:

  • Test5Controller.Get執行AsyncAwait_GetSomeDataAsync (在 ASP.NET 請求上下文中)。
  • AsyncAwait_GetSomeDataAsync執行HttpClient.GetAsync (在 ASP.NET 請求上下文中)。
  • HTTP 請求發出, HttpClient.GetAsync返回一個未完成的Task
  • AsyncAwait_GetSomeDataAsync等待Task 由於未完成, AsyncAwait_GetSomeDataAsync返回未完成的Task
  • Test5Controller.Get阻塞當前線程,直到該Task完成。
  • HTTP響應進來, HttpClient.GetAsync返回的Task就完成了。
  • AsyncAwait_GetSomeDataAsync嘗試在 ASP.NET 請求上下文中恢復。 但是,該上下文中已經有一個線程:在Test5Controller.Get阻塞的線程。
  • 僵局。

這就是其他人工作的原因:

  • test1test2test3 ): Continuations_GetSomeDataAsync將延續安排到 ASP.NET 請求上下文之外的線程池。 這允許Continuations_GetSomeDataAsync返回的Task完成,而無需重新輸入請求上下文。
  • test4test6 ):由於Task等待,ASP.NET 請求線程不會被阻塞。 這允許AsyncAwait_GetSomeDataAsync在准備好繼續時使用 ASP.NET 請求上下文。

以下是最佳實踐:

  1. 在您的“庫” async方法中,盡可能使用ConfigureAwait(false) 在您的情況下,這會將AsyncAwait_GetSomeDataAsync更改為var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要阻塞Task 它一直是async的。 換句話說,使用await而不是GetResultTask.ResultTask.Wait也應該替換為await )。

這樣,您將獲得兩個好處:延續( AsyncAwait_GetSomeDataAsync方法的其余部分)在基本線程池線程上運行,該線程池線程不必進入 ASP.NET 請求上下文; 並且控制器本身是async (它不會阻塞請求線程)。

更多信息:

2012 年 7 月 13 日更新:將此答案合並到博客文章中

編輯:通常盡量避免執行以下操作,除非作為避免死鎖的最后努力。 閱讀 Stephen Cleary 的第一條評論。

這里快速修復。 而不是寫:

Task tsk = AsyncOperation();
tsk.Wait();

嘗試:

Task.Run(() => AsyncOperation()).Wait();

或者,如果您需要結果:

var result = Task.Run(() => AsyncOperation()).Result;

從源(編輯以匹配上面的例子):

AsyncOperation 現在將在沒有 SynchronizationContext 的 ThreadPool 上調用,並且 AsyncOperation 內部使用的延續不會被強制返回到調用線程。

對我來說,這看起來像是一個可用的選項,因為我沒有辦法讓它一直異步(我更喜歡)。

從來源:

確保 FooAsync 方法中的 await 找不到要封送回的上下文。 最簡單的方法是從 ThreadPool 調用異步工作,例如通過將調用包裝在 Task.Run 中,例如

int Sync() { return Task.Run(() => Library.FooAsync()).Result; }

現在將在 ThreadPool 上調用 FooAsync,其中不會有 SynchronizationContext,並且在 FooAsync 內部使用的延續不會被強制返回到調用 Sync() 的線程。

由於您正在使用.Result.Waitawait這最終會導致您的代碼死鎖

您可以在async方法中使用ConfigureAwait(false)防止死鎖

像這樣:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

您可以盡可能將ConfigureAwait(false)用於 Don't Block Async Code 。

這兩個學校並不是真的排斥。

這是您只需要使用的場景

   Task.Run(() => AsyncOperation()).Wait(); 

或類似的東西

   AsyncContext.Run(AsyncOperation);

我有一個位於數據庫事務屬性下的 MVC 操作。 這個想法(可能)是在出現問題時回滾操作中所做的一切。 這不允許上下文切換,否則事務回滾或提交本身就會失敗。

我需要的庫是異步的,因為它應該運行異步。

唯一的選擇。 將其作為普通同步調用運行。

我只是對每個人說自己的。

我將把它放在這里更多的是為了完整性,而不是與 OP 直接相關。 我花了將近一天的時間調試一個HttpClient請求,想知道為什么我從來沒有得到響應。

最后發現我忘記在調用堆棧中await async調用了。

感覺就像缺少一個分號一樣好。

在我的情況下,'await' 在執行請求時由於異常而從未完成,例如服務器沒有響應等。用 try..catch 包圍它以識別發生了什么,它也會優雅地完成你的 'await'。

public async Task<Stuff> GetStuff(string id)
{
    string path = $"/api/v2/stuff/{id}";
    try
    {
        HttpResponseMessage response = await client.GetAsync(path);
        if (response.StatusCode == HttpStatusCode.OK)
        {
            string json = await response.Content.ReadAsStringAsync();
            return JsonUtility.FromJson<Stuff>(json);
        }
        else
        {
            Debug.LogError($"Could not retrieve stuff {id}");
        }
    }
    catch (Exception exception)
    {
        Debug.LogError($"Exception when retrieving stuff {exception}");
    }
    return null;
}

我在這看:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

和這里:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

並看到:

此類型及其成員旨在供編譯器使用。

考慮到await版本是有效的,並且是“正確”的做事方式,你真的需要回答這個問題嗎?

我的投票是: 濫用API

我正在使用許多等待,所以我沒有得到響應,我轉換為同步調用它開始工作

            using (var client = new HttpClient())
            using (var request = new HttpRequestMessage())
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                request.Method = HttpMethod.Get;
                request.RequestUri = new Uri(URL);
                var response = client.GetAsync(URL).Result;
                response.EnsureSuccessStatusCode();
                string responseBody = response.Content.ReadAsStringAsync().Result;
               

暫無
暫無

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

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