我正在更新一个 C# WPF 项目,该项目之前使用 WCF 服务,现在调用基于 REST 的 API。
以下代码是 REST API 调用的典型示例:
public static async Task<List<KBase>> GetKbase(int shopID)
{
// set request
string uri = ROOT_URI + "kbase/" + shopID.ToString();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token.token);
// get
HttpResponseMessage response = await http_client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
string result = response.Content.ReadAsStringAsync().Result;
List<KBase> kb = JsonConvert.DeserializeObject<List<KBase>>(result);
return kb;
}
else
{
string result = response.Content.ReadAsStringAsync().Result;
gFunc.AddLogEntry("Get Kbase: " + result);
}
return null;
}
如果我使用以下代码调用上述 API 方法,一切都会按预期发生。消息框显示 true。
private async void btnTest_MouseUp(object sender, MouseButtonEventArgs e)
{
List<Models.API.KBase> kb = await apiSamadhi.GetKbase(1);
bool is_data = !(kb == null);
MessageBox.Show(is_data.ToString());
}
但是,程序的许多部分我都使用后台工作人员来保持主 UI 线程的响应能力。我创建后台工作者如下:
BackgroundWorker bgw;
List<Models.API.KBase> kb;
private void InitBGW()
{
bgw = new BackgroundWorker();
bgw.DoWork += bgw_DoWork;
bgw.RunWorkerCompleted += bgw_WorkCompleted;
}
以下代码显示了 DoWork 和 WorkCompleted 方法:
private async void bgw_DoWork(object sender, DoWorkEventArgs e)
{
kbase = await apiSamadhi.GetKbase(1);
}
private void bgw_WorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
bool is_data = !(kbase == null);
MessageBox.Show(is_data.ToString());
System.Threading.Thread.Sleep(1000);
is_data = !(kbase == null);
MessageBox.Show(is_data.ToString());
}
当 bgw_WorkCompleted 运行时,kbase 为 null,第一个消息框显示 false。然后,在线程休眠一秒钟后,kbase不为空,第二个消息框显示true。
这不是我所期望的行为。就像 bgw_DoWork 中的 await 不起作用一样。为什么 bgw_WorkCompleted 在对 API 的“等待”调用完成之前运行?
我错过了什么或者做了什么愚蠢的事情吗?我需要能够在非 UI 线程上运行“等待”API 调用/任务,并能够在任务完成时更新 UI。
尽管遵循了建议的链接并阅读了有关后台工作人员、异步和等待的更多信息,但我仍然在努力寻找可行的解决方案。
考虑到后台工作人员已经老了,不建议与 Async 和 Await 一起使用,我尝试使用 Task.Run 方法。但是,在到达方法末尾之前,所谓的“等待”任务仍未完成。
在下面的代码中,id是静态通用帮助器类中的全局int变量。
static public async void ReloadData()
{
id = 0;
await Task.Run(() => InitialiseDaysheet());
MessageBox.Show(id.ToString());
await Task.Run(() => LoadDaysheet());
MessageBox.Show(id.ToString());
// update ui after loading data
}
当上述方法运行时,第一个消息框显示“0”,尽管 InitialiseDaysheet() 末尾的代码分配了值 50。第二个消息框显示“50”,但 LoadDaysheet() 末尾分配了值为 100。
InitialiseDaysheet() 和 LoadDaysheet 有很多代码,但每个方法内都有各种 API 调用,如下所示。
static private async void InitialiseDaysheet()
{
// code
kb = await apiSamadhi.GetKbase(1);
// code
}
此外,添加如下所示的ConfigureAwait(false)也没有什么区别。
static private async void InitialiseDaysheet()
{
// code
kb = await apiSamadhi.GetKbase(1).ConfigureAwait(false);
// code
}
BackgroundWorker
希望 DoWork
处理程序阻塞,但在 async void
的情况下,它会在实际异步操作完成并调用 WorkCompleted
处理程序之前退出 - 这里真正的工作只是 starting 异步操作,而不是等待它完成。 BackgroundWorker
与 async
/await
配合不佳。
你可以完全放弃
BackgroundWorker
,这样的事情应该可以解决问题:
private async void InitBGW()
{
var kbase = await Task.Run(async () =>
await apiSamadhi
.GetKbase(1)
.ConfigureAwait(false));
var is_data = kbase is not null;
MessageBox.Show(is_data.ToString());
}
此外,由于我们这里有一个 GUI 应用程序,因此它会有一个
SynchronizationContext
,这意味着所有非 UI 相关代码都应该使用 ConfigureAwait(false)
以避免返回 UI 线程:
await http_client.SendAsync(request).ConfigureAwait(false);
等待后与UI交互时不需要添加
ConfigureAwait
:
await Task.Run(/* ... */); // no ConfigureAwait() here - get back on UI thread after
MessageBox.Show("good, back to UI thread");
正如有人在评论中提到的,当您已经在
Task<T>.Result
方法中时,没有理由调用 async
:
string result = await response.Content
.ReadAsStringAsync()
.ConfigureAwait(false);
如果忘记了
ConfigureAwait
,您可以返回 UI 线程,并且 .Result
调用将简单地阻止它。
最后一点 - 您并不总是需要将所有内容包装在
Task.Run()
中 - 仅当异步操作具有长时间执行的同步前缀时,这里可能不是这种情况 - 您可以简单地使用
var data = await apiSamadhi.GetKbase(1);
如果您小心使用
ConfigureAwait()
并且不要在 UI 线程上调用 Task<T>.Result
。
将方法声明为
async void
明确声明它不能被等待 - 您需要一些可等待的东西,例如 Task
,以便能够跟踪方法完成情况:
static private async Task InitialiseDaysheet()
{
// code
kb = await apiSamadhi.GetKbase(1);
// code
}
如前所述,
Task.Run()
不是必需的 - 它用于更紧密地模仿 BackgroundWorker
的行为。
// this one might also be better with changing to async Task,
// but hard to tell from the provided example, depends on usage
static public async void ReloadData()
{
id = 0;
await InitialiseDaysheet();
MessageBox.Show(id.ToString());
await LoadDaysheet();
MessageBox.Show(id.ToString());
}
async void
通常与事件处理程序一起使用,因为 async
不会更改方法签名:
// same signature as without async, but now we can await
// and still be a valid button click handler
async void OnButtonClick(object? sender, EventArgs e)
{
try
{
await MyAsyncCall();
}
catch(Exception exc)
{
MessageBox.Show(exc.Message);
// if we dont handle exceptions in async void - nobody will
// because nobody can await this call - it looks like a normal
// sync method to the external observer
}
}
并且
ConfigureAwait(false)
与等待调用的能力无关 - 它只影响任务继续运行的线程的选择:
async void A()
{
System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
await SomeAsyncMethod();
System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
// back to UI thread if A() was called from UI thread
}
async void B()
{
System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
await SomeAsyncMethod().ConfigureAwait(false);
System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
// continue on a thread pool thread, don't go back to UI thread
}