使用 ContinueWiths 链更新 UI

Update UI with chains of ContinueWiths

我尝试使用多个 ContinueWiths 来更新 UI。所以我希望每个 ContinueWith Task 都会单独更新 UI,但真正发生的是,他们互相等待并立即更新 UI。因此,即使每个 ContinueWith 运行 都准时,但 UI 仅在最后一个完成时更新。

Thread.Sleep 表示此上下文中的工作。

答案:StartNew is Dangerous by Stephen Cleary。博客的最后一部分 post 几乎使用了与我相同的示例。

我编辑了 post 并排回答我自己的问题。

编辑:在 Test1() 中,StartNew 将在线程池上 运行,因为没有设置任何特定的 TaskScheduler。所以'Work'会在线程池上运行,但是'More Work'和'stop'会在GUI线程上运行,因为有一个集合上下文。

exp:

    private void Test1()
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Start");

        Task task = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(1000);
                Task.Factory.StartNew(() =>
                {
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Work");
                }, CancellationToken.None, TaskCreationOptions.None, context);
            })
            .ContinueWith(_ =>
            {
                Thread.Sleep(2000);
                listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - More Work");
            }, context)
            .ContinueWith(_ =>
                {
                    Thread.Sleep(1000);
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - stop");
                }, context);
    }

编辑:但是,如果线程池上有 'Work' 运行s,为什么结果会有延迟?因为我错了。当我第二次或第 n 次 运行 测试应用程序时,您在下面看到的内容只是真实的。

如果您 运行 这会发生以下情况:

13:32:00 - Start
--- 4 seconds later ---
13:32:01 - Work
13:32:03 - More Work
13:32:04 - stop

如果我运行第一次调试,它看起来像这样:

13:32:00 - Start
--- 1 second later ---
13:32:01 - Work
--- 3 seconds later ---
13:32:03 - More Work
13:32:04 - stop

编辑:我想,如果我第一次 运行 Test1(),'Work' 确实在线程池上 运行s。但是第二次 TaskScheduler.FromCurrentSynchronizationContext() 也将应用于 'Work',因此它将在 GUI 线程上 运行s。那些 Thread.Sleep 确实在 GUI 线程上 运行ing,我无法移动表格。

所以我的第一个问题是,为什么 UI 没有单独更新?因为这是我所期待的。

如果我从每个 ContinueWith 开始一个单独的任务,我会得到想要的结果(单独 UI 更新),但这看起来 wrong/ugly/complicated 我觉得我没有使用ContinueWith 正确(用于 UI 更新)。

编辑:之所以可行,是因为我没有将 'context' 与 ContinueWiths 一起使用,因此它们将 运行 在线程池上,而不是锁定向上UI。新创建的任务将在 UI 上 运行,因为它们设置了上下文。

    private void Test2()
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Start");

        Task task = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(1000);
                Task.Factory.StartNew(() =>
                {
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Short Work");
                }, Task.Factory.CancellationToken, TaskCreationOptions.None, context);

            })
            .ContinueWith(_ =>
            {
                Thread.Sleep(2000);
                Task.Factory.StartNew(() =>
                {
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Long Work");
                }, Task.Factory.CancellationToken, TaskCreationOptions.None, context);
            })
            .ContinueWith(_ =>
                {
                    Thread.Sleep(1000);
                    Task.Factory.StartNew(() =>
                    {
                        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - stop");
                    }, Task.Factory.CancellationToken, TaskCreationOptions.None, context);
                });
    }

结果:

13:32:00 - Start
--- 1 second later ---
13:32:01 - Work
--- 2 seconds later ---
13:32:03 - More Work
--- 1 second later ---
13:32:04 - stop

编辑:tl;dr:小心使用 StartNew 和 TaskScheduler.FromCurrentSynchronizationContext();

正确的方法是使用 async/await 这将始终确保上下文在任务 returns 之后在 UI 线程上继续,即使该任务在另一个线程上执行也是如此.

private async Task Test1()
{

    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Start");

    await Task.Run(HardWorkTakesLongTime);

    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Work");

    await Task.Run(HardWorkTakesLongTime);

    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - More Work");

    await Task.Run(HardWorkTakesLongTime);

    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - stop");
}

void HardWorkTakesLongTime(){
    Thread.Sleep(2000);
}


// From a button click event or something like it
btn_Click(){

    Test1();
}