openplanning

Hướng dẫn lập trình đa luồng trong C#

  1. Khái niệm về đa luồng (Multithreading)
  2. Truyền tham số vào Thread
  3. Thread sử dụng phương thức không tĩnh
  4. ThreadStart Delegate
  5. Thread với các code nặc danh
  6. Đặt tên cho Thread
  7. Độ ưu tiên giữa các Thread
  8. Sử dụng Join()
  9. Sử dụng Yield()

1. Khái niệm về đa luồng (Multithreading)

Đa luồng là một khái niệm quan trọng trong các ngôn ngữ lập trình, và C# cũng vậy, đó là cách tạo ra các luồng chương trình chạy song song với nhau. Để đơn giản bạn hãy xem một ví dụ sau:
HelloThread.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class HelloThread
    {
        public static void Main(string[] args)
        { 
            Console.WriteLine("Create new Thread...\n");   
            // Tạo ra một Thread con, để chạy song song với Thread chính (main thread).
            Thread newThread = new Thread(WriteB); 
            Console.WriteLine("Start newThread...\n");

            // Kích hoạt chạy newThread.
            newThread.Start(); 
            Console.WriteLine("Call Write('-') in main Thread...\n");

            // Trong Thread chính ghi ra các ký tự '-'
            for (int i = 0; i < 50; i++)
            {
                Console.Write('-'); 
                // Ngủ (sleep) 70 mili giây.
                Thread.Sleep(70);
            } 
            Console.WriteLine("Main Thread finished!\n");
            Console.Read();
        }  
        public static void WriteB()
        {
            // Vòng lặp 100 lần ghi ra ký tự 'B'
            for (int i = 0; i < 100; i++)
            {
                Console.Write('B');

                // Ngủ 100 mili giây
                Thread.Sleep(100);
            } 
        }
    }  
}
Và bạn chạy class này:
Nguyên tắc hoạt động của luồng (Thread) chỉ được giải thích trong minh hoạ dưới đây:

2. Truyền tham số vào Thread

Ở phần trên bạn đã làm quen với ví dụ HelloThread, bạn đã tạo ra một đối tượng bao lấy (wrap) một phương thức tĩnh để thực thi phương thức này song song với luồng (thread) cha.
Phương thức tĩnh có thể trở thành một tham số truyền vào Constructor của lớp Thread nếu phương thức đó không có tham số, hoặc có một tham số duy nhất kiểu object.
// Một phương thức tĩnh, không có tham số.
public static void LetGo()
{
      // ...
}

// Phương thức tĩnh có 1 tham số duy nhất, và kiểu là object.
public static void GetGo(object value)
{
      // ...
}
Ví dụ tiếp theo này, tôi sẽ tạo ra một Thread bao lấy một phương thức tĩnh có 1 tham số (kiểu object). Chạy thread và truyền giá trị cho tham số.
MyWork.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class MyWork
    { 
        public static void DoWork(object ch)
        {
            for (int i = 0; i < 100; i++)
            {  
                Console.Write(ch); 
                // Ngủ (sleep) 50 mili giây.
                Thread.Sleep(50);
            }
        } 
    }
}
ThreadParamDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadParamDemo
    { 
        public static void Main(string[] args)
        {  
            Console.WriteLine("Create new thread.. \n");

            // Tạo một đối tượng Thread bao lấy (wrap) phương thức tĩnh MyWork.DoWork
            Thread workThread = new Thread(MyWork.DoWork); 
            Console.WriteLine("Start workThread...\n");

            // Chạy workThread,
            // và truyền vào tham số cho phương thức MyWork.DoWork.
            workThread.Start("*"); 

            for (int i = 0; i < 20; i++)
            {
                Console.Write("."); 
                // Ngủ 30 giây.
                Thread.Sleep(30);
            } 
            Console.WriteLine("MainThread ends");
            Console.Read();
        }
    } 
}
Chạy lớp ThreadParamDemo:

3. Thread sử dụng phương thức không tĩnh

