【问题标题】:Keep UI thread responsive when running long task in windows forms在 Windows 窗体中运行长任务时保持 UI 线程响应
【发布时间】:2018-06-03 11:23:02
【问题描述】:

我正在尝试将大型文本文件读入文本框,并在将文件拖到文本框时保持 ui 响应。

无法按预期工作,Windows 窗体被冻结,似乎只能执行读取文件并将内容附加到文本框的任务。

IDE 抛出了 ContextSwitchDeadLock,但并不是真正的错误。 这是一项长期运行的任务。我已修复它更改异常菜单下的行为。

感谢 JSteward,Peter 将代码更改为此。

运行此任务时如何保持 ui(主线程)响应? 谢谢。

private SynchronizationContext fcontext;

public Form1()
{      
    InitializeComponent();            
    values.DragDrop += values_DragDrop; //<----------- This is a textbox
    fcontext = WindowsFormsSynchronizationContext.Current;
}

// The async callback 
async void values_DragDrop(object sender, DragEventArgs e)
{
    try
    {
        string dropped = ( (string[]) e.Data.GetData(DataFormats.FileDrop))[0];
        if ( dropped.Contains(".csv") || dropped.Contains(".txt"))
        {
                using ( StreamReader sr = File.OpenText(dropped) )
                {
                    string s = String.Empty;
                    while ( ( s = await sr.ReadLineAsync() ) != null )
                    {                                                                
                       values.AppendText(s.Replace(";",""));
                    }
                }                 
         }
     }
  catch (Exception ex) { }
}

【问题讨论】:

  • 您可以只使用...AppendText(await ...ReadLineAsync) 而不是使用File api 的阻塞变体。这样您就不需要手动存储或发布到上下文。
  • 我认为你的意思是它只能应用于 async lamda 表达式。你能更新问题中的代码吗?
  • 您将无法从Task.Run 默认上下文中AppendText。使用async api,您不应该需要Task.Run,但要了解您的拖放可能会在您完成读取文件之前结束。此外,您可能需要分批加载文件以保持 ui 响应并让 Forms 循环轮到它。
  • “问题似乎是加载函数” -- 你认为什么函数是“加载函数”? @JSteward 已经解释说您不需要 Task.Run(),因为您正在使用 async/await 进行阅读。实际回答您的问题是不可能的,因为您没有提供一个很好的minimal reproducible example 来重现该问题。但是,很可能您一次读取的数据太少,更新请求使 UI 线程饱和,这与阻止它一样糟糕。尝试重构代码,使 UI 更新仅每 100-500 毫秒左右发生一次。
  • “我认为问题是完整且可验证的” -- 如果您是回答问题的人,那么您认为可能是相关的。但是,你不是,它不是。请阅读minimal reproducible example。另外,请阅读How to Ask,以及该页面底部链接的所有文章,以便您了解minimal reproducible example 的实际含义,以及以清晰、可回答的方式提出您的问题所需的内容。

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


【解决方案1】:

有时确实需要在 UI 线程上执行一些异步后台操作(例如,语法高亮、拼写检查等)。我不会用您的特定(IMO,人为的)示例来质疑设计问题 - 很可能您应该在这里使用 MVVM 模式 - 但您当然可以保持 UI 线程响应。

