调度程序的实用性和良好实践

本文关键字:实用性 调度程序 | 更新日期: 2023-09-27 18:34:47

为了使我的问题简单,让我们说我使用线程来动态更新字符串中的替换。在我的真实代码中,我需要一个线程。我知道我可以在这个简单的例子中避免它。

所以,我的软件有两个字段。用户选择一个文件,编写一个(某种(正则表达式,并在键入句子的同时查看修改结果。当用户选择一个文件时,我启动线程(请参阅listViewFiles_SelectionChanged方法(。我的线程的工作是在DoWork方法中。

public void DoWork()
    {
        while (true)
        {
            FileData fileData = _selectedFile;
            if (fileData != null)
            {
                string name = fileData.FileName;
                string searchRegEx = GenerateRegex(_searchTextBox.Text);
                string replacement = _replaceTextBox.Text;
                name = Regex.Replace(name, searchRegEx, replacement);
                /*
                foreach (var action in _actionCollection)
                {
                    name = action.Rename(name);
                }*/
                _searchSample.Content = fileData.FileName;
                _replaceSample.Content = name;
            }
            Thread.Sleep(1000);
        }
    }
    private void listViewFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _selectedFile = listViewFiles.SelectedItem as FileData;
        _thread.Start();
    }

当我的线程做他的工作时,我在行字符串 search上得到一个异常 RegEx = GenerateRegex(_searchTextBox.Text(; :调用线程无法访问此对象,因为其他线程拥有它。我读了很多关于这个例外的信息,但我不明白。

为了修复它,我用调度程序包围了我的代码。我不明白机械主义,但它有效。我不知道它是否正确或性能,但它有效。

 public void DoWork()
    {
        while (true)
        {
            FileData fileData = _selectedFile;
            if (fileData != null)
            {
                //use Window.Dispatcher
                this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
                    new Action(delegate()
                    {
                        string name = fileData.FileName;
                        string searchRegEx = GenerateRegex(_searchTextBox.Text);
                        string replacement = _replaceTextBox.Text;
                        name = Regex.Replace(name, searchRegEx, replacement);
                        /*
                        foreach (var action in _actionCollection)
                        {
                            name = action.Rename(name);
                        }*/
                        _searchSample.Content = fileData.FileName;
                        _replaceSample.Content = name;
                    }));
            }
            Thread.Sleep(1000);
        }
    }
    private void listViewFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _selectedFile = listViewFiles.SelectedItem as FileData;
        _thread.Start();
    }

我想知道这段代码是否正确。您可以在注释中看到 foreach 说明。我的代码应该做很多工作,我不知道在 delagate 中这样做是否是最好的方法。调度程序的效用和良好实践?

调度程序的实用性和良好实践

我认为您的一篇文章中有很多问题,我将尝试解决所有问题:

从其他线程访问可视控件

Winforms和WPF都是基于这样一个事实构建的:只有一个线程可以更改对象状态,当然,该线程与创建对象的线程相同。

您可以想象为什么这很重要:控件是知道如何"呈现"或"绘制"自己的对象。在绘制、调整大小、移动/拖动时,无法从控件本身的"外部"更改对象的属性。当然,主线程已经在忙于执行我提到的转换,因此可以保证在主线程上运行的"用户"代码不会更改它。但是,并行运行的另一个线程可能会做到这一点。例如,想象一下,主线程正在渲染一个TextBox的文本,当第二个线程更改文本时,单词的一半是写的。例如,这将导致文本宽度的计算出现问题。

使用调度程序

线程

调度程序所做的是*将代码封送到主线程。您会看到,大多数可视化框架(包括WinForms和WPF(都基于"应用程序循环"。这意味着您的应用程序在while(true){}块内运行。您的代码(例如,listViewFiles_SelectionChanged(在需要时从此循环中调用。管理此调用的对象是 Dispatcher 。它有一个要运行的东西队列,它决定接下来运行。当调度程序调用的代码运行时,应用程序的可视部分不会发生任何其他事情 - 毕竟,这就是线程正在做的事情,对吧?因此,它无法处理用户输入、重新渲染屏幕等。

Dispatcher为您提供了一个接口,该接口可以从另一个线程调用,该线程通过将调度程序插入队列来发布要由调度程序调用的新方法。正如您可以理解的那样,它不会立即执行:您在调用main的第二个线程上,main可能正忙于渲染屏幕,处理输入甚至运行listViewFiles_SelectionChanged等代码。循环迭代结束后,Dispatcher将检查队列。根据方法的优先级,它可能是下一个要执行的方法,甚至等待几次迭代。

出于这个原因,如果你把你在调度程序方法上的第二个线程中所做的一切都放在上面,你实际上是在要求框架在主线程上运行你的第二个线程代码。而且,由于您的线程代码永远运行,主线程将永远忙于运行该代码(DoWork(,并且不再能够执行任何其他操作。

调度员良好做法

因此,在上述项目的结论中,当您将代码编组到调度程序时,主线程会忙于执行此操作。在繁忙时,您的应用程序变得无响应。由于我们一直想要一个响应式应用程序,因此我们必须在主线程上做尽可能多的事情,也就是说,在我们要求调度程序封送的任何内容上。

在这种情况下,您必须做的只是访问控件的封送线。即使这意味着对调度程序的多次调用 - 当然,您将为这些调用付出代价,但这比将主线程与代码卡住的时间超过必要的时间要好。

使用具有单线程的调度程序

即使您

没有第二个线程,Dispatcher也可以为您提供帮助。如果需要执行长时间计算,可以使用一组标志或enum来跟踪状态。在该方法中,您调用调度程序传递您自己的方法(优先级较低(,然后它们返回(因此您将计算分成几部分(。

问题是,你的代码正在访问_searchTextBox.Text和__replaceTextBox.Text。您使用调度程序的解决方案正在工作,但实际上无法解决任何问题。代码将在 UI 线程中执行,但在 ListView 选择更改后不会立即执行。

要使其正常工作,请在没有调度程序的情况下返回到您的第一个版本,但将 SearchText 和 ReplaceText 作为线程启动参数传递。下面是一些伪代码:

var searchText = _searchTextBox.Text;
var replaceText = _replaceTextBox.Text
Thread.Start(() => DoWork(searchText, replaceText));

只能在创建控件的线程中访问控件。在大多数情况下,它是 UI 线程。Windows 窗体就是这种情况,WPF 也是如此。调用 Dispatch 只会阻塞您的线程并在 UI 线程中运行其余部分,这会剥夺将工作卸载到其他线程的大部分好处,当然还会阻止 UI 线程执行其他工作。正如上面提到的,解决方案是将 UI 代码与后台工作线程代码分开。在这种情况下,这很简单,因为您仅使用接口中的输入值,您可以从 UI 线程中添加项目的队列中获取这些输入值。需要同步对队列的访问以避免赛车条件(锁定(queue.同步((。这是生产者-消费者设计模式的经典案例。

所以,如果我们只看你的代码,我建议只包装 ui update/get 代码来调用,因为正如我目前所说,你只是在 UI 线程中调用你的代码,而你的附加线程是无用的,因为它只是调用和睡眠。

这是一个例子:

 private Thread _thread;
    public MainWindow()
    {
        InitializeComponent();
        _thread = new Thread(DoWork);
        _thread.Start();
    }
    private void DoWork()
    {
        while (true)
        {
            var str = (string)Dispatcher.Invoke(new Func<object>(() => NotifyLabel.Content));
            str += "a";
            Dispatcher.Invoke(new Action(() => NotifyLabel.Content = str));
            Thread.Sleep(500);
        }
    }

我建议您阅读调用信息,并且有关于调度程序/线程的有趣问题

PS:我一般不审查您的多线程代码,因为如果您在此处使用 mvvm/其他模式,它可以大大改进