不使用等待Task.Delay(x)(x> 0)使UI线程冻结

时间:2016-04-29 12:58:41

标签: c# wpf multithreading asynchronous

我有这个函数强制GUID计算ID(GUID是ID的哈希计算,所以逆转过程除了通过2 ^ 32-1强制执行之外别无他法(是的,那个&#39) ; s)可能的ID。

我使用await Task.Delay(1);来刷新" UI线程所以一切看起来都很自然,但这会使过程变慢,但是如果我使用await Task.Delay(0);整个应用程序会冻结,我不会知道搜索的当前进度(显示为进度条的2 IProgress

    async public Task<Dictionary<string, string>> GetSteamIds(Dictionary<string,string> GUIDs, IProgress<UInt32> progress, IProgress<double> progress2)
    {
        List<string> Progress = new List<string>();
        foreach(string GUID in GUIDs.Keys)
        {
            Progress.Add(GUID);
        }
        for (;Iteration <= UInt32.MaxValue;Iteration++)
        {
            await Task.Delay(0);
            string guid = CalculateGUID(MinAcc+Iteration);
            if(GUIDs.ContainsKey(guid))
            {
                GUIDs[guid] = Convert.ToString((Int64)(MinAcc + Iteration));
            }

            progress.Report(Iteration);
            progress2.Report((1.0 * Iteration / UInt32.MaxValue * 100));

            if(Progress.Count==0)
            {
                if (progress2 != null)
                {
                    progress2.Report(100);
                }
                break;
            }
        }

        return GUIDs;
    }

我不知道如何减轻这种情况,即使我的整个方法都是异步的,并且调用是异步的,它仍然位于&#34; UI线程&#34;而且我不知道如何解决这个问题。

当然,我很欣赏一个有效的答案,但我也很欣赏这一切如何运作的详细说明。我已经阅读了很多文章,但没有一个人能够以某种方式向我解释,以便我了解所有内容。

4 个答案:

答案 0 :(得分:4)

async关键字添加到方法实际上并不会使方法异步,这只是意味着您可以在方法内部使用await关键字。您已经编写了一个完全同步的CPU绑定方法,只是将方法标记为async,使其仍然只是一个同步CPU绑定方法,并且从UI线程调用同步CPU绑定方法将阻止UI线程,阻止其他UI操作发生。

添加await Task.Delay(1)使得你的方法仍然在UI线程中完成所有长时间运行的CPU绑定工作,它只是偶尔停止一次移动到行尾,允许其他操作跑步。这不仅会减慢您的工作速度,还意味着UI线程在绝大多数情况下无法执行其他UI操作,因为此过程耗费了大量时间。

解决方案是根本不在UI线程中完成长时间运行的CPU绑定工作,并将该工作卸载到另一个线程。

由于您的此方法不是实际异步,因此您应该从方法中删除async关键字(并相应地更改返回类型)。当您调用此方法时,如果您碰巧需要从UI线程执行此操作,只需将其包装在对Task.Run的调用中,然后将其卸载到线程池线程中。如果该UI操作在完成结果后需要工作,则该UI操作可以await Task.Run的结果, >方法应该标记为async,因为它确实是异步地进行工作。

答案 1 :(得分:3)

不要在UI线程上运行任务。

您可以这样称呼它:

var myTask = Task.Run(() => GetSteamIds(...));

并在某处保留对myTask的引用,以便您可以测试myTask.IsCompleted以查看它何时完成。

或者,您可以使用ContinueWith在任务完成时自动运行其他一些代码。例如:

var myTask = Task.Run(() => GetSteamIds(...)).ContinueWith(() => {
    //do something here now that the task is done
  }, TaskScheduler.FromCurrentSynchronizationContext());

TaskScheduler.FromCurrentSynchronizationContext()是为了确保延续代码在UI线程上运行(因为您可能希望使用结果更新UI)。

使用这种方法,您的方法根本不需要异步(它不需要返回Task&lt;&gt;)。

或按照评论中的建议使用BackgroundWorker

答案 2 :(得分:3)

您的GetSteamIds方法完全是同步的并且受CPU限制,因此它应该具有同步签名:

public Dictionary<string, string> GetSteamIds(Dictionary<string,string> GUIDs, IProgress<UInt32> progress, IProgress<double> progress2);

现在,当您想要从UI线程调用此(同步,CPU绑定)方法时,使用await Task.Run在线程池线程上运行它并(异步)获取方法的结果:

var results = await Task.Run(() => GetSteamIds(guids, progress, progress2));

如果您正在使用Progress<T>,那么它将负责处理UI线程上的进度更新;没有必要使用笨拙的DispatcherControl.Invoke

另一方面,如果使用非常紧凑的CPU绑定循环,您可能会发现自己的进度报告会降低用户界面的速度并降低用户体验。在这种情况下,请考虑使用我的ObservableProgressIProgress<T>实施,以限制进度更新。

答案 3 :(得分:-1)

当你调用await Task.Delay(1)时,它实际上会返回对UI线程的访问权限,因为可能问题不在你正在调用的方法中,而是在方法的调用中。

所以你应该删除方法中的Task.Delay()并告诉我们方法的调用,以便我们可以帮助你进一步