任务 WhenAll 与 ContinueWith 结合不能按预期工作
Task WhenAll combined with ContinueWith not work as expected
我有一个 Winform 项目,在 winform class 里面我有一个 属性 像这样调用 DataBindingTasks。
// create a task list to determine when tasks have finished during load
protected List<Task> DataBindingTasks = new List<Task>();
我在 winform "Load" 事件中调用了几个 async void 方法,它们都类似于以下内容。
private async void BindSomething(int millSecToWait)
{
var someTask = Task.Factory.StartNew(() =>
{
// do some work
System.Threading.Thread.Sleep(millSecToWait);
// return some list for binding
return new List<int>();
});
// add the task to the task list
DataBindingTasks.Add(someTask);
// wait until data has loaded
var listToBind = await someTask;
// bind the data to a grid
}
我在加载时调用 BindSomething
方法。
我说方法是因为有几种绑定类型的方法在加载时被调用。
private void Form_Load(object sender, EventArgs e)
{
// async bind something and let UI continue
// fire and forget
BindSomething(5000);
BindSomething(8000);
BindSomething(2000);
BindSomething(2000);
// code to execute when all data binding tasks have completed
Task.WhenAll(DataBindingTasks).ContinueWith((x) =>
{
// Do something after all async binding tasks have completed
});
}
除了 ContinueWith
代码正在执行,即使所有任务尚未完成。
这是显示所有任务未完成的屏幕截图。
10 月 29 日更新
问题显然比上面的示例代码更深,上面的示例代码并没有完全解释真实的场景。
我会尝试更详细地解释,但尽量不要太长。
这是一个 Winform 应用程序。
我们已经创建了一个基础 winform "BaseForm",所有其他 winform 都将继承它。
我们已经覆盖了 "BaseForm" 中的 "OnLoad" 事件,这样我们就可以调用一个新方法,所有继承的表单都会调用 "LoadData".
由于 "LoadData" 可以进行异步方法调用,因此基本表单需要知道 "LoadData" 方法何时完成。
所以在基本形式中有以下一些:
protected List<Task> DataBindingTasks = new List<Task>();
public event EventHandler DataBindingTasksComplete;
protected void OnDataBindingTasksComplete(EventArgs e)
{
if (DataBindingTasksComplete != null)
{
DataBindingTasksComplete(this, e);
}
// now clear the list
DataBindingTasks.Clear();
}
// NOTE: this is inside the OnLoad called before base.OnLoad(e)
Task.WhenAll(DataBindingTasks).ContinueWith((x) =>
{
OnDataBindingTasksComplete(EventArgs.Empty);
});
希望所有继承的表单都将其任何 "async" 任务添加到此列表中,以便基本表单可以触发 "DataBindingTasksComplete" 事件,这样他们就会知道表单已完成加载。
问题 "as perceived to us at the time of the issue" 是 "WhenAll().ContinueWith" 没有等到列表中的所有任务都完成。
但是正如有人指出的那样,列表可能已经更改。
所以这里很可能发生了什么。
有 4 个 "BindSomething" 标记为异步的方法全部从 Form_Load
调用
"BindSomething" 方法中的第二行左右用于将任务添加到 "BaseForm.DataBindingTasks" 列表。
由于这些调用中的每一个都被标记为异步,Form_Load 继续调用所有 4 个作为 "fire and forget"。
之后,它 returns 返回到 BaseForm OnLoad,然后查看 "DataBindingTasks" 列表以查看是否所有任务都已完成。
我最好的猜测是 "BindSomething" 方法之一正在将其任务添加到列表中,但 Base.OnLoad 已经开始查看列表。
我可以将 4 个 "fake" 任务(如线程睡眠)添加到列表中,甚至在将 "BindSomething" 方法调用为 "place holders" 之前,然后在 "BindSomething" 方法内部交换用 "real" 个任务完成 "fake" 个任务。
这接缝很乱,很可能会导致其他问题。
最可能的解决方法是不使用任务列表 / WhenAll.ContinueWith 而是使用 "await" 调用加载数据,然后在下一行引发事件。
您不需要 .ContinueWith()。只需等待 Task.WhenAll(),然后将您想要的任何代码放在 运行 之后。另外,将方法签名中的 "void" 更改为 "async Task".
async void
方法被调用为 fire-and-forget
,并且无法等待它们,这就是为什么您的委托没有正确等待的原因 - 它根本无法做到这一点。因此,您需要对代码进行一些更改。
更新:@Servy 指出了我遗漏的代码中的主要问题,感谢他:
DataBindingTasks.Add(someTask);
这个操作不是线程安全的!您只是在并行调用 Add
方法期间丢失了一些任务。您需要更改此设置:通过使用 lock
、使用 ConcurrentCollection
或使用数据分离:通过不同的索引将任务分配给数组,以便并行任务不会相互交叉。
首先,在这种情况下你不应该使用StartNew
,使用Task.Run
,否则你can met some problems in your app.
第二件事是你可以让 Load
方法 async
和 await
它,所以你的 UI 不会冻结,你可以切换签名正如@digimunk 提到的那样,您的 BindSomething
方法变得可等待:
// note that we return the task here
private async Task BindSomething(int millSecToWait)
{
// use Task.Run in this case
var someTask = Task.Run(() =>
{
// Some work
System.Threading.Thread.Sleep(millSecToWait);
// return some list for binding
return new List<int>();
});
DataBindingTasks.Add(someTask);
// wait until data has loaded
var listToBind = await someTask;
// bind the data to a grid
}
// async void for the event handler
private async void Load()
{
// start tasks in fire-and-forget fashion
BindSomething(5000);
BindSomething(8000);
BindSomething(2000);
// code to execute when all data binding tasks have completed
await Task.WhenAll(DataBindingTasks);
// Do something after all binding is complete
}
在这种情况下,您可以安全地 await
Load
方法。
我有一个 Winform 项目,在 winform class 里面我有一个 属性 像这样调用 DataBindingTasks。
// create a task list to determine when tasks have finished during load
protected List<Task> DataBindingTasks = new List<Task>();
我在 winform "Load" 事件中调用了几个 async void 方法,它们都类似于以下内容。
private async void BindSomething(int millSecToWait)
{
var someTask = Task.Factory.StartNew(() =>
{
// do some work
System.Threading.Thread.Sleep(millSecToWait);
// return some list for binding
return new List<int>();
});
// add the task to the task list
DataBindingTasks.Add(someTask);
// wait until data has loaded
var listToBind = await someTask;
// bind the data to a grid
}
我在加载时调用 BindSomething
方法。
我说方法是因为有几种绑定类型的方法在加载时被调用。
private void Form_Load(object sender, EventArgs e)
{
// async bind something and let UI continue
// fire and forget
BindSomething(5000);
BindSomething(8000);
BindSomething(2000);
BindSomething(2000);
// code to execute when all data binding tasks have completed
Task.WhenAll(DataBindingTasks).ContinueWith((x) =>
{
// Do something after all async binding tasks have completed
});
}
除了 ContinueWith
代码正在执行,即使所有任务尚未完成。
这是显示所有任务未完成的屏幕截图。
10 月 29 日更新
问题显然比上面的示例代码更深,上面的示例代码并没有完全解释真实的场景。
我会尝试更详细地解释,但尽量不要太长。
这是一个 Winform 应用程序。
我们已经创建了一个基础 winform "BaseForm",所有其他 winform 都将继承它。
我们已经覆盖了 "BaseForm" 中的 "OnLoad" 事件,这样我们就可以调用一个新方法,所有继承的表单都会调用 "LoadData".
由于 "LoadData" 可以进行异步方法调用,因此基本表单需要知道 "LoadData" 方法何时完成。
所以在基本形式中有以下一些:
protected List<Task> DataBindingTasks = new List<Task>();
public event EventHandler DataBindingTasksComplete;
protected void OnDataBindingTasksComplete(EventArgs e)
{
if (DataBindingTasksComplete != null)
{
DataBindingTasksComplete(this, e);
}
// now clear the list
DataBindingTasks.Clear();
}
// NOTE: this is inside the OnLoad called before base.OnLoad(e)
Task.WhenAll(DataBindingTasks).ContinueWith((x) =>
{
OnDataBindingTasksComplete(EventArgs.Empty);
});
希望所有继承的表单都将其任何 "async" 任务添加到此列表中,以便基本表单可以触发 "DataBindingTasksComplete" 事件,这样他们就会知道表单已完成加载。
问题 "as perceived to us at the time of the issue" 是 "WhenAll().ContinueWith" 没有等到列表中的所有任务都完成。
但是正如有人指出的那样,列表可能已经更改。
所以这里很可能发生了什么。
有 4 个 "BindSomething" 标记为异步的方法全部从 Form_Load
调用
"BindSomething" 方法中的第二行左右用于将任务添加到 "BaseForm.DataBindingTasks" 列表。
由于这些调用中的每一个都被标记为异步,Form_Load 继续调用所有 4 个作为 "fire and forget"。
之后,它 returns 返回到 BaseForm OnLoad,然后查看 "DataBindingTasks" 列表以查看是否所有任务都已完成。
我最好的猜测是 "BindSomething" 方法之一正在将其任务添加到列表中,但 Base.OnLoad 已经开始查看列表。
我可以将 4 个 "fake" 任务(如线程睡眠)添加到列表中,甚至在将 "BindSomething" 方法调用为 "place holders" 之前,然后在 "BindSomething" 方法内部交换用 "real" 个任务完成 "fake" 个任务。
这接缝很乱,很可能会导致其他问题。
最可能的解决方法是不使用任务列表 / WhenAll.ContinueWith 而是使用 "await" 调用加载数据,然后在下一行引发事件。
您不需要 .ContinueWith()。只需等待 Task.WhenAll(),然后将您想要的任何代码放在 运行 之后。另外,将方法签名中的 "void" 更改为 "async Task".
async void
方法被调用为 fire-and-forget
,并且无法等待它们,这就是为什么您的委托没有正确等待的原因 - 它根本无法做到这一点。因此,您需要对代码进行一些更改。
更新:@Servy 指出了我遗漏的代码中的主要问题,感谢他:
DataBindingTasks.Add(someTask);
这个操作不是线程安全的!您只是在并行调用 Add
方法期间丢失了一些任务。您需要更改此设置:通过使用 lock
、使用 ConcurrentCollection
或使用数据分离:通过不同的索引将任务分配给数组,以便并行任务不会相互交叉。
首先,在这种情况下你不应该使用StartNew
,使用Task.Run
,否则你can met some problems in your app.
第二件事是你可以让 Load
方法 async
和 await
它,所以你的 UI 不会冻结,你可以切换签名正如@digimunk 提到的那样,您的 BindSomething
方法变得可等待:
// note that we return the task here
private async Task BindSomething(int millSecToWait)
{
// use Task.Run in this case
var someTask = Task.Run(() =>
{
// Some work
System.Threading.Thread.Sleep(millSecToWait);
// return some list for binding
return new List<int>();
});
DataBindingTasks.Add(someTask);
// wait until data has loaded
var listToBind = await someTask;
// bind the data to a grid
}
// async void for the event handler
private async void Load()
{
// start tasks in fire-and-forget fashion
BindSomething(5000);
BindSomething(8000);
BindSomething(2000);
// code to execute when all data binding tasks have completed
await Task.WhenAll(DataBindingTasks);
// Do something after all binding is complete
}
在这种情况下,您可以安全地 await
Load
方法。