TOC

This article has been localized into Vietnamese by the community.

Misc.:

Đa luồng với BackgroundWorker

Theo mặc định, mỗi lần ứng dụng của bạn thực thi một đoạn code, code này được chạy trên cùng một luồng với chính ứng dụng đó. Điều này có nghĩa là trong khi code này đang chạy, không có gì khác xảy ra trong ứng dụng của bạn, kể cả việc cập nhật giao diện người dùng của bạn.

Điều này khá bất ngờ với những người mới làm quen với lập trình Windows, khi họ lần đầu tiên làm điều gì đó mất hơn một giây và nhận ra rằng ứng dụng của họ thực sự bị treo trong khi làm như vậy. Kết quả là rất nhiều bài đăng trên diễn đàn nản lòng từ những người đang cố gắng chạy một quy trình dài trong khi cập nhật thanh tiến trình, chỉ để nhận ra rằng thanh tiến trình không được cập nhật cho đến khi quá trình được chạy xong.

Giải pháp cho tất cả những điều này là sử dụng nhiều luồng và trong khi C# làm cho việc này khá dễ thực hiện, thì đa luồng đi kèm với rất nhiều cạm bẫy và đối với nhiều người, chúng không thể hiểu được. Đây là lúc BackgroundWorker phát huy tác dụng - nó làm cho nó đơn giản, dễ dàng và nhanh chóng để làm việc với một luồng bổ sung trong ứng dụng của bạn.

Cách thức hoạt động của BackgroundWorker

Khái niệm khó khăn nhất về đa luồng trong ứng dụng Windows là thực tế là bạn không được phép thay đổi giao diện người dùng từ một luồng khác - nếu bạn làm như vậy, ứng dụng sẽ ngay lập tức bị sập. Thay vào đó, bạn phải gọi một phương thức trên luồng UI (chính) để thực hiện các thay đổi mong muốn. Đây là tất cả một chút rườm rà, nhưng không phải khi bạn sử dụng BackgroundWorker.

Khi thực hiện một tác vụ trên một luồng khác, bạn thường phải giao tiếp với phần còn lại của ứng dụng trong hai tình huống: Khi bạn muốn cập nhật nó để cho biết bạn đang ở đâu trong quá trình, và sau đó tất nhiên là khi nhiệm vụ được thực hiện và bạn muốn hiển thị kết quả BackgroundWorker được xây dựng xung quanh ý tưởng này, và do đó đi kèm với hai sự kiện ProgressChangedRunWorkerCompleted.

Sự kiện thứ ba được gọi là DoWork và quy tắc chung là bạn không thể chạm vào bất cứ thứ gì trong UI từ sự kiện này. Thay vào đó, bạn gọi phương thức ReportProgress(), từ đó làm tăng sự kiện ProgressChanged, từ đó bạn có thể cập nhật giao diện người dùng. Khi bạn đã hoàn tất, bạn chỉ định một kết quả cho nhân viên và sau đó sự kiện RunWorkerCompleted được đưa ra.

Vì vậy, để tóm tắt, sự kiện DoWork đảm nhận tất cả các công việc khó khăn. Tất cả các mã trong đó được thực thi trên một luồng khác và vì lý do đó, bạn không được phép chạm vào giao diện người dùng từ nó. Thay vào đó, bạn đưa dữ liệu (từ UI hoặc các nơi khác) vào sự kiện bằng cách sử dụng đối số trên phương thức RunWorkerAsync() và dữ liệu kết quả từ nó bằng cách gán nó cho thuộc tính e.Result.

Các ProgressChangedRunWorkerCompleted sự kiện, mặt khác, được thực hiện trên cùng một sợi như BackgroundWorker được tạo ra trên, mà thường sẽ là chính chủ đề / UI và do đó bạn được phép cập nhật giao diện người dùng từ họ. Do đó, giao tiếp duy nhất có thể được thực hiện giữa tác vụ chạy nền và giao diện người dùng của bạn là thông qua phương thức ReportProgress().

Đó là rất nhiều lý thuyết, nhưng mặc dù BackgroundWorker rất dễ sử dụng, nhưng điều quan trọng là phải hiểu cách thức và những gì nó làm, vì vậy bạn không vô tình làm điều gì đó sai - như đã nêu, lỗi trong đa luồng có thể dẫn đến một số vấn đề khó chịu.

Một ví dụ về BackgroundWorker

Đủ với lý thuyết - hãy xem tất cả những gì về nó. Trong ví dụ đầu tiên này, chúng tôi muốn thực hiện một công việc khá đơn giản nhưng tốn thời gian. Mỗi số từ 0 đến 10.000 được kiểm tra xem nó có chia hết cho số 7. Đây thực sự là một miếng bánh cho các máy tính nhanh hiện nay, vì vậy để làm cho nó tốn nhiều thời gian hơn và do đó dễ dàng hơn để chứng minh quan điểm của chúng tôi, tôi đã thêm một độ trễ một mili giây trong mỗi lần lặp.

Ứng dụng mẫu của chúng tôi có hai nút: Một nút sẽ thực hiện nhiệm vụ một cách đồng bộ (trên cùng một luồng) và một nút sẽ thực hiện tác vụ với BackgroundWorker và do đó trên một luồng khác. Điều này sẽ làm cho nó rất dễ dàng để thấy sự cần thiết của một chủ đề bổ sung khi thực hiện các nhiệm vụ tốn thời gian. Code trông như thế này:

