TOC

This article has been localized into Ukrainian by the community.

Різне:

Реалізація багатопочності із фоновим робітником

За умовчуванням, ваш застосунок постійно виконує певний код, при чому цей код працює у тому ж потоці, що й застосунок. Тобто під час роботи цього коду не відбувається нічого іншого, зокрема оновлення інтерфейсу.

Для людей, які тільки почали займатися програмування, досить несподівано побачити, що їхня програма зависає, виконуючи завдання, що займає більше секунди. Як наслідок чимало людей запитують, як оновити ідентифікатор виконання, побачивши, що він оновлюється тільки після завершення завдання.

Розв'язанням цієї проблеми є багатопоточність. Попри те, що її досить просто реалізувати в C#, вона має чимало підводних каменів, незрозумілих для багатьох людей. Тож їм на допомогу приходить фоновий робітник (надалі BackgroundWorker). Завдяки йому набагато легше та простіше працювати з додатковим потоком у вашому застосунку.

Як працює BackgroundWorker

Найбільша проблема багатопоточності у Windows застосунках є те, що ви не можете змінювати інтерфейс з іншого потоку, якщо ви зробите це, то застосунок негайно завершить роботу. Замість цього вам потрібно викликати у головному потоці метод, що виконає потрібні зміни. Це досить громіздко, але не із BackgroundWorker.

При виконанні завдання на неосновному потоці, ви, як правило, контактуєте з рештою застосунку одним із двох способів. Першим, коли ви хочете показати прогрес виконання, та другим, коли ви хочете показати результат роботи. BackgroundWorker працює навколо цієї концепції, тож він має дві події ProgressChanged та RunWorkerCompleted.

Третя подія називається DoWork. В ній не можна змінювати нічого в інтерфейсі. Замість цього слід викликати метод ReportProgress(), який викликає подію ProgressChanged, з якої можна оновити інтерфейс. Після завершення роботи викликається подія RunWorkerCompleted.

Підсумуймо: подія DoWork виконую всю важку роботу, весь код цієї події працює в неосновному потоці, тож він не може редагувати інтерфейс. Замість цього ви поміщаєте дані (з інтерфейсу або ще звідкись) в подію, використовуючи параметер методу RunWorkerAsync(). А для того, щоб повідомити результат роботи, слід встановити його, як значення властивості e.Result.

Події ProgressChanged та RunWorkerCompleted , навпаки , виконуються в тому потоці, в якому було створено BackgroundWorker, зазвичай, – це основний потік. Тож в них можна редагувати інтерфейс. То ж зв'язок між фоновим процесом та застосунком повинен здійснюватися в методі ReportProgress().

Це була теорія, однак попри простоту BackgroundWorker, все одно важливо розуміти, як він працює. Помилки при роботі з багатопочністю можуть призвести до серйозних проблем.

Приклад BackgroundWorker

Досить теорії – перейдімо до прикладу. В ньому нам потрібно перевірити всі числа від 0 до 10 000 на те, чи вони кратні числу 7. Завдання просте, але тривале, однак не для сучасних комп'ютерів, тож щоб ускладнити його, я додав затримку в одну мілісекунду між кожною ітерацією.

Наш простий застосунок має дві кнопки. При натиску на першу кнопку завдання виконається синхронно, а другу - асинхронно, використовуючи BackgroundWorker. Цей приклад чудово показує, навіщо потрібен додатковий потік. Ось, як виглядає код:

<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BackgroundWorkerSample" Height="300" Width="375">
    <DockPanel Margin="10">
        <DockPanel DockPanel.Dock="Top">
            <Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
            <Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
        </DockPanel>
        <ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />

        <ListBox Name="lbResults" Margin="0,10" />

    </DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;

namespace WpfTutorialSamples.Misc
{
	public partial class BackgroundWorkerSample : Window
	{
		public BackgroundWorkerSample()
		{
			InitializeComponent();
		}

		private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			int max = 10000;
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			int result = 0;
			for(int i = 0; i < max; i++)
			{
				if(i % 42 == 0)
				{
					lbResults.Items.Add(i);
					result++;
				}
				System.Threading.Thread.Sleep(1);
				pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
			}
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
		}

