Khi lập trình Windows Form đôi lúc chương trình sẽ bị “treo” (stop responding) một lúc khi bạn chạy các lệnh SQL, vòng lặp lớn,…. Nó thật là phiền phức. Không chỉ thế , người khác sẽ đánh giá chương trình của bạn thiếu tính chuyên nghiệp. Một điều như tôi và các bạn đang hướng tới.

Vấn đề

Tôi sẽ lấy một ví dụ đơn giản khi lập trình Windows Form như sau: “lồng 3 vòng lặp lại với nhau”

 int i, j, k, t; for (i = 0; i < 100; i++) { for (j = 0; j < 10000; j++) { for (k = 0; k < 10000; k++) { t = 1; t++; t--; } } } 

Nhìn lướt qua bạn cũng thấy được vòng lặp này lớn đến thế nào rồi! 100 * 10000 * 10000 quá to so với tốc độ CPU hiện tại. Để hoàn thành hết toàn bộ số vòng lặp này, CPU 2.4GHz mất tầm 100s để hoàn thành. Nếu bạn để đoạn code này trong một sự kiện nhấn phím, chương trình của bạn sẽ treo, treo cho đến khi 3 vòng lặp đó kết thúc.

Điều tương tự cũng xảy ra khi bạn execute một lệnh SQL, hoặc truy cập internet bằng socket.

Biểu hiện:

  • Cửa sổ treo, không di chuyển được, không sử dụng được các button trong cửa sổ.
  • Thanh tiêu đề có ghi dòng chữ: Not Responding.

Nguyên nhân

Nói tóm lại thì, bạn bị treo là do chương trình chỉ có 1 luồng. Khi 3 vòng lặp đó chạy, nó chiếm hết tài nguyên của chương trình, khiến chương trình không thể xử lý các sự kiện khác.

Stuck at this loop

Khi chương trình đang chạy, vòng lặp màu đỏ được chạy. Nó liên tục kiểm tra tương tác từ người dùng, nếu có, lập tức chúng sẽ xử lý khiến cho người dùng cảm thấy chương trình đang chạy. Khi không có tương tác nào, chương trình thực tế đang bị treo. Nhưng không sao, vì người dùng có tương tác với chương trình đâu.

Khi bấm nút, giả sử button1 nơi bạn cài đặt 3 vòng for lồng nhau kia, chương trình nhận được sự kiện nhấn phím, khu vực “xử lý sự kiện“ sẽ chuyển quyền điều khiển cho hàm button1_Click, tức là vòng lặp màu đen ở trên. Tôi nói chuyển nghĩa là, vòng lặp màu đỏ sẽ chờ đến khi button1_Click kết thúc, trả lại quyền điều khiển cho nó. Trong lúc button1_Click đang thực thi, các sự kiện khác sẽ không được xử lý. Dẫn đến tình trạng treo (Not responding) và bạn không thấy phản ứng gì từ chương trình cả. Các sự kiện chưa được xử lý sẽ dồn lại, chờ được giải quyết.

Như vậy, vấn đề nằm ở vòng lặp chính màu đỏ dừng lại khi gọi hàm button1_Click. Để giải quyết, chúng ta phải dùng cách nào đó để vòng lặp màu đỏ này tiếp tục hoạt động cho dù button1_Click chưa trả quyền điều khiển về cho nó.

Giải pháp

Phương án thứ nhất

May mắn thay, C# cung cấp cho chúng ta một hàm rất đặc biệt:

Application.DoEvents();

Hàm này sẽ tạm dừng vòng lặp màu đen lại. Tức là tạm dừng công việc xử lý 3 vòng lặp for lồng nhau, thay vào đó là vòng lặp màu đỏ tiếp tục chạy để xử lý các sự kiện từ người dùng. Khi không còn sự kiện nào nữa, hàm này trả quyền điều khiển lại cho hàm button1_Click để nó thực hiện tiếp công việc.

