我正在使用客户端库来访问第三方API。该库是由Swagger文档中的NSwagStudio生成的。
我正在处理的应用程序在其所有调用中完全同步,并且将其更新为异步超出了我正在处理的范围。
当我从单元测试中测试客户端库时,它工作正常。当我尝试从ASP.Net应用程序中调用它时,我收到以下错误:
CancellationTokenSource已被处理。
我已将客户端库提炼为演示问题的基本要素,我选择了提供同步方法和异步的选项:
public class ClientApi
{
private readonly HttpClient _httpClient;
public ClientApi(HttpClient httpClient)
{
_httpClient = httpClient;
}
public string BaseUrl { get; set; }
public object Get()
{
return Task.Run(async () => await GetAsync(CancellationToken.None)).GetAwaiter().GetResult();
}
/// <returns>OK</returns>
/// <param name="cancellationToken">
/// A cancellation token that can be used by other objects or threads to receive notice of
/// cancellation.
/// </param>
public async Task<string> GetAsync(CancellationToken cancellationToken)
{
var client_ = _httpClient;
try
{
using (var request_ = new HttpRequestMessage())
{
request_.Method = new HttpMethod("GET");
request_.RequestUri = new System.Uri(BaseUrl, System.UriKind.RelativeOrAbsolute);
var response_ = await client_.SendAsync(
request_,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken
).ConfigureAwait(false);
try
{
// Exception occurs on following line
var responseData_ = response_.Content == null
? null
: await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
return responseData_;
}
finally
{
response_?.Dispose();
}
}
}
finally { }
}
}
这是调用它的代码:
protected void OnClick(object sender, EventArgs e)
{
var httpClient = new HttpClient();
var client = new ClientApi(httpClient)
{
BaseUrl = "https://www.google.com"
};
var html = client.Get();
}
调用它的代码只是一个带有按钮的asp.net页面,按钮事件运行的代码与通过的单元测试相同。
当我比较调试器中的运行时:来自单元测试,response_.Content对象没有取消令牌,但是当从asp.net运行时它确实如此。实际上它们几乎看起来是不同的对象,尽管事实上GetType()将它们都报告为System.Net.Http.StreamContent。从反编译该类,它没有_cancellationtoken属性,所以调试器从哪里获取它?
我猜测我的asp.net网络应用程序的http请求有它自己的令牌和源代码,它以某种方式被HttpClient使用。但是,客户端正在等待所有异步调用以同步获取结果,所以我不明白底层CTS是如何处理的,因为我们尚未从调用客户端库返回。
谁能理解正在发生的事情并且有解决方案吗?
首先,您应该重新考虑重写您的客户端应用程序,以便您可以一直实现异步。
“始终异步”意味着您不应该在不仔细考虑后果的情况下混合使用同步和异步代码。特别是,通过调用Task.Wait或Task.Result来阻止异步代码通常是个坏主意。
取自this伟大的向导。
基本上,通过运行异步代码同步,您总是会做错事。
但是如果你真的需要一个解决方案,首先要将一次性对象包装在using语句中,而不是手动处理它们。这是ClientApi类的简化解决方案,它可以满足您的需求(但它可能会死锁)。代码与此answer中的代码基本相同。
public class ClientApi
{
public object Get(string url)
{
using (var client = new HttpClient())
{
var response = client.GetAsync(url).Result;
if (response.IsSuccessStatusCode)
{
var responseContent = response.Content;
return responseContent.ReadAsStringAsync().Result;
}
}
}
}
阅读更多关于死锁here的信息