3

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

所以,我的软件有两个领域。用户选择一个文件,编写一个(某种)正则表达式并在输入句子的同时查看修改结果。我在用户选择文件时启动线程(请参阅 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();
    }

当我的线程做他的工作时,我在行字符串 searchRegEx = GenerateRegex(_searchTextBox.Text); 上遇到异常。:调用线程无法访问此对象,因为不同的线程拥有它。我读了很多关于这个异常的内容,但我不明白。

为了解决这个问题,我用 Dispatcher 包围了我的代码。我不明白机制,但它的工作原理。我不知道它是否正确或高效,但它有效。

 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 中这样做是否是最好的方法。Dispatcher 的实用性和良好实践?

4

4 回答 4

5

我认为您的单个帖子中有很多问题,我将尝试解决所有问题:

从其他线程访问可视化控件

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

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

使用调度程序

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

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

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

调度员良好实践

因此,综上所述,当您将代码编组到调度程序时,主线程会忙于执行此操作。在忙碌时,您的应用程序变得无响应。因为我们一直想要一个响应式应用程序,所以我们必须在主线程上做尽可能少的事情,也就是说,无论我们要求 Dispatcher 编组什么。

既然如此,您必须做的只是编组访问 Controls 的线路。即使这意味着对 Dispatcher 的多次调用——当然,你会为这些调用付出代价,但它总比在你的代码中停留的时间长于主线程要好。

使用单线程调度程序

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

于 2012-05-18T15:24:32.803 回答
1

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

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

var searchText = _searchTextBox.Text;
var replaceText = _replaceTextBox.Text
Thread.Start(() => DoWork(searchText, replaceText));
于 2012-05-18T14:52:57.900 回答
0

您只能在创建 的线程中访问您的控件。在大多数情况下,它是 UI 线程。Windows 窗体就是这种情况,WPF 就是这种情况。调用 Dispatch 只会阻塞您的线程并在 UI 线程中运行其余线程,这会带走将工作卸载到其他线程的大部分好处,当然还会阻止 UI 线程执行其他工作。如上所述,解决方案是将您的 UI 代码与后台工作人员代码分开。在这种情况下,它很简单,因为您只使用来自界面的输入值,您可以从您在 UI 线程中添加项目的队列中获取这些输入值。需要同步对队列的访问以避免竞争条件(lock(queue.Synch))。这是 Produce-Consumer 设计模式的经典案例。

于 2012-05-18T14:48:58.130 回答
0

所以,如果我们只看你的代码,我建议只将 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/other 模式可以大大改进

于 2012-05-18T14:51:04.760 回答