Bạn cũng có thể tạo một luồng (thread) sử dụng các phương thức thông thường. Xem ví dụ:
Worker.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace MultithreadingTutorial
{
    class Worker
    {
        private string name;
        private int loop;
        public Worker(string name, int loop)
        {
            this.name = name;
            this.loop = loop;
        }
        public void DoWork(object value)
        {
            for (int i = 0; i < loop; i++)
            {
                Console.WriteLine(name + " working " + value);
                Thread.Sleep(50);
            }
        }
    }
}
WorkerTest.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class WorkerTest
    { 
        public static void Main(string[] args)
        {
            Worker w1 = new Worker("Tom",10);

            // Tạo một đối tượng Thread.
            Thread workerThread1 = new Thread(w1.DoWork);

            // Truyền tham số vào cho phương thức DoWork.
            workerThread1.Start("A"); 

            Worker w2 = new Worker("Jerry",15);

            // Tạo một đối tượng Thread.
            Thread workerThread2 = new Thread(w2.DoWork);

            // Truyền tham số vào phương thức DoWork.
            workerThread2.Start("B");  
            Console.Read();
        }
    } 
}
Chạy ví dụ:
Tom working A
Jerry working B
Jerry working B
Tom working A
Tom working A
Jerry working B
Tom working A
Jerry working B
Tom working A
Jerry working B
Tom working A
Jerry working B
Tom working A
Jerry working B
Jerry working B
Tom working A
Tom working A
Jerry working B
Jerry working B
Tom working A
Jerry working B
Jerry working B
Jerry working B
Jerry working B
Jerry working B

4. ThreadStart Delegate

ThreadStart là một class ủy quyền (Delegate), nó được khởi tạo bằng cách bao lấy một phương thức. Và nó được truyền vào như một tham số để khởi tạo đối tượng Thread.
Với .Net < 2.0, để khởi động (start) một luồng (thread), bạn cần tạo ThreadStart, nó là một delegate.

Bắt đầu từ phiên bản 2.0 của .NET Framework, không cần thiết để tạo ra một đối tượng ủy nhiệm (ThreadStart) một cách rõ ràng. Bạn chỉ cần chỉ định tên của các phương thức trong constructor của Thread.
Programmer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class Programmer
    {
        private string name;
        public Programmer(string name)  
        {
            this.name= name;
        } 
        // Đây là một phương thức không tĩnh, không tham số.
        public void DoCode()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(name + " coding ... ");
                Thread.Sleep(50);
            } 
        }
    } 
}
ThreadStartDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;


namespace MultithreadingTutorial
{
    class ThreadStartDemo
    { 
        public static void Main(string[] args)
        {
            // Tạo một đối tượng ThreadStart bao lấy 1 phương thức tĩnh.
            // (Nó chỉ có thể bao lấy các phương thức không tham số)
            // (Nó là một đối tượng được ủy quyền để thực thi phương thức).
            ThreadStart threadStart1 = new ThreadStart(DoWork);

            // Tạo một thread bao lấy (wrap) threadStart1.
            Thread workThread = new Thread(threadStart1);

            // Gọi start thread
            workThread.Start();

            // Khởi tạo một đối tượng Programmer.
            Programmer tran = new Programmer("Tran");

            // Bạn cũng có thể tạo ra đối tượng ThreadStart bao lấy phương thức không tĩnh.
            // (ThreadStart chỉ có thể bao lấy các phương thức không có tham số)
            ThreadStart threadStart2 = new ThreadStart(tran.DoCode);

            // Tạo một Thread bao lấy threadStart2.
            Thread progThread = new Thread(threadStart2); 
            progThread.Start();

            Console.WriteLine("Main thread ends");
            Console.Read();
        } 
        public static void DoWork()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("*");
                Thread.Sleep(100);
            }                
        }
    } 
}
Chạy ví dụ:
Main thread ends
*Tran coding ...
Tran coding ...
Tran coding ...
*Tran coding ...
*Tran coding ...
*******

5. Thread với các code nặc danh

Ở các phần trên bạn đã tạo ra các Thread sử dụng một phương thức cụ thể. Bạn có thể tạo ra một thread để thực thi một đoạn code bất kỳ.
// Sử dụng delegate() để tạo ra môt phương thức nặc danh.
delegate()
{
     //  ...
}
Ví dụ:
ThreadUsingSnippetCode.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadUsingSnippetCode
    { 
        public static void Main(string[] args) 
        { 
            Console.WriteLine("Create thread 1"); 
            // Tạo ra một thread để thực thi một đoạn code.
            Thread newThread1 = new Thread(
                delegate()
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine("Code in delegate() " + i);
                        Thread.Sleep(50);
                    } 
                }
            ); 
            Console.WriteLine("Start newThread1");  
            // Start thread. 
            newThread1.Start(); 
            Console.WriteLine("Create thread 2");

            // Tạo ra một thread để thực thi một đoạn code.
            Thread newThread2 = new Thread(
                delegate(object value)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine("Code in delegate(object) " + i + " - " + value);
                        Thread.Sleep(100);
                    } 
                }
            ); 
            Console.WriteLine("Start newThread2");

            // Bắt đầu thread 2.
            // Truyền giá trị vào cho delegate().
            newThread2.Start("!!!");  
            Console.WriteLine("Main thread ends");
            Console.Read(); 
        }
    } 
}
Chạy ví dụ:
Create thread 1
Start newThread1
Create thread 2
Start newThread2
Main thread ends
Code in delegate() 0
Code in delegate(object) 0 - !!!
Code in delegate() 1
Code in delegate() 2
Code in delegate(object) 1 - !!!
Code in delegate() 3
Code in delegate(object) 2 - !!!
Code in delegate() 4
Code in delegate() 5
Code in delegate(object) 3 - !!!
Code in delegate() 6
Code in delegate() 7
Code in delegate(object) 4 - !!!
Code in delegate() 8
Code in delegate() 9
Code in delegate(object) 5 - !!!
Code in delegate(object) 6 - !!!
Code in delegate(object) 7 - !!!
Code in delegate(object) 8 - !!!
Code in delegate(object) 9 - !!!

