【问题标题】:WPF / XAML: How do I execute threaded processes and prevent the main UI from being busy / freezing?WPF / XAML:如何执行线程进程并防止主 UI 繁忙/冻结?
【发布时间】:2011-02-12 02:31:35
【问题描述】:

我有一个用作自动化 UI 的 XAML 应用程序。整个自动化过程可能需要 20-30 小时才能完全执行,因此我创建了一个 Task 类对象,该对象基本上包装了 Thread 方法(开始/停止/重置)。

但是,当我在 Task 对象下运行自动化方法时,XAML UI 很忙,我无法与其他控件交互,包括切换 Thread.Set() 标志的暂停按钮。

还有一个帖子 Prevent UI from freezing without additional threads

有人推荐了 BackgroundWorker 类,这篇 MSDN 文章提到,当它在 UI 中操作对象时使用它是一个坏主意,我的做法是为了显示状态计数: http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx

对此有什么想法吗?

    private void OnButtonStartAutomationClick(object sender, RoutedEventArgs e)
    {
        btnPauseAutomation.IsEnabled = true;
        Automation.Task AutomationThread = new Automation.Task(RunFullAutomation);
    }

    private void RunFullAutomation()
    {
        // do stuff that can take 20+ hours
        // threaded so I can utilize a pause button (block)
    }

class Task
{
    private ManualResetEvent _shutdownFlag = new ManualResetEvent(false);
    private ManualResetEvent _pauseFlag = new ManualResetEvent(true);
    private Thread _thread;
    private readonly Action _action;

    public Task(Action action)
    {
        _action = action;
    }

    public void Start()
    {
        ThreadStart ts = new ThreadStart(DoDelegatedMethod);
        _thread = new Thread(ts);            
        _thread.Start();
        _thread.Priority = ThreadPriority.Lowest;
    }

    public void Resume()
    {
        _pauseFlag.Set();
    }

    public void Stop()
    {
        _shutdownFlag.Set();
        _pauseFlag.Set();
        _thread.Join();
    }

    private void DoDelegatedMethod()
    {
        do
        {
            _action();
        }
        while (!_shutdownFlag.WaitOne(0));
    }
}

【问题讨论】:

标签: c# multithreading xaml


【解决方案1】:

有人推荐了 BackgroundWorker 类,这篇 MSDN 文章提到,当它在 UI 中操作对象时使用它是一个坏主意,我的做法是为了显示状态计数

BackgroundWorker 实际上非常适合这种情况,因为它是为这种类型的场景设计的。警告是您不应更改 DoWork 内的 UI 元素,而应通过 ReportProgressProgressChanged 事件。

警告存在的原因是“DoWork”在后台线程上执行。如果您从那里设置 UI 元素值,您将得到一个跨线程异常。但是,ReportProgress/ProgressChanged 会自动将调用编组回适合您的 SynchronizationContext

【讨论】:

    【解决方案2】:

    看看 WPF 中的 Dispatcher 对象。您可以并且应该在您的场景中在后台线程上运行长时间运行的任务,BackgroundWorker 是一个很好的方法。当您需要更新 UI 时,您需要验证对 UI 线程的访问权限,如果没有,请使用调度程序在 UI 线程上调用更新方法。

    【讨论】:

      【解决方案3】:

      这里有两个可能的原因:首先,阻塞任务正在阻塞 UI 线程而不是在后台线程上运行,其次,后台线程正在使 UI 线程处于饥饿状态,以至于它永远没有机会响应输入。您需要找出其中的情况。一种粗略的方法是,在您的 Click 处理程序中,Debug.WriteLine 当前线程 ID (Thread.CurrentThread.ManagedThreadId),并在 RunFullAutomation 回调中执行相同操作。

      如果这些打印相同的数字,那么您遇到了第一个问题。 Reed 和 TheZenker 已经为此提供了解决方案。

      如果这些打印不同的数字,那么你已经在一个工作线程上,你有第二个问题。 (BackgroundWorker 可能会让你更优雅地进入工作线程,并有助于更新 UI,但它不会停止饥饿。)在这种情况下,最简单的解决方法可能是在启动工作线程之前设置 _thread.Priority = ThreadPriority.BelowNormal;

      顺便说一句,您的代码似乎从未真正调用过AutomationThread.Start,这意味着甚至没有执行 RunFullAutomation 回调。这只是一个错字吗?

      【讨论】:

        【解决方案4】:

        鉴于 .NET 4 完全支持使用任务并行库在后台异步运行任务,我建议不要推出您自己的任务类 也就是说,您可以执行 Reed 建议的操作并使用理想的 BackgroundWorker,或者如果您更喜欢对任务执行方式的性质进行更多控制,您可以使用 System.Threading.Tasks 中的 Task 类并实现如下所示:

        public partial class MainWindow : Window
        {
            CancellationTokenSource source = new CancellationTokenSource();
            SynchronizationContext context = SynchronizationContext.Current;
            Task task;
            public MainWindow()
            {
                InitializeComponent();
            }
        
            private void DoWork()
            {
                for (int i = 0; i <= 100; i++)
                {
                    Thread.Sleep(500); //simulate long running task
                    if (source.IsCancellationRequested)
                    {
                        context.Send((_) => labelPrg.Content = "Cancelled!!!", null);
                        break;
                    }
                    context.Send((_) => labelPrg.Content = prg.Value = prg.Value + 1, null);
                }
            }
        
            private void Start_Click(object sender, RoutedEventArgs e)
            {
                task = Task.Factory.StartNew(DoWork, source.Token);
            }
        
            private void Cancel_Click(object sender, RoutedEventArgs e)
            {
                source.Cancel();
            }       
        }
        

        DoWork() 中,您使用 WPF SynchronizationContext 并发布消息来更新您需要的 UI wiget。

        该示例有一个进度条和一个标签控件,在 for 循环的每次迭代中都会更新。使用在每次迭代中检查的 CancellationTokenSource 支持取消。

        希望这会有所帮助。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-10-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-05-20
          • 1970-01-01
          相关资源
          最近更新 更多