RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 615113
Accepted
VladD
VladD
Asked:2020-01-16 07:21:21 +0000 UTC2020-01-16 07:21:21 +0000 UTC 2020-01-16 07:21:21 +0000 UTC

为什么 Thread.Sleep 行为异常?如何在图形程序中进行延迟或长时间计算?

  • 772

我需要延迟向用户显示信息。例如,每秒更改文本标签的内容。(或输出长时间计算的中间结果。)在命令行程序中,我这样做了:

Console.WriteLine("значение 1");
Thread.Sleep(1000);
Console.WriteLine("значение 2");
Thread.Sleep(1000);
Console.WriteLine("значение 3");

有效。现在我需要在图形程序中做同样的事情。我写了一个方法

void OnClick(object sender, EventArgs args)
{
    label.Text = "значение 1";
    Thread.Sleep(1000);
    label.Text = "значение 2";
    Thread.Sleep(1000);
    label.Text = "значение 3";
}

但它不是那样工作的。中间值不显示,程序长时间停止响应。当它挂起时,它会立即显示最后一个值。

发生了什么?为什么程序运行不正确,如何正确运行?

c#
  • 4 4 个回答
  • 10 Views

4 个回答

  • Voted
  1. Best Answer
    VladD
    2020-01-16T07:21:21Z2020-01-16T07:21:21Z

    图形程序与控制台程序的不同之处在于主线程做很多事情。在控制台程序中,我们有完全的控制权,我们完全控制它的里程数。在图形程序中,我们启动应用程序,框架为我们创建一个消息循环。在这个循环中,框架处理鼠标移动、按键、窗口大小调整、定时器回调等,并且还调用我们的事件处理程序,每次循环迭代一次(好吧,这是一个简化的图片,但为了这个演示文稿它会做)。循环迭代完成后,执行进入下一次迭代。

    所有这些都在同一个线程上运行,该线程称为 UI 线程。

    现在,如果我们在 UI 线程上执行会发生什么Thread.Sleep(1000)?事情是这样的:线程被阻塞并且整整一秒什么都不做。就在这一秒,我们的消息循环处于空闲状态,因为执行线程被我们阻塞了!这一刻,窗口消息没有被处理,没有鼠标响应发生,没有回调被调用,甚至没有重绘窗口内容——毕竟,所有这些都是在我们阻止的同一个消息循环中完成的!

    为使程序正常运行,我们的事件处理程序(如OnClick)、对象构造函数以及通常在 UI 线程上运行的所有代码都必须尽可能快地运行,没有延迟。

    如何暂停一秒钟?幸运的是,在该语言的现代版本中(自 C# 5 起)有一个简单的解决方案。这是异步/等待。让我们的处理程序异步(关键字async),并将其替换Thread.Sleep为await Task.Delay:

    async void OnClick(object sender, EventArgs args)
    {
        label.Text = "значение 1";
        await Task.Delay(1000);
        label.Text = "значение 2";
        await Task.Delay(1000);
        label.Text = "значение 3";
    }
    

    此方法工作正常!¹

    发生了什么?问题是await Task.Delay等待时间不会阻塞流程。在等待的过程中,方法似乎停止执行,消息循环不再阻塞。[小心,它可能在其他地方被阻塞。]当等待结束时,消息循环从它停止的地方恢复方法,到下一个,await或者到方法的末尾。²

    因此,我们的代码不再阻塞 UI 线程,框架可以继续绘制窗口并执行其他内务处理任务。


    但是,如果您需要执行一些计算而不是延迟怎么办?它们不是那么容易从函数流中切出的,它们仍然必须被执行。出于这些目的,可以将它们卸载到另一个流中。不要害怕,这很简单。而不是代码

    label.Text = "парсим большой файл";
    size = ParseBigFile();
    label.Text = "закончили, результат = " + size;
    

    你这样写:

    label.Text = "парсим большой файл";
    size = await Task.Run(() => ParseBigFile());
    label.Text = "закончили, результат = " + size;
    

    Task.Run在后台线程上执行您的代码,并且该函数在此执行期间不会再次阻塞 UI 线程。³获利!请注意,您不能从后台线程读取控件的值,因此必须提前读取它们:

    它是:

    label.Text = "парсим большой файл";
    size = ParseBigFileFromPath(textbox.Text);
    label.Text = "закончили, результат = " + size;
    

    它变成了:

    label.Text = "парсим большой файл";
    string path = textbox.Text; // читаем из контрола в UI-потоке
    size = await Task.Run(() => ParseBigFileFromPath(path)); // обращается к переменной
    label.Text = "закончили, результат = " + size;
    

    在旧版本的语言中,没有 async/await,你必须以更复杂的方式实现同​​样的事情。例如,启动一个计时器,订阅它的滴答声,并更改它们的控件中的值。同时,必须将局部变量移至类字段(或移至特殊的结构上下文)。或者你可以用DoEvents. 幸运的是,那些糟糕的过去早已一去不复返了。


    相关问题:

    • 使用 sleep() 方法
    • 在 C# 中使用多线程
    • 使用 Thread.Sleep();
    • 线程块窗口 wpf

    ¹ 但是对于其他异步方法,不是事件处理程序,我们需要返回 not void,而是返回Task一些Task<string>,以便调用代码可以等待它们完成并获得结果。

    ² 陈述过于简单化,所以不要把它当作最终的真理。这是一个大概的图,如果你想知道确切的图,最好看书或者文档。或者问一个问题,如果有什么东西表现得难以理解。

    ³ 如果您需要在后台线程上执行大量长时间运行的工作,那么完全卸载这些工作并通过Progress<T>.

    • 34
  2. Bulson
    2020-02-14T23:07:46Z2020-02-14T23:07:46Z

    作为答案的例证VladD。

    任务: 组织具有以下条件的方法的执行:

    1. 该方法必须执行一些长时间的任务并且不能阻塞 程序窗口;
    2. 该方法应在程序窗口中显示有关其执行进度的信息;
    3. 在方法运行期间,应该可以根据程序用户的命令取消其运行。

    解决方案:

    为了实现第1点,我们将使用async/await和Task.Run()。

    要执行第2点,我们将使用一个实例Progress<T>

    为完成第3步,我们将使用所谓的取消令牌,该令牌将由 提供给我们CancellationTokenSource。

    我们应该这样:

    程序说明

    让我们创建一个实用程序类,它将有一个以 100 毫秒的延迟发送的方法。从 0 到 1000 的连续数字。

    class Iteration
    {
        public IEnumerable<int> StartIterator()
        {
            for (int i = 0; i < 1000; i++)
            {
               // Задержка.
               Thread.Sleep(100);
    
               // Вывод числа i.
               yield return i;
            }
        }
    } 
    

    这是我们进一步的解决方案

    public partial class Form1 : Form
    {
    
        public Form1()
        {
            InitializeComponent();
        }
    
        //источник токена отмены
        private CancellationTokenSource _tokenSource;
    
        // обработчик кнопки Старт
        private async void buttonStart_Click(object sender, EventArgs e)
        {
            // через него будем оповещать о ходе выполнения задачи
            // в нашем случае типизирован будет stringом,
            // т.к. вывод будет осуществляться в Label
            Progress<string> progress = new Progress<string>(text => this.labelOutput.Text = text);
    
            //готовим токен отмены
            _tokenSource = new CancellationTokenSource();
            CancellationToken cancelToken = _tokenSource.Token;
    
            //кнопки
            buttonStart.Enabled = false;
            buttonCancel.Enabled = true;
    
            try
            {
                // ВНИМАНИЕ! Вот запуск нашего долгого метода
                // обратите внимание на передачу параметров
                // токен отмены мы должны передать и в целевой метод
                // и собственно в метод Run()
                await Task.Run(() => DoIteration(cancelToken, progress), cancelToken);
            }
            catch (OperationCanceledException)
            {
                //случай отмены
                this.labelOutput.Text = "0";
            }
            catch (Exception)
            {
                //случай если возникнет какая-то ошибка
                this.labelOutput.Text = "Ошибка";
            }
            finally
            {
                // кнопки
                buttonStart.Enabled = true;
                buttonCancel.Enabled = false;
    
                // удаляем источник токена отмены
               _tokenSource.Dispose();
            }
        }
    
        // Это наш долгий метод, который сообщает о ходе своего выполнения
        // и может быть отменен в любой нужный момент
        // обратите внимание на второй параметр IProgress<string> progress
        // здесь должен быть обязательно интерфейс,
        // если случайно нашите Progress<string>, то не найдете Report() 
        private void DoIteration(CancellationToken cancelToken, IProgress<string> progress)
        {
            //создаем экземпляр нашего подсобного класса
            Iteration iteration = new Iteration();
    
            // типа какая-то полезная работа
            foreach (int number in iteration.StartIterator())
            {
                //отображаем ход работы метода
                progress.Report(number.ToString());
    
                //выбрасываем исключение в случае нажатия на кнопку отмены
                cancelToken.ThrowIfCancellationRequested();
            }
        }
    
        //обработчик кнопки Отмены
        private void buttonCancel_Click(object sender, EventArgs e)
        {
            // даем команду на отмену
            _tokenSource.Cancel();
        }
    }
    

    PS 在 WPF 和 MVVM 的情况下相同的例子 WPF案例中的工作示例

    这是 XAML

    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Canvas>
        <ProgressBar Height="18"
                     Canvas.Left="104"
                     Canvas.Top="128"
                     Width="170"
                     Minimum="0"
                     Maximum="1000"
                     Value="{Binding ProgressShow}"/>
        <TextBlock Canvas.Left="288"
                   Canvas.Top="130">
            <Run Text="{Binding ProgressShow}" />
            <Run Text="итераций" />
        </TextBlock>
        <Button Content="Старт"
                Command="{Binding StartCommand, Mode=OneTime}"
                Canvas.Left="154"
                Canvas.Top="216"
                Width="75" />
        <Button Content="Отмена"
                Command="{Binding CancelCommand, Mode=OneTime}"
                Canvas.Left="154"
                Canvas.Top="273"
                Width="75" />
    </Canvas>
    

    这是视图模型

    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        //источник токена отмены
        private CancellationTokenSource _tokenSource;
    
    
        //ctor
        public MainViewModel()
        {
    
        }
    
    
        /// <summary>
        /// Для отображения в ProgressBar
        /// </summary>
        private int _ProgressShow;
        public int ProgressShow
        {
            get => _ProgressShow;
            set
            {
                _ProgressShow = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProgressShow)));
            }
        }
    
        /// <summary>
        /// Флаг работы долгой задачи
        /// </summary>
        private bool _IsRunning;
        public bool IsRunning
        {
            get => _IsRunning;
            set
            {
                _IsRunning = value;
                //обновление состояния кнопок
                StartCommand.RaiseCanExecuteChanged();
                CancelCommand.RaiseCanExecuteChanged();
            }
        }
    
        /// <summary>
        /// Кнопка Старт
        /// </summary>
        private RelayCommand _StartCommand;
        public RelayCommand StartCommand
        {
            get => _StartCommand = _StartCommand ?? new RelayCommand(OnStart, CanStart);
        }
        private bool CanStart()
        {
            if (IsRunning)
            {
                return false;
            }
            return true;
        }
        private async void OnStart()
        {
            // через него будем оповещать о ходе выполнения задачи
            Progress<int> progress = new Progress<int>(n => ProgressShow = n);
    
            //готовим токен отмены
            _tokenSource = new CancellationTokenSource();
            CancellationToken cancelToken = _tokenSource.Token;
    
            //кнопки
            IsRunning = true;
    
            try
            {
                // ВНИМАНИЕ! Вот запуск нашего долгого метода
                // обратите внимание на передачу параметров
                // токен отмены мы должны передать и в целевой метод
                // и собственно в метод Run()
                await Task.Run(() => DoIteration(cancelToken, progress), cancelToken);
            }
            catch (OperationCanceledException)
            {
                //случай отмены
                ProgressShow = 0;
            }
            catch (Exception)
            {
                //случай если возникнет какая-то ошибка
                Debug.WriteLine("-->Error");
                ProgressShow = 0;
            }
            finally
            {
                // кнопки
                IsRunning = false;
    
                // удаляем источник токена отмены
                _tokenSource.Dispose();
            }
    
        }
    
        // Это наш долгий метод, который сообщает о ходе своего выполнения
        // и может быть отменен в любой нужный момент
        // обратите внимание на второй параметр IProgress<int> progress
        // здесь должен быть обязательно интерфейс,
        // если случайно нашите Progress<int>, то не найдете Report() 
        private void DoIteration(CancellationToken cancelToken, IProgress<int> progress)
        {
            //создаем экземпляр нашего подсобного класса
            Iteration iteration = new Iteration();
    
            // типа какая-то полезная работа
            foreach (int number in iteration.StartIterator())
            {
                //отображаем ход работы метода
                progress.Report(number);
    
                //выбрасываем исключение в случае нажатия на кнопку отмены
                cancelToken.ThrowIfCancellationRequested();
            }
    
        }
    
    
        /// <summary>
        /// Кнопка Отмена
        /// </summary>
        private RelayCommand _CancelCommand;
        public RelayCommand CancelCommand
        {
            get => _CancelCommand = _CancelCommand ?? new RelayCommand(OnCancel, CanCancel);
        }
        private bool CanCancel()
        {
            if (!IsRunning)
            {
                return false;
            }
            return true;
        }
        private void OnCancel()
        {
            _tokenSource.Cancel();
        }
    }
    
    • 6
  3. K G
    2020-01-17T09:58:32Z2020-01-17T09:58:32Z
    using System.Threading;
    ...
    new Thread(new ThreadStart(() => {
        this.Invoke((MethodInvoker)delegate
        {
            label.Text = "значение 1";
        });
        Thread.Sleep(1000);
        this.Invoke((MethodInvoker)delegate
        {
            label.Text = "значение 2";
        });
        Thread.Sleep(1000);
        this.Invoke((MethodInvoker)delegate
        {
            label.Text = "C# 3.0, привет из 2017!";
        });
    })).Start();
    
    • 3
  4. Andrew_STOP_RU_AGRESSION_IN_UA
    2020-11-18T18:18:25Z2020-11-18T18:18:25Z

    Thread.Sleep 阻塞整个线程。包括处理 UI 的整个线程(如果在那里调用)。

    Лично я бы сказал бы что на это есть несколько решений.

    Решение №1: async await (советую использовать именно его)

    Его расписал Влад, но я повторюсь и распишу его немного короче чисто что бы люди не мотались туда-назад:

    async void OnClick(object sender, EventArgs args)
    // обрати внимание на модификатор async - он вызывает метод асинхронно как Task 
    //не блокируя главный поток
    {
        label.Text = "значение 1";
        await Task.Delay(1000);  //Правильная замена Thread.Sleep которая в даном случае всего 
                                 //лишь приостанавливает таску, но не блокирует поток
        label.Text = "значение 2";
        await Task.Delay(1000);
        label.Text = "значение 3";
    }
    

    Решение №2: использование делегатов (не лучшее решение даной проблемы, но лучше с ним ознакомится)

    Суть в чем: в UI потоке ты создаешь делегат который будет делать что-то нужное. Например, обновлять в UI потоке некий progressBar который будет показывать на сколько процентов скачался большой файл. Собственно, сама скачка должна воспроизводится в другом потоке (создай новый тред) или в таске.

    На что нужно обратить внимание: используй BeginInvoke(делегат); (запускает метод по ссылке делегата в главном потоке) а не делегат.Invoke(); (запускает метод по ссылке делегата в этом же потоке).

    也可以在不声明特定委托实例的情况下完成。KG在他的回答中展示了旧版本语言如何处理任务的相同示例。事实上,他准确地描述了路径的这种变体,只是他没有声明任何具体的委托:

    using System.Threading;
    ...
    new Thread(new ThreadStart(() => {// Мы описываем что должно делатся в новом потоке
        this.Invoke((MethodInvoker)delegate //мы запускаем код в UI потоке
        {
            label.Text = "значение 1";
        });
        Thread.Sleep(1000); //Мы ждем 1 секунду в даном потоке   
        this.Invoke((MethodInvoker)delegate //мы запускаем код в UI потоке
        {
            label.Text = "значение 2";
        });
        Thread.Sleep(1000); //Мы ждем 1 секунду в даном потоке 
        this.Invoke((MethodInvoker)delegate //мы запускаем код в UI потоке
        {
            label.Text = "C# 3.0, привет из 2017!";
        });
    })).Start();//мы запускаем описанный метод в новом потоке
    

    解决方案 #3:实施 MVC 或 MVP 模式之一或任何其他类似模式。(一个非常正确的解决方案,但更难实施和繁琐)

    思路是:UI对程序的实现一无所知。一切都与 UI 分开完成。而实际上,逻辑的实现是通过一层事件与UI进行通信的。

    • 1

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5