您可以通过感知任何待处理的用户输入并让步到主消息循环来实现这一点,从而为其提供处理优先级。这是一个完整的、剪切粘贴并运行的示例,说明如何根据您要解决的任务在 WinForms 中执行此操作。注意await InputYield(token) 就是这样做的:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFormsYield
{
    static class Program
    {
        // a long-running operation on the UI thread
        private static async Task LongRunningTaskAsync(Action<string> deliverText, CancellationToken token)
        {
            for (int i = 0; i < 10000; i++)
            {
                token.ThrowIfCancellationRequested();
                await InputYield(token);
                deliverText(await ReadLineAsync(token));
            }
        }

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // create some UI

            var form = new Form { Text = "Test", Width = 800, Height = 600 };

            var panel = new FlowLayoutPanel
            {
                Dock = DockStyle.Fill,
                FlowDirection = FlowDirection.TopDown,
                WrapContents = true
            };

            form.Controls.Add(panel);
            var button = new Button { Text = "Start", AutoSize = true };
            panel.Controls.Add(button);

            var inputBox = new TextBox
            {
                Text = "You still can type here while we're loading the file",
                Width = 640
            };
            panel.Controls.Add(inputBox);

            var textBox = new TextBox
            {
                Width = 640,
                Height = 480,
                Multiline = true,
                ReadOnly = false,
                AcceptsReturn = true,
                ScrollBars = ScrollBars.Vertical
            };
            panel.Controls.Add(textBox);

            // handle Button click to "load" some text

            button.Click += async delegate
            {
                button.Enabled = false;
                textBox.Enabled = false;
                inputBox.Focus();
                try
                {
                    await LongRunningTaskAsync(text =>
                        textBox.AppendText(text + Environment.NewLine),
                        CancellationToken.None);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
                finally
                {
                    button.Enabled = true;
                    textBox.Enabled = true;
                }
            };

            Application.Run(form);
        }

        // simulate TextReader.ReadLineAsync
        private static async Task<string> ReadLineAsync(CancellationToken token)
        {
            return await Task.Run(() =>
            {
                Thread.Sleep(10); // simulate some CPU-bound work
                return "Line " + Environment.TickCount;
            }, token);
        }

        //
        // helpers
        //

        private static async Task TimerYield(int delay, CancellationToken token)
        {
            // yield to the message loop via a low-priority WM_TIMER message (used by System.Windows.Forms.Timer)
            // https://web.archive.org/web/20130627005845/http://support.microsoft.com/kb/96006 

            var tcs = new TaskCompletionSource<bool>();
            using (var timer = new System.Windows.Forms.Timer())
            using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false))
            {
                timer.Interval = delay;
                timer.Tick += (s, e) => tcs.TrySetResult(true);
                timer.Enabled = true;
                await tcs.Task;
                timer.Enabled = false;
            }
        }

        private static async Task InputYield(CancellationToken token)
        {
            while (AnyInputMessage())
            {
                await TimerYield((int)NativeMethods.USER_TIMER_MINIMUM, token);
            }
        }

        private static bool AnyInputMessage()
        {
            var status = NativeMethods.GetQueueStatus(NativeMethods.QS_INPUT | NativeMethods.QS_POSTMESSAGE);
            // the high-order word of the return value indicates the types of messages currently in the queue. 
            return status >> 16 != 0;
        }

        private static class NativeMethods
        {
            public const uint USER_TIMER_MINIMUM = 0x0000000A;
            public const uint QS_KEY = 0x0001;
            public const uint QS_MOUSEMOVE = 0x0002;
            public const uint QS_MOUSEBUTTON = 0x0004;
            public const uint QS_POSTMESSAGE = 0x0008;
            public const uint QS_TIMER = 0x0010;
            public const uint QS_PAINT = 0x0020;
            public const uint QS_SENDMESSAGE = 0x0040;
            public const uint QS_HOTKEY = 0x0080;
            public const uint QS_ALLPOSTMESSAGE = 0x0100;
            public const uint QS_RAWINPUT = 0x0400;

            public const uint QS_MOUSE = (QS_MOUSEMOVE | QS_MOUSEBUTTON);
            public const uint QS_INPUT = (QS_MOUSE | QS_KEY | QS_RAWINPUT);

            [DllImport("user32.dll")]
            public static extern uint GetQueueStatus(uint flags);
        }
    }
}

现在您应该问问自己,如果用户修改了编辑器的内容,而编辑器的内容仍在背景中填充,您会怎么做。在这里为简单起见,我只是禁用了按钮和编辑器本身(UI 的其余部分是可访问和响应的),但问题仍然悬而未决。此外,您应该考虑实现一些取消逻辑,这超出了本示例的范围。

【讨论】:

  • 不错!但我有一个问题。为什么你使用 Thread.Sleep 而不是 Task.Delay ?
  • @ppk,这里的ReadLineAsync 实现只是一个用于测试的模型。我在这里故意在Task.Run lambda 中使用Thread.Sleep,以表示生成一行文本的一些CPU 绑定工作,该文本被卸载到池线程。实际上,除了用于测试之外,您几乎不需要在生产代码中使用 Thread.Sleep
  • 感谢您的补充说明。