6. Đặt tên cho Thread

Trong lập trình đa luồng bạn có thể chủ động đặt tên cho luồng (thread), nó thực sự có ích trong trường hợp gỡ lỗi (Debugging), để biết đoạn code đó đang được thực thi trong thread nào.

Trong một thread bạn có thể gọi Thread.CurrentThread.Name để lấy ra tên của luồng đang thực thi tại thời điểm đó.

Xem ví dụ minh họa:
NamingThreadDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class NamingThreadDemo
    { 
        public static void Main(string[] args)
        {
            // Sét đặt tên cho thread hiện thời
            // (Đang là thread chính).
            Thread.CurrentThread.Name = "Main"; 
            Console.WriteLine("Code of "+ Thread.CurrentThread.Name); 
            Console.WriteLine("Create new thread");

            // Tạo một thread.
            Thread letgoThread = new Thread(LetGo);

            // Đặt tên cho thread này.
            letgoThread.Name = "Let's Go"; 
            letgoThread.Start();

            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Code of " + Thread.CurrentThread.Name);
                Thread.Sleep(30);
            } 
            Console.Read();
        } 
        public static void LetGo()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Code of " + Thread.CurrentThread.Name);
                Thread.Sleep(50);
            }
        }
    } 
}
Chạy ví dụ:
Code of Main
Create new thread
Code of Main
Code of Let's Go
Code of Main
Code of Let's Go
Code of Main
Code of Main
Code of Main
Code of Let's Go
Code of Let's Go
Code of Let's Go
Code of Let's Go
Code of Let's Go
Code of Let's Go
Code of Let's Go
Code of Let's Go

7. Độ ưu tiên giữa các Thread

Trong C# có 5 mức độ ưu tiên của một luồng, chúng được định nghĩa trong enum ThreadPriority.
** ThreadPriority enum **
enum ThreadPriority {
    Lowest,
    BelowNormal,
    Normal,
    AboveNormal,
    Highest
}
Thông thường với các máy tính tốc độ cao, nếu các luồng chỉ làm số lượng công việc ít, bạn rất khó phát hiện ra sự khác biệt giữa các luồng có ưu tiên cao và luồng có ưu tiên thấp.

Ví dụ dưới đây có 2 luồng, mỗi luồng in ra 100K dòng text (Một số lượng đủ lớn để thấy sự khác biệt).
ThreadPriorityDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadPriorityDemo
    {
        private static DateTime endsDateTime1;
        private static DateTime endsDateTime2; 
        public static void Main(string[] args)
        {
            endsDateTime1 = DateTime.Now;
            endsDateTime2 = DateTime.Now;

            Thread thread1 = new Thread(Hello1);

            // Sét độ ưu tiên cao nhất cho thread1
            thread1.Priority = ThreadPriority.Highest; 
            Thread thread2 = new Thread(Hello2);

            // Sét độ ưu tiên thấp nhất cho thread2.
            thread2.Priority = ThreadPriority.Lowest; 
            thread2.Start(); thread1.Start(); 
            Console.Read();
        }  
        public static void Hello1()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("Hello from thread 1: "+ i);
            }
            // Thời điểm thread1 kết thúc.
            endsDateTime1 = DateTime.Now; 
            PrintInterval();
        } 
        public static void Hello2()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("Hello from thread 2: "+ i);
            }
            // Thời điểm thread2 kết thúc.
            endsDateTime2 = DateTime.Now; 
            PrintInterval();
        } 
        private static void PrintInterval()
        {
            // Khoảng thời gian (Mili giây)
            TimeSpan interval = endsDateTime2 - endsDateTime1; 
            Console.WriteLine("Thread2 - Thread1 = " + interval.TotalMilliseconds + " milliseconds");
        }
    } 
}
Chạy ví dụ:

