This article has been localized into Vietnamese by the community.
Đ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 ProgressChanged và RunWorkerCompleted.
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 ProgressChanged và RunWorkerCompleted 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
Lý thuyết xong rồi - giờ thì tới thực hành. Trong ví dụ đầu tiên này, ta sẽ thực hiện một ví dụ khá đơn giản nhưng tốn thời gian. Đó là kiểm tra các số từ 0 đến 10.000 có chia hết cho số 7 hay không. Với sức mạnh của các máy tính hiện nay nó là một công việc dễ dàng, vì vậy ta sẽ làm cho nó tồn thời gian hơn để bạn hiểu hơn những gì mà bạn vừa đọc về đa luồng. Tôi đã thêm một khoảng delay nhỏ 1 mili giây trong mỗi lần lặp.
Ví dụ dưới đây ta sẽ tạo ra hai button: Một cái thực hiện công việc đồng bộ (trên cùng một luồng) và cái còn lại thực hiện công việc cùng với một BackgroundWorker và thực hiện trên một luồng khác. Điều này giúp bạn thấy được sự cần thiết của multi thread khi thực hiện các công việc tốn thời gian. Đoạn code sẽ như sau:
<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 code XAML ta sẽ có hai nút, một cái chạy đồng bộ (ngay trên UI thread) và cái còn lại chạy bất đồng bộ (trên một background thread), một ListBox control dùng để hiển thị tất cả các số đã tính toán và sau đó một ProgressBar control ở bên dưới sẽ hiển thị ra...Uhm, Quá trình tính toán!
Trong Code-behind, ta bắt đầu với việc xử lý các sự kiện một cách đồng bộ. Như đã đề cập, nó lặp từ 0 đến 10000 với một khoảng delay nhỏ trong mỗi lần lặp, và nếu số đó chia hết cho 7 thì ta sẽ add nó vào list. Trong mỗi lần lặp như thế ta cũng sẽ thực hiện cập nhật ProgressBar và khi hoàn tất ta sẽ hiển thị một thông báo cho người dùng biết về số lượng số chia hết cho 7 được tìm thấy.
Nếu bạn chạy ứng dụng và nhấn vào button đầu tiên, nó sẽ trông như bên dưới, bất kể tiến trình của bạn đang thực hiện tới đâu:
Không có bất cứ item nào trong list và cũng không có process nào được thể hiện trên ProgressBar, và button mà bạn vừa nhấn không được release (thả ra), điều đó có nghĩa rằng không có một lần cập nhật giao diện nào xảy ra kể từ khi bạn nhấn button.
Thay vào đó khi bạn nhấn button thứ hai nó sẽ sử dụng BackgroundWorker. Như bạn có thể thấy ở trong code, trông giống với code chạy đồng bộ nhưng theo một cách hơi khác. Tất cả công việc kiểm tra được đưa vào event DoWork, event này sẽ chạy ngay sau khi bạn chạy phương thức RunWorkerAsync(). Phương thức này sẽ lấy input từ ứng dụng của bạn để worker có thể sử dụng, ta sẽ nói phần này 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.