		private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			BackgroundWorker worker = new BackgroundWorker();
			worker.WorkerReportsProgress = true;
			worker.DoWork += worker_DoWork;
			worker.ProgressChanged += worker_ProgressChanged;
			worker.RunWorkerCompleted += worker_RunWorkerCompleted;
			worker.RunWorkerAsync(10000);
		}

		void worker_DoWork(object sender, DoWorkEventArgs e)
		{
			int max = (int)e.Argument;
			int result = 0;
			for(int i = 0; i < max; i++)
			{
				int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
				if(i % 42 == 0)
				{
					result++;
					(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
				}
				else
					(sender as BackgroundWorker).ReportProgress(progressPercentage);
				System.Threading.Thread.Sleep(1);

			}
			e.Result = result;
		}

		void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			pbCalculationProgress.Value = e.ProgressPercentage;
			if(e.UserState != null)
				lbResults.Items.Add(e.UserState);
		}

		void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
		}

	}
}

XAML частина містить дві кнопки, одна з яких запускає процес синхронно, а інша асинхронно, ListBox, що показує числа, кратні цифрі сім, та ProgressBar, що показує прогрес виконання завдання.

В C# коді ми почали з синхронного обробника подій. Як згадано, він зациклюється в проміжку від 0 до 10 000, із невеличким проміжком між кожною ітерацією. Якщо число кратне 7, то ми додаємо його до списку. Крім того, в кожній ітерації ми оновлюємо ProgressBar. Після завершення виконання ми надішлемо користувачу повідомленню про кількість чисел, що відповідають умові.

Якщо ви запустите застосунок та натиснете першу кнопку, то ви нічого не побачите, аж доки не закінчиться процес:

В список не додано жодного об'єкта, немає жодного прогресу в ProgressBar, а кнопку навіть не відпущено. Це підтверджує те, що відтоді, як миша була натиснута на кнопку пройшов один кадр.

При натисканні на другу кнопку, ми використаємо BackgroundWorker. Як ви бачите з коду, в цьому випадку ми робимо майже те саме, але іншим способом. Вся важка робота тепер поміщається в події DoWork, яку BackgroundWorker викликає після запуску методу RunWorkerAsync(). Цей метод приймає ввід із вашого застосунку, який потім використає фоновий працівник, як вже було сказано раніше.

Як вже згадано, не можна оновлювати інтерфейс у події DoWork. Замість цього слід викликати метод ReportProgress. Якщо число кратне 7, то ми викликаємо цей метод, щоб додати число до списку, інакше ми просто повідомляємо у ньому поточний прогрес, щоб оновити ProgressBar.

Після перевірки всіх чисел, як значення властивості e.Result ми задаємо результат роботи. Після цього ми використаємо її в події RunWorkerCompleted, де ми покажемо результат користувачеві. Це трохи громіздко, але ми не можемо змінювати інтерфейс в події DoWork, тож залишається цей спосіб.

Як ви бачите, результат куди зручніший для користувача:

Вікно більше не зависає, кнопка натиснута, але не утримується, список відповідних чисел оновлюється, як і ProgressBar. Інтерфейс виконує всі свої функції.

Ввід та вивід

Запам'ятайте, що як ввід через метод RunWorkerAsync(), так і вивід через властивість e.Result події DoWor, мають тип object. Тож ви можете вибрати для них будь-який тип даних. Наш приклад досить простий, оскільки як ввід, так і вивід – це одне ціле число, однак вони можуть бути куди складнішими.

Для цього використовують складні типи, зокрема користувацькі структури та класи. Завдяки цьому ви можете повідомляти як BackgroundWorker, так і інтерфейсу, практично будь-яку інформацію.

Те саме стосується методу ReportProgress. Другий параметер, під назвою userState має тип object, тож ви можете помістити в нього все, що забажаєте.

Підсумок

BackgroundWorker – це чудове рішення для реалізації багатопоточності у вашому застосунку, оскільки він дуже простий. В цьому розділі ми дізналися про основні функції BackgroundWorker, однак є ще одна, громіздка функції – скасування роботи. Про неї ми дізнаємось у наступній статті


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!