Vậy nên đặt hàm đó ở đâu, giả sử tôi đặt ở đây:

 for (i = 0; i < 100; i++) { Application.DoEvents(); for (j = 0; j < 10000; j++) { for (k = 0; k < 10000; k++) { t = 1; t++; t--; } } } 

Ở đầu vòng lặp biến i. Khi chạy, chương trình đã có thể phản ứng với các thao tác như thu nhỏ, phóng to rồi. Nhưng, nó thật sự không hoàn hảo vì vẫn còn giựt!

Lý do là vì sau khi gọi hàm DoEvents xong, chúng ta bước vào vòng lặp hai biến j và k. Hai vòng lặp này chiếm toàn quyền điều khiển. Nếu có một sự kiện được gửi tới lúc vòng lặp j, k đang thực hiện, các sự kiện đó sẽ phải chờ. Đến khi vòng lặp j, k kết thúc, biến i khởi động vòng lặp mới, hàm DoEvents được gọi, lúc này chương trình mới phản ứng lại thao tác của người dùng.

Với phân tích trên, bạn cũng hiểu các để chương trình đỡ giựt hơn rồi: “Thêm các lệnh DoEvents vào vòng lặp j, k”. Như sau:

 for (i = 0; i < 100; i++) { Application.DoEvents(); for (j = 0; j < 10000; j++) { Application.DoEvents(); for (k = 0; k < 10000; k++) { Application.DoEvents(); t = 1; t++; t--; } } } 

Woa, chương trình chạy ổn hơn rất nhiều. Phản ứng nhanh với thao tác của người dùng. Nhưng, chắc chắn rằng nó không nhanh như ban đầu.

Đó là phương án đầu tiên, nhanh nhất, nhưng vẫn chưa hoàn hảo, vì

  • Chỉ có thể cài đặt khi có vòng lặp for như thế. Nếu chỉ gọi một hàm, mà hàm đó làm treo chương trình thì cách này phá sản vì không biết để hàm DoEvents ở đâu hết.
  • Đoạn mã bị thêm vào rất nhiều lệnh DoEvents, trông khá là “urgly!”.

Phương án thứ hai

Chúng ta sẽ yêu cầu vòng lặp xử lý sự kiện (vòng lặp đỏ) tiếp tục chạy ngay cả khi 3 vòng lặp for lồng nhau kia chạy. Tức là chúng ta sẽ không trao quyền điều khiển cho vòng lặp màu đen nữa.

Để hiện thực hoá giải pháp này, chúng ta phải sử dụng đến tính năng multi-thread của hệ điều hành. Vòng lặp màu đen sẽ chạy song song với vòng lặp màu đỏ.

Multi-thread

Tư tưởng chính: khi gặp sự kiện button1_Click, chúng ta gọi hàm khởi động (hàm này sẽ chạy 3 vòng for kia) sau đó trở về vòng lặp đỏ luôn. Hàm khởi động sẽ quản lý quá trình 3 vòng for chạy. Như thế bạn đã đạt được mục tiêu khi mà vòng lặp đỏ tiếp tục chạy để xử lý thao tác từ phía người dùng.

Phần quan trọng nhất: làm sao để cài đặt tính năng này???

BackgroundWorker

Đây là 1 control trong C# giúp bạn cài đặt quá trình trên một cách dễ dàng. Hãy thêm một BackgroundWorker vào form, đặt tên là backgroundWorker1. Sau đó tạo hết tất cả 3 sự kiện DoWork, ProgressChangedRunWorkerCompleted với tên tương ứng. (sử dụng tên mặc định nhen).

Ngoài ra, bạn thêm vào form một ProgressBar tên progressBar1, một Button tên là button1 và 1 Label tên label1.

