任务何时与ContinueWith结合使用不能按预期工作

时间:2017-09-28 21:27:30

标签: c# winforms async-await task-parallel-library

我有一个Winform项目,在winform类中我有一个名为DataBindingTasks的属性,就像这样。

// create a task list to determine when tasks have finished during load
protected List<Task> DataBindingTasks = new List<Task>();

我在winform“Load”事件中调用了几个异步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代码正在执行。

这是一个屏幕截图,显示所有任务都未完成。

enter image description here

更新10/29
问题显然比上面的示例代码更深,上面的示例代码并不能完全解释真实场景。

我会尝试更详细地解释,但尽量不要长久。

这是一个Winform应用程序 我们已经创建了一个基础winform“BaseForm”,所有其他winforms将继承 我们已经覆盖了“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);
});

希望所有继承的表单都会将任何“异步”任务添加到此列表中,以便基本表单可以触发“DataBindingTasksComplete”事件,以便他们知道表单已完成加载。

“在问题发生时我们认为”这个问题是“WhenAll()。ContinueWith”没有等到列表上的所有任务完成。
但有人注意到,列表可能已经改变 所以这很有可能发生了什么 有4个“BindSomething”方法被标记为async,都是从Form_Load中调用的 “BindSomething”方法中的第二行左右用于将任务添加到“BaseForm.DataBindingTasks”列表中。
由于这些调用中的每一个都被标记为异步,因此Form_Load继续将所有4调用称为“发射并忘记” 之后,它返回到BaseForm OnLoad,然后查看“DataBindingTasks”列表以查看是否所有任务都已完成。
我最好的猜测是,其中一个“BindSomething”方法正在将其任务添加到列表中,但Base.OnLoad已经开始查看列表了。

我甚至可以在将“BindSomething”方法称为“占位符”之前将4个“假”任务(如线程休眠)添加到列表中,然后在“BindSomething”方法中将“假”任务换成“假”任务真正的“任务。” 这种接缝很乱,很可能会引起其他问题 最可能的解决方法是不使用任务列表/ WhenAll.ContinueWith,而是使用“await”调用加载数据,然后在下一行引发事件。

2 个答案:

答案 0 :(得分:1)

async void方法被称为fire-and-forget,没有办法等待它们,这就是你的代表不能正常等待的原因 - 它根本无法做到。因此,您需要对代码进行一些更改。

更新:@Servy注意到我错过了您的代码中的主要问题,感谢他:

DataBindingTasks.Add(someTask);

此操作不是线程安全!在Add方法的并行调用期间,您只是丢失了一些任务。您需要更改此项:使用lock,使用ConcurrentCollection或使用数据分离:通过不同的索引将任务分配给数组,以便并行任务不会相互交叉。

首先,在这种情况下,您不应使用StartNew,请使用Task.Run,否则使用can met some problems in your app

第二件事是你可以使用Load方法asyncawait,因此您的用户界面不会冻结,您可以切换{{1}的签名正如@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方法。

答案 1 :(得分:-1)

你不需要.ContinueWith()。等待Task.WhenAll(),然后在它之后放置你想要运行的任何代码。另外,改变&#34; void&#34;在方法签名中&#34; async Task&#34;。