вторник, 10 января 2017 г.

Многопоточность и GUI

Маленький пример использования многопоточности в приложениях, содержащих графический пользовательский интерфейс (GUI). Продемонстрировано два способа обращения к элементам пользовательского интерфейса из рабочего потока в UI-поток. Графический интерфейс при этом не "подвисает".

.Net Framework 4.5

 /* Sandbox.cs
 * © Andrey Bushman, 2017
 *
 * Небольшой пример создания дополнительного потока,
 * работающего параллельно с потоком пользовательского
 * интерфейса (UI) и обновляющего этот интерфейс по мере
 * необходимости. Дополнительных потоков можно создавать
 * сколько угодно. В данном примере для простоты создаётся
 * только один.
 *
 * В данном примере вместо прямого использоватия потока
 * (Thread) я использую задачу (Task), которая в свою очередь
 * использует пул потоков.
 *
 * Рабочий поток (т.е. задача) вычисляет текущую дату и время,
 * после чего записывает их в ListBox, находящийся в потоке UI.
 *
 * Снятие/установка галочки "Do work" управляет
 * стартом/завершением рабочего потока. Кнопка "Clear" очищает
 * ListBox и заголовок окна.
 *
 * В консоль выводятся идентификаторы текущих потоков и маркер
 * их принадлежности (или не принадлежности) к пулу потоков.
 *
 * В данном примере для обращения к потоку UI из рабочего
 * потока я использую два способа: диспетчер и контекст
 * синхронизации.
 *
 * В примере используется WPF, но всё то же самое применимо к
 * WinForms и ASP.NET.
 */
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Bushman.Sandbox {

    class MyWindow : Window {

        SynchronizationContext context = null;

        public MyWindow() : base() {

            Console.Title = "Sandbox";
            string prefix = "Main Window";
            Title = prefix;
            Topmost = true;

            Width = 300;
            Height = 600;

            ResizeMode = ResizeMode.NoResize;

            WindowStartupLocation = WindowStartupLocation
                .CenterScreen;

            Grid grid = new Grid();
            grid.RowDefinitions.Add(new RowDefinition());
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());

            grid.ColumnDefinitions[1].Width = new GridLength(0,
                GridUnitType.Auto);
            grid.RowDefinitions[1].Height = new GridLength(0,
                GridUnitType.Auto);

            Content = grid;

            Thickness margin = new Thickness(5, 5, 5, 5);

            ListBox listbox = new ListBox();
            listbox.Margin = margin;
            grid.Children.Add(listbox);
            listbox.SetValue(Grid.RowProperty, 0);
            listbox.SetValue(Grid.ColumnProperty, 0);
            listbox.SetValue(Grid.ColumnSpanProperty, 2);

            CheckBox chBox = new CheckBox();
            chBox.Margin = margin;
            chBox.Content = "Do work";
            grid.Children.Add(chBox);
            chBox.SetValue(Grid.ColumnProperty, 0);
            chBox.SetValue(Grid.RowProperty, 1);

            Task task = null;
            long i = 0;

            chBox.Checked += (s, e) => {

                task = new Task(() => {
                    using (task) {
                        i = 0;
                        while (Dispatcher.Invoke(
                            () => chBox.IsChecked == true) &&
                            i < long.MaxValue) {

                            // В рабочем потоке выполняем
                            // некоторую работу. Например -
                            // формируем строку текущих даты и
                            // времени.
                            string value = DateTime.Now
                                .ToString("yyyy-MM-dd hh:mm:ss"
                                );

                            // В данном примере мы мы можем
                            // имитировать длительную работу,
                            // либо заблокировать эту строку
                            // кода, если такая имитация нам не
                            // нужна:
                            //Thread.Sleep(TimeSpan.FromSeconds
                            //    (1));

                            Console.WriteLine(
                                "Current thread Id: {0}. " +
                                "Is pull thread: {1}",
                                Thread.CurrentThread
                                .ManagedThreadId.ToString(),
                                Thread.CurrentThread
                                .IsThreadPoolThread);

                            // Результат наших "вычислений"
                            //записываем в поток UI
                            context.Post(_ => {
                                listbox.Items.Add(value);
                                Title = string.Format(
                                    "{0}. Items Count: {1}",
                                prefix, i++.ToString());

                                Console.WriteLine(
                                "Current thread Id: {0}. " +
                                "Is pull thread: {1}",
                                Thread.CurrentThread
                                .ManagedThreadId.ToString(),
                                Thread.CurrentThread
                                .IsThreadPoolThread);
                            }, null);
                        }
                    }
                });

                task.Start();
            };

            chBox.IsChecked = false;

            Button button = new Button();
            button.Content = "Clear";
            button.Margin = margin;
            button.Padding = margin;
            grid.Children.Add(button);
            button.SetValue(Grid.ColumnProperty, 1);
            button.SetValue(Grid.RowProperty, 1);
            button.Click += (s, e) => {
                listbox.Items.Clear();
                Title = prefix;
                i = 0;
            };

            EventHandler action = null;

            action = (s, e) => {
                context = SynchronizationContext.Current;
                Activated -= action;
            };

            Activated += action;

            NameScope.SetNameScope(this, new NameScope());
            RegisterName(nameof(listbox), listbox);
            RegisterName(nameof(chBox), chBox);
            RegisterName(nameof(button), button);
        }
    }

    class Sandbox {
        [STAThread]
        static void Main(string[] args) {

            MyWindow win = new MyWindow();
            Application app = new Application();
            app.Run(win);
        }
    }
}


Результат работы кода выглядит следующим образом:

 

1 комментарий:

Александр комментирует...

Замечательный пример. Мало где найдешь подобное - в основном примеры с использованием Dispatcher, который сам по себе затрачивает много времени на обновление GUI. "Прикрутил" данный код из статьи к своим нуждам и работа плагина стала в разы быстрее!
Теперь нужно 105 раз это прочитать, чтобы понять и запомнить =))