Hướng dẫn lập trình đa luồng trong C#
Công ty Vĩnh Cửu tuyển dụng lập trình viên Java

1- Khái niệm đa luồng

Đ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 luồng con, để chạy song song với luồng chính.
            Thread newThread = new Thread(WriteB);

            Console.WriteLine("Start newThread...\n");

            // Kích hoạt chạy luồng newThread.
            newThread.Start();

            Console.WriteLine("Call Write('-') in main Thread...\n");

            // Trong luồng chính ghi ra các ký tự '-'
            for (int i = 0; i < 50; i++)
            {
                Console.Write('-');

                // Dừng lại 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');

                // Dừng lại 100 mili giây
                Thread.Sleep(100);
            }
                
        }
    }

   
}
Và bạn chạy class này:
The operating principle of the thread is explained in the following illustration:

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 cha.
Phương thức tĩnh có thể trở thành một tham số truyền vào Constructor của class 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()
{
     // Làm gì đó ở đây.
}

// Phương thức tĩnh có 1 tham số duy nhất, và kiểu là object.
public static void GetGo(object value)
{
      // Làm gì đó ở đây.
}
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++)
            {
                // Ghi ra màn hình
                Console.Write(ch);

                // Nghỉ 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 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ừng 30 giây.
                Thread.Sleep(30);
            }

            Console.WriteLine("MainThread ends");
            Console.Read();
        }
    }

}
Chạy class ThreadParamDemo.cs:

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 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("Tran",10);

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

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


            Worker w2 = new Worker("Marry",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ụ:

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.
Chú ý: Đây là cách của .Net phiên bản trước 2.0, để bắt đầu một thread sử dụng một phương thức tĩnh, sử dụng tên class và tên phương thức khi bạn tạo đối tượng ủy nhiệm ThreadStart.

Bắt đầu từ phiên bản 2.0 của .NET Framework, nó không phải là cần thiết để tạo ra một đối tượng ủy nhiệm 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, và trình biên dịch sẽ tự tạo ra đối tượng ủy nhiệm cho bạn.
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 + " codding ... ");
               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 ủy quyền để thực thi phương thức).
            ThreadStart threadStart1 = new ThreadStart(DoWork);

            // Tạo một luồng bao lấy 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 tham số)
            ThreadStart threadStart2 = new ThreadStart(tran.DoCode);

            // Tạo một luồng 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ụ:

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() ám chỉ rằng bạn đang tạo ra môt phương thức nặc danh.
delegate()
{
     // Làm gì đó ở đây.
}
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");

            // Bắt đầu 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ụ:

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, 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 luồng hiện thời
            // (Đang là luồng chính).
            Thread.CurrentThread.Name = "Main";

            Console.WriteLine("Code of "+ Thread.CurrentThread.Name);

            Console.WriteLine("Create new thread");

            // Tạo một luồng
            Thread letgoThread = new Thread(LetGo);
          
            // Đặt tên cho luồng.
            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ụ:

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
            TimeSpan interval = endsDateTime2 - endsDateTime1;

            // Tổng số mili giây hơn kém nhau.
            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.
            letgoThread.Start();

            // Nói với thread cha (ở đây sẽ là Main thread)
            // hãy chờ cho letgoThread xong rồi mới tiếp tục chạy.
            letgoThread.Join();

            // Dòng này phải chờ cho letgoThread hoàn thành 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ụ:

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 text 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;

            // Bắt đầu các luồng.
            unImportantThread.Start();
            importantThread.Start();
           

            Console.Read();

        }

        // Một việc quan trọng, yêu cầu ư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
                // 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
            TimeSpan interval = unImportantEndTime - importantEndTime;

            // Tổng số mili giây hơn kém nhau.
            Console.WriteLine("UnImportant Thread - Important Thread = " + interval.TotalMilliseconds +" mliseconds");
        }
         
    }

}
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.

10- TODO - Tai lieu dang cap nhap