HttpClient.GetAsync() не връща стойност, когато използвам await/async

+2 гласа
161 прегледа
попитан 2016 юни 7 от Nikoleta.V. (4,090 точки)

Попаднах на странно държание на System.Net.Http.HttpClient в .NET 4.5. Когато await-вам резултата на извикването(например на httpClient.GetAsync(...)), нищо не се връща. 

Това се случва само при определени обстоятелства- когато ползвам новите функционалности на езика async/await и Tasks API-то. Кодът изглежда работи винаги когато използвам само продължения. 

Имам следните GET заявки: 

/api/test1 

/api/test2 

/api/test3 

/api/test4 

/api/test5 <--- никога не завършва 

/api/test6 

Всичките връщат едни и същи данни, освен /api/test5, който не връща нищо. 

На бъг ли съм попаднал или използвам HttpClient класа неправилно? 

Ето малко от кода, който създава проблем 

public class BaseApiController : ApiController 

{ 

    /// <summary> 

    /// Получава данни чрез continuations 

    /// </summary> 

    protected Task<string> Continuations_GetSomeDataAsync() 

    { 

        var httpClient = new HttpClient(); 

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

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

    } 

    /// <summary> 

    /// Взима данни чрез async/await 

    /// </summary> 

    protected async Task<string> AsyncAwait_GetSomeDataAsync() 

    { 

        var httpClient = new HttpClient(); 

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

        return result.Content.Headers.ToString(); 

    } 

} 

public class Test1Controller : BaseApiController 

{ 

    /// <summary> 

    /// Обработва Task-а с Async/Await 

    /// </summary> 

    public async Task<string> Get() 

    { 

        var data = await Continuations_GetSomeDataAsync(); 

        return data; 

    } 

} 

public class Test2Controller : BaseApiController 

{ 

    /// <summary> 

   ///Обработва task-а като блокира нишката докато се изпълни 

     

    /// </summary> 

    public string Get() 

    { 

        var task = Continuations_GetSomeDataAsync(); 

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

        return data; 

    } 

} 

public class Test3Controller : BaseApiController 

{ 

    /// <summary> 

   /// Подава task-а обратно на контролера 

    /// </summary> 

    public Task<string> Get() 

    { 

        return Continuations_GetSomeDataAsync(); 

    } 

} 

public class Test4Controller : BaseApiController 

{ 

    /// <summary> 

    /// Обработва task-а с async/await 

    /// </summary> 

    public async Task<string> Get() 

    { 

        var data = await AsyncAwait_GetSomeDataAsync(); 

        return data; 

    } 

} 

public class Test5Controller : BaseApiController 

{ 

    /// <summary> 

    /// Обработва task-а като блокира нишката докато се изпълни 

    /// </summary> 

    public string Get() 

    { 

        var task = AsyncAwait_GetSomeDataAsync(); 

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

        return data; 

    } 

} 

public class Test6Controller : BaseApiController 

{ 

    /// <summary> 

    /// Подава task-а обратно на контролера 

    /// </summary> 

    public Task<string> Get() 

    { 

        return AsyncAwait_GetSomeDataAsync(); 

    } 

} 

2 отговори

+1 глас
отговорени 2016 юни 8 от Viktor.Ivanov. (1,550 точки)
избран 2016 юни 8 от Nikoleta.V.
 
Най-добър отговор

Ползваш Api-то неправилно.

В ASP.NET само една нишка може да обработва определена заявка в даден момент. Можеш да правиш паралелна обработка ако е необходимо(взимаш допълнителни нишки от thread pool-а), но само една нишка може да работи в контекста на заявката,допълнителните нишки нямат достъп до него.

Това се оправлява от SynchronizationContext:

msdn.microsoft.com/en-us/magazine/gg598924.aspx

По подразбиране, когато 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 awaitва Task-а. Тъй като е незавършен AsyncAwait_GetSomeDataAsync връща незавършен Task.
  • Test5Controller.Get блокира текущата нишка докато се изпълни Task-а
  • HTTP отговора идва и Task-а,върнат от HttpClient.GetAsync се завършва
  • AsyncAwait_GetSomeDataAsync опитва да продължи в контекста на ASP.NET заявката. Обаче вече има друга нишка в този контекст: нишката блокирана в Test5Controller.Get.
  • Deadlock

Ето защо другите работят:

  • (test1, test2, and test3): Continuations_GetSomeDataAsync назначава продължението в thread pool-а,извън контекста на ASP.NET заявката.Това позволява Task-а, върнат от Continuations_GetSomeDataAsync, да завърши без да трябва да влиза отново в контекста на заявката.
  • (test4 and test6): Тъй като Task-а await-нат, нишката на ASP.NET заявката не се блокира. Това позволява на AsyncAwait_GetSomeDataAsync да ползва контекста на ASP.NET заявката когато е готов да продължи.

Ето и някои добри практики:

  1. В твоята „библиотека“ с async методи, използвай ConfigureAwait(false)  когато е възможно. В твоя случай, трябва да промениш AsyncAwait_GetSomeDataAsync на  var result = await httpClient.GetAsync("http://google.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Не блокирай на Task-овете. Надолу са все асинхронни. Иначе казано, ползвай await вместо GetResult (Task.Result и Task.Wait трябва да се заменят с await).

По този начин ще извлечеш и двете ползи: продължението се пуска на обикновена нишка от thread pool-а, която не трябва да влиза в контекста на заявката и контролерът сам по себе си е асинхронен(не блокира нишката на заявката.)

Ако ти се чете на английски:

0 гласа
отговорени 2016 юни 8 от pyo (180 точки)
Не трябва ли да извикаш .Result накрая ?
...