Ý nghĩa các sự kiện của BackgroundWorker:

  • DoWork: nơi mà tất cả đoạn code sẽ được chạy, cụ thể là 3 vòng lặp for lồng nhau kia. Những gì chạy ở hàm DoWork này sẽ không gây ảnh hưởng tới chương trình chính của bạn (vòng lặp đỏ). CHÚ Ý: nó không chỉ không ảnh hưởng, mà còn không được phép can thiệp tới chương trình chính. Hay có nghĩa, không được gọi, thay đổi thuộc tính bất kì control nào khác trong form.
  • ProgressChanged: sự kiện này nằm trong vòng lặp xử lý chính (màu đỏ). Do đó nó có thể thao tác với các control khác cách dễ dàng.
  • RunWorkerCompleted: sự kiện này cũng nằm trong vòng lặp xử lý chính, xảy ra khi hàm DoWork ở vòng lặp màu đen kết thúc.

OK, giờ tôi sẽ di chuyển 3 vòng for lồng nhau vào sự kiện DoWork.

 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { int i, j, k, t; for (i = 0; i < 100; i++) { for (j = 0; j < 10000; j++) { for (k = 0; k < 10000; k++) { t = 1; t++; t--; } } } } 

Nhưng làm sao để gọi sự kiện DoWork này? Bạn cần một hàm “khởi động” để làm điều này. Nhớ, không được gọi trực tiếp hàm DoWork. Nếu làm thế, DoWork sẽ chạy tại vòng lặp màu đỏ, và chương trình bị treo ngay lập tức. Hàm khởi động của chúng ta có tên là:

backgroundWorker1.RunWorkerAsync();

Khi gọi hàm này, DoWorker sẽ được gọi thực thi ở một thread (luồng) khác, trong khi đó thì vòng lặp xử lý chính vẫn tiếp tục hoạt động. Rồi, bạn hãy nhấn F5 và kiểm tra xem chương trình có còn LAG hay không!

Nhưng khả năng của BackgroundWorker không chỉ có thế. Tiếp theo chúng ta sẽ cài đặt để cho hàm DoWork tương tác với vòng lặp xử lý chính. Tất cả được thực hiện qua hàm:

backgroundWorker1.ReportProgress(i);

Bạn có thể gọi hàm này ở trong hàm DoWork mà không gặp vấn đề gì. Tôi đặt nó ở đầu vòng lặp i vì giá trị của nó chạy từ 0 tới 100, rất thích hợp để biểu diễn phần trăm cho thanh progress bar.

Sau khi gọi, một sự kiện sẽ được gửi tới vòng lặp chính. Và sự kiện đó là ProgressChanged. Bây giờ chúng ta sẽ chỉnh sửa hàm ProgressChanged:

 private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { label1.Text = "Working... " + e.ProgressPercentage.ToString() + "%"; progressBar1.Value = e.ProgressPercentage; } 

Giá trị i tôi truyền từ hàm DoWork nằm trong biến e.ProgressPercentage. Bạn thấy tôi cập nhật label1 và progressBar1 sang giá trị mới rồi đó. Sau khi viết xong, bạn hãy ấn F5 và thưởng thức.

Tương tự, hàm RunWorkerCompleted sẽ được gọi khi công việc đã hoàn tất:

 private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { label1.Text = "Completed!"; } 

Quá tiện lợi phải không nào. Vấn đề cuối cùng tôi muốn giới thiệu là khả năng kiểm tra tiến trình của BackgroundWorker qua hàm IsBusy. Tôi chỉnh lại hàm button1_Click như sau:

 if (backgroundWorker1.IsBusy) { MessageBox.Show("We're working, wait, please!"); return; } backgroundWorker1.RunWorkerAsync(); 

Như vậy, nếu công việc đang chạy mà người dùng nhấn button1, chúng ta sẽ thông bào rằng tác vụ chưa hoàn tất, hãy chờ thêm.

The final result

Tham khảo thêm tại: http://www.codeproject.com/Articles/841751/MultiThreading-Using-a-Background-Worker-Csharp

Post Views: 723

Share this:

Related

ĐỂ LẠI PHẢN HỒI CỦA BẠN TẠI ĐÂY

Phản hồi về bài viết này

NO COMMENTS

LEAVE A REPLY


*