<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);
		}

	}
}

Phần XAML bao gồm một vài nút, một nút để chạy quy trình một cách đồng bộ (trên luồng UI) và một để chạy không đồng bộ (trên luồng nền), điều khiển ListBox để hiển thị tất cả các số được tính toán và sau đó điều khiển ProgressBar trong phía dưới cửa sổ để hiển thị ... tốt, tiến độ!

Trong Code-behind, chúng tôi bắt đầu với trình xử lý sự kiện đồng bộ. Như đã đề cập, nó lặp từ 0 đến 10.000 với độ trễ nhỏ trong mỗi lần lặp và nếu số đó chia hết cho số 7, thì chúng ta thêm nó vào danh sách. Trong mỗi lần lặp, chúng tôi cũng cập nhật ProgressBar và sau khi hoàn tất, chúng tôi sẽ hiển thị một thông báo cho người dùng về số lượng số được tìm thấy.

Nếu bạn chạy ứng dụng và nhấn nút đầu tiên, nó sẽ trông như thế này, bất kể bạn đang ở đâu trong quá trình:

Không có mục nào trong danh sách và, không có tiến trình nào trên ProgressBar và nút thậm chí chưa được phát hành, điều đó chứng tỏ rằng chưa có một bản cập nhật nào cho UI kể từ khi chuột được nhấn xuống nút.

Nhấn nút thứ hai thay vào đó sẽ sử dụng phương pháp BackgroundWorker. Như bạn có thể thấy từ code, chúng tôi làm khá giống nhau, nhưng theo một cách hơi khác. Tất cả công việc khó khăn hiện được đặt trong sự kiện DoWork, mà trương trình gọi sau khi bạn chạy phương thức RunWorkerAsync(). Phương pháp này lấy đầu vào từ ứng dụng của bạn mà trương trình có thể sử dụng, như chúng ta sẽ nói về sau.

Như đã đề cập, chúng tôi không được phép cập nhật giao diện người dùng từ sự kiện DoWork. Thay vào đó, chúng tôi gọi phương thức ReportProgress trên worker. Nếu số hiện tại chia hết cho 7, chúng tôi sẽ đưa nó vào danh sách - nếu không, chúng tôi chỉ báo cáo tỷ lệ phần trăm tiến độ hiện tại, để có thể cập nhật ProgressBar.

Khi tất cả các số đã được kiểm tra, chúng tôi gán kết quả cho thuộc tính e.Result. Điều này sau đó sẽ được đưa đến sự kiện RunWorkerCompleted, nơi chúng tôi hiển thị nó cho người dùng. Điều này có vẻ hơi cồng kềnh, thay vì chỉ hiển thị cho người dùng ngay khi công việc được hoàn thành, nhưng một lần nữa, nó đảm bảo rằng chúng tôi không liên lạc với UI từ sự kiện DoWork, điều không được phép.

Kết quả là, như bạn có thể thấy, thân thiện với người dùng hơn nhiều:

Cửa sổ không còn bị treo, nút được bấm nhưng không bị chặn, danh sách các số có thể được cập nhật nhanh chóng và ProgressBar đang tăng đều đặn - giao diện trở nên nhạy hơn rất nhiều.

Input and output

Lưu ý rằng cả đầu vào, dưới dạng đối số được truyền cho phương thức RunWorkerAsync(), cũng như đầu ra, dưới dạng giá trị được gán cho thuộc tính e.Result của sự kiện DoWork, thuộc loại đối tượng. Điều này có nghĩa là bạn có thể gán bất kỳ loại giá trị nào cho chúng. Ví dụ của chúng tôi là cơ bản, vì cả đầu vào và đầu ra có thể được chứa trong một giá trị số nguyên duy nhất, nhưng việc có đầu vào và đầu ra phức tạp hơn là điều bình thường.

Điều này được thực hiện bằng cách sử dụng một loại phức tạp hơn, trong nhiều trường hợp một cấu trúc hoặc thậm chí một lớp mà bạn tự tạo và vượt qua. Bằng cách này, các khả năng là vô tận và bạn có thể vận chuyển nhiều dữ liệu phức tạp như bạn muốn giữa BackgroundWorker và ứng dụng/UI của bạn.

Điều này được thực hiện bằng cách sử dụng một loại phức tạp hơn, trong nhiều trường hợp một cấu trúc hoặc thậm chí một lớp mà bạn tự tạo và vượt qua. Bằng cách này, các khả năng là vô tận và bạn có thể vận chuyển nhiều dữ liệu phức tạp như bạn muốn giữa BackgroundWorker và ứng dụng / UI của bạn.

Tổng kết

BackgroundWorker là một công cụ tuyệt vời khi bạn muốn đa luồng trong ứng dụng của mình, chủ yếu vì nó rất dễ sử dụng. Trong chương này, chúng tôi đã xem xét một trong những điều được thực hiện rất dễ dàng bởi BackgroundWorker, đó là báo cáo tiến độ, nhưng hỗ trợ để hủy tác vụ đang chạy cũng rất tiện dụng. Chúng ta sẽ xem xét điều đó trong chương tiếp theo.

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!