【解决方案2】:

如果您需要保持 UI 的响应速度,只需给它一些喘息的时间。
阅读一行文本的速度如此之快,以至于您 (a) 几乎无需等待,而更新 UI 则需要更长的时间。插入即使是非常小的延迟也可以更新 UI。

使用 Async/Await(SynchronizationContext 被 await 捕获)

public Form1()
{
   InitializeComponent();
   values.DragDrop += new DragEventHandler(this.OnDrop);
   values.DragEnter += new DragEventHandler(this.OnDragEnter);
}

public async void OnDrop(object sender, DragEventArgs e)
{
   string dropped = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
   if (dropped.Contains(".csv") || dropped.Contains(".txt")) {
      try {
         string line = string.Empty;
         using (var reader = new StreamReader(dropped)) {
            while (reader.Peek() >= 0) {
               line = await reader.ReadLineAsync();
               values.AppendText(line.Replace(";", " ") + "\r\n");
               await Task.Delay(10);
            }
         }
      }
      catch (Exception) {
         //Do something here
      }
   }
}

private void OnDragEnter(object sender, DragEventArgs e)
{
   e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop, false) 
            ? DragDropEffects.Copy 
            : DragDropEffects.None;
}

使用 Task.Factory 的 TPL
TPL 通过 TaskScheduler 执行任务。
TaskScheduler 可用于将任务排队到 SynchronizationContext。

TaskScheduler _Scheduler = TaskScheduler.FromCurrentSynchronizationContext();

//No async here
public void OnDrop(object sender, DragEventArgs e)
{
   string dropped = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
   if (dropped.Contains(".csv") || dropped.Contains(".txt")) {
      Task.Factory.StartNew(() => {
         string line = string.Empty;
         int x = 0;
         try {
            using (var reader = new StreamReader(dropped)) {
               while (reader.Peek() >= 0) {
                  line += (reader.ReadLine().Replace(";", " ")) + "\r\n";
                  ++x;
                  //Update the UI after reading 20 lines
                  if (x >= 20) {
                     //Update the UI or report progress 
                     Task UpdateUI = Task.Factory.StartNew(() => {
                        try {
                           values.AppendText(line);
                        }
                        catch (Exception) {
                           //An exception is raised if the form is closed
                        }
                     }, CancellationToken.None, TaskCreationOptions.PreferFairness, _Scheduler);
                     UpdateUI.Wait();
                     x = 0;
                  }
               }
            }
         }
         catch (Exception) {
            //Do something here
         }
      });
   }
}

【讨论】:

    【解决方案3】:

    也许为此使用 Microsoft 的反应式框架。这是您需要的代码:

    using System.Reactive.Concurrency;
    using System.Reactive.Linq;
    
    namespace YourNamespace
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
    
                IDisposable subscription =
                    Observable
                        .FromEventPattern<DragEventHandler, DragEventArgs>(h => values.DragDrop += h, h => values.DragDrop -= h)
                        .Select(ep => ((string[])ep.EventArgs.Data.GetData(DataFormats.FileDrop))[0])
                        .ObserveOn(Scheduler.Default)
                        .Where(dropped => dropped.Contains(".csv") || dropped.Contains(".txt"))
                        .SelectMany(dropped => System.IO.File.ReadLines(dropped))
                        .ObserveOn(this)
                        .Subscribe(line => values.AppendText(line + Environment.NewLine));
            }
        }
    }
    

    如果您想在添加值之前清除文本框,然后将 .SelectMany 替换为:

    .SelectMany(dropped => { values.Text = ""; return System.IO.File.ReadLines(dropped); })
    

    NuGet "System.Reactive" & "System.Reactive.Windows.Forms" 获取位。

    关闭表单时,只需执行subscription.Dispose() 即可删除事件处理程序。

    【讨论】:

      猜你喜欢
      • 2020-05-13
      • 2010-09-14
      • 2010-09-30
      • 1970-01-01
      • 1970-01-01
      • 2011-06-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-08
      相关资源
      最近更新 更多