8. Sử dụng Join()

Thread.Join() là một method thông báo rằng hãy chờ thread này hoàn thành rồi thread cha mới được tiếp tục chạy.
ThreadJoinDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;


namespace MultithreadingTutorial
{
    class ThreadJoinDemo
    { 
        public static void Main(string[] args)
        {
            Console.WriteLine("Create new thread"); 
            Thread letgoThread = new Thread(LetGo); 
            
            // Bắt đầu Thread (start thread).
            letgoThread.Start();

            // Nói với thread cha (ở đây sẽ là Main thread)
            // hãy chờ cho letgoThread hoàn thành rồi mới tiếp tục chạy.
            letgoThread.Join();
            
            // Dòng code này phải chờ cho letgoThread hoàn thành, rồi mới được chạy.
            Console.WriteLine("Main thread ends");
            Console.Read();
        }  
        public static void LetGo()
        {
            for (int i = 0; i < 15; i++)
            {
                Console.WriteLine("Let's Go " + i);
            }
        }
    } 
}
Chạy ví dụ:
Create new thread
Let's Go 0
Let's Go 1
Let's Go 2
Let's Go 3
Let's Go 4
Let's Go 5
Let's Go 6
Let's Go 7
Let's Go 8
Let's Go 9
Let's Go 10
Let's Go 11
Let's Go 12
Let's Go 13
Let's Go 14
Main thread ends

9. Sử dụng Yield()

Về mặt lý thuyết, "Yield" có nghĩa là để cho đi, từ bỏ, đầu hàng. Một luồng Yield nói với hệ điều hành rằng nó sẵn sàng để cho các thread khác được sắp xếp ở vị trí của nó. Điều này cho thấy rằng nó không phải làm một cái gì đó quá quan trọng. Lưu ý rằng nó chỉ là một gợi ý, mặc dù, và không đảm bảo có hiệu lực ở tất cả.

Như vậy phương thức Yield() được sử dụng khi bạn bạn thấy rằng thread đó đang rảnh rỗi, nó không phải làm việc gì quan trọng, nên nó gợi ý hệ điều hành tạm thời nhường quyền ưu tiên cho các luồng khác.
Ví dụ dưới đây, có 2 luồng, mỗi luồng in ra một dòng văn bản 100K lần (con số đủ lớn để thấy sự khác biệt). Một luồng được sét độ ưu tiên cao nhất và một luồng được sét độ ưu tiên ít nhất. Đo khoảng thời gian kết thúc của 2 luồng.
ThreadYieldDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadYieldDemo
    {

        private static DateTime importantEndTime;
        private static DateTime unImportantEndTime;

        public static void Main(string[] args)
        {
            importantEndTime = DateTime.Now;
            unImportantEndTime = DateTime.Now;

            Console.WriteLine("Create thread 1");

            Thread importantThread = new Thread(ImportantWork);

            // Sét đặt quyền ưu tiên cao nhất cho luồng này.
            importantThread.Priority = ThreadPriority.Highest; 

            Console.WriteLine("Create thread 2");

            Thread unImportantThread = new Thread(UnImportantWork);

            // Sét đặt quyền ưu tiên thấp nhất cho luồng này.
            unImportantThread.Priority = ThreadPriority.Lowest; 

            // Start threads. 
            unImportantThread.Start();
            importantThread.Start();
           

            Console.Read();

        }

        // Một việc quan trọng, đòi hỏi ưu tiên cao.
        public static void ImportantWork()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("\n Important work " + i); 

                // Thông báo với hệ điều hành, luồng này sẽ
                // nhường độ ưu tiên cho các luồng khác.
                Thread.Yield();
            }
            // Thời điểm thread này kết thúc.
            importantEndTime = DateTime.Now;
            PrintTime();
        }

        public static void UnImportantWork()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("\n  -- UnImportant work " + i); 
            }
            // Thời điểm thread này kết thúc.
            unImportantEndTime = DateTime.Now;
            PrintTime();
        }

        private static void PrintTime()
        { 
            // Khoảng thời gian (Mili giây)
            TimeSpan interval = unImportantEndTime - importantEndTime; 
      
            Console.WriteLine("UnImportant Thread - Important Thread = " + interval.TotalMilliseconds +" milliseconds");
        }
         
    }

}
Chạy class trên trong trường hợp không có Thread.Yield():
Chạy class trên trong trường hợp luồng ưu tiên cao hơn liên tục gọi Thread.Yield() để yêu cầu hệ thống tạm thời nhường ưu tiên sang các luồng khác.