C# 在windows窗体中运行长任务时保持UI线程响应

C# 在windows窗体中运行长任务时保持UI线程响应,c#,winforms,async-await,task-parallel-library,C#,Winforms,Async Await,Task Parallel Library,我试图将一个大的文本文件读入文本框,并在将文件拖到文本框时保持ui响应 windows窗体未按预期工作,已冻结,似乎只在执行读取文件并将内容附加到文本框的任务 IDE引发了ContextSwitchDeadLock,但实际上并不是一个错误。 这是一项长期的任务。我已经修复了它,更改了异常菜单下的行为 多亏了JSteward,Peter将代码更改为这个 运行此任务时,如何保持ui(主线程)的响应性? 谢谢 private SynchronizationContext fcontext; 公共表格1

我试图将一个大的文本文件读入文本框,并在将文件拖到文本框时保持ui响应

windows窗体未按预期工作,已冻结,似乎只在执行读取文件并将内容附加到文本框的任务

IDE引发了ContextSwitchDeadLock,但实际上并不是一个错误。 这是一项长期的任务。我已经修复了它,更改了异常菜单下的行为

多亏了JSteward,Peter将代码更改为这个

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

private SynchronizationContext fcontext;
公共表格1()
{      
初始化组件();

values.DragDrop+=values\u DragDrop;//可能使用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));
        }
    }
}
NuGet“System.Reactive”和“System.Reactive.Windows.Forms”来获取位


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

也许可以使用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));
        }
    }
}
NuGet“System.Reactive”和“System.Reactive.Windows.Forms”来获取位


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

如果需要保持UI响应,只需给它喘息的时间。
阅读一行文本的速度非常快,以至于您(a)几乎什么都不等待,而更新UI则需要更长的时间。插入一点延迟都会让UI更新。

使用异步/Await(同步上下文由Await捕获)

第三方物流使用任务工厂
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
         }
      });
   }
}

如果您需要保持UI的响应性,只需给它喘息的时间。
阅读一行文本的速度非常快,以至于您(a)几乎什么都不等待,而更新UI则需要更长的时间。插入一点延迟都会让UI更新。

使用异步/Await(同步上下文由Await捕获)

第三方物流使用任务工厂
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
         }
      });
   }
}

有时确实需要在UI线程上执行一些异步的后台操作(例如,语法突出显示、键入时拼写检查等)。我不会对您的特定(IMO,人为的)设计问题提出质疑示例—这里最有可能使用的是MVVM模式—但您肯定可以保持UI线程的响应性

您可以通过感测任何挂起的用户输入并向主消息循环让步来实现这一点,从而为其提供处理优先级。下面是一个完整的、剪切粘贴并运行的示例,说明如何在WinForms中根据您试图解决的任务来实现这一点。注意
等待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);
        }
    }
}
使用系统;
使用System.Runtime.InteropServices;
使用系统线程;
使用System.Threading.Tasks;
使用System.Windows.Forms;
名称空间WinFormsField
{
静态类程序
{
//UI线程上的长时间运行操作
专用静态异步任务LongRunningTaskAsync(Action deliverText,CancellationToken令牌)
{
对于(int i=0;i<10000;i++)
{
token.ThrowIfCancellationRequested();
等待输入收益率(令牌);
deliverText(等待ReadLineSync(令牌));
}
}
[状态线程]
静态void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
//创建一些用户界面
var form=new form{Text=“Test”,宽度=800,高度=600};
var面板=新的FlowLayoutPanel
{
Dock=DockStyle.Fill,
FlowDirection=FlowDirection.TopDown,
WrapContents=true
};
窗体.控件.添加(面板);
var按钮=新按钮{Text=“Start”,AutoSize=true};
面板。控件。添加(按钮);
var inputBox=新文本框
{
Text=“我们加载文件时,您仍可以在此处键入”,
宽度=640
};
panel.Controls.Add(输入框);
var textBox=新的textBox
{
宽度=640,
高度=480,
多行=真,
ReadOnly=false,
AcceptsReturn=true,
滚动条=滚动条。垂直
};
panel.Controls.Add(文本框);
//点击手柄按钮“加载”一些文本
按钮。单击+=异步委托
{
按钮。已启用=错误;
textBox.Enabled=false;
Focus();
尝试
{
等待LongRunningTaskAsync(文本=>
textBox.AppendText(text+Environment.NewLine),
取消令牌(无);
}
捕获(例外情况除外)
{
MessageBox.Show(例如Message);
}
最后
{
button.Enabled=true;
textBox.Enabled=true;
}
};
申请表格;
}
//模拟TextReader.ReadLineAsync
专用静态异步任务ReadLineAsync(CancellationToken令牌)
{
返回等待任务。运行(()=>
{
Thread.Sleep(10);//模拟一些CPU限制的工作
返回“Line”+Environment.TickCount;
},代币);
}
//
//助手
//
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);
        }
    }
}