Buy Me a Coffee

“.NET 多線程:精通 CSharp 並行編程”

線程基礎 (Threading Basics)

在本章中,我們將介紹在 CSharp 中進行線程操作的基本任務。您將學習到以下內容:

  • 在 CSharp 中創建一個線程
  • 暫停一個線程
  • 使一個線程等待
  • 終止一個線程
  • 確定線程狀態
  • 線程優先級
  • 前景和背景線程
  • 向線程傳遞參數
  • 使用 CSharp lock 關鍵字鎖定
  • 使用 Monitor 結構鎖定
  • 處理異常

線程基礎

引言

在過去,一般的電腦只有一個計算單元,無法同時執行多個計算任務。然而,作業系統仍能同時處理多個程式,實現了多工任務的概念。為防止某個程式獲取 CPU 控制權時,導致其他應用程式和作業系統本身出現停頓,作業系統必須以某種方式將實體計算單元切分為幾個虛擬處理器,並將一定量的計算能力分配給每個執行的程式。此外,作業系統本身必須始終優先獲取 CPU 存取權,並能對不同程式的 CPU 存取進行優先級排序。線程就是這個概念的實現。它可以被視為分配給執行它的特定程式的虛擬處理器,使該程式能夠獨立執行。

請注意,線程會消耗大量的作業系統資源。嘗試在單核 CPU 上跨多個線程共用一個實體處理器,將導致作業系統忙於管理線程而無法執行程式。因此,儘管通過提高每秒能執行的指令數量來增強計算機處理器是可能的,但在單核 CPU 上使用多線程並行計算任務,會比順序執行需要更多時間。

為了有效利用現代處理器的計算能力,必須能夠將程式組成使用多個核心的幾個線程進行通訊和同步。此文章將集中於使用 CSharp 語言執行一些非常基本的線程操作。我們將涵蓋線程的生命週期,包括創建、暫停、使線程等待和終止線程,然後我們將介紹基本的同步技術。


在 C# 中創建線程 (Creating a Thread in C#)

範例程式

為了理解如何在 C# 程式中創建並使用新線程,請參考以下範例:

using System;
using System.Threading.Tasks;

namespace CreatingThread
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Task t = Task.Run(() => DoSomething());
            DoSomething();
            await t;
        }

        static void DoSomething()
        {
            Console.WriteLine("Starting...");
            for (int i = 1; i < 10; i++)
            {
                Console.WriteLine(i);
            }
        }
    }
}

工作原理

在上述範例中,我們透過 Task.Run 來非同步地創建並啟動一個新線程執行 DoSomething 方法。這裡的 DoSomething 方法會先在主線程上執行,然後在由 Task.Run 創建的另一個線程上再次執行。

由於 Main 方法被標記為 async,我們可以使用 await 關鍵字等待由 Task.Run 創建的線程完成執行。這使得主線程在等待新線程完成工作期間可以繼續執行其他任務。

結果,我們將會看到兩組數字從 1 到 10 的輸出,這展示了 DoSomething 方法是如何在主線程和另一個由 Task API 創建的線程上同時執行的。這種方法充分利用了 C# 的異步程式設計能力,為多線程程式設計提供了更大的靈活性和控制。

—### 暫停一個線程 (Pausing a Thread)

在 .NET 7 環境下,我們可以利用 Taskawait/async 模式來實現線程的暫停,同時避免過度消耗操作系統資源。

範例程式

using System;
using System.Threading.Tasks;

namespace PausingAThread
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Task delayedTask = DoSomethingWithDelay();
            DoSomething();
            await delayedTask;
        }

        static void DoSomething()
        {
            Console.WriteLine("DoSomething is Starting...");
            for (int i = 1; i < 10; i++)
            {
                Console.WriteLine(i);
            }
        }

        static async Task DoSomethingWithDelay()
        {
            Console.WriteLine("DoSomethingWithDelay is Starting...");
            for (int i = 1; i < 10; i++)
            {
                await Task.Delay(TimeSpan.FromSeconds(2));
                Console.WriteLine(i);
            }
        }
    }
}

工作原理

當程式運行時,Main 方法首先非同步地調用 DoSomethingWithDelay 方法,該方法內部包含了異步的延遲 (Task.Delay)。與此同時,DoSomething 方法在主線程上同步運行,不受影響。由於 DoSomethingWithDelay 方法中存在異步延遲,DoSomething 方法的輸出通常會先於 DoSomethingWithDelay 方法的輸出。這個範例展示了如何在 .NET 7 中使用異步編程技術來實現線程暫停,同時避免不必要的 CPU 資源消耗。


讓線程等待 (Making a Thread Wait)

這一節將展示如何在 .NET 7 中使用 Taskawait 來實現線程等待的功能。

範例程式

using System;
using System.Threading.Tasks;

namespace MakingAThreadWait
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting program...");
            Task t = DoSomethingWithDelayAsync();
            await t;
            Console.WriteLine("Thread completed");
        }

        static async Task DoSomethingWithDelayAsync()
        {
            Console.WriteLine("Starting...");
            for (int i = 1; i < 10; i++)
            {
                await Task.Delay(TimeSpan.FromSeconds(2));
                Console.WriteLine(i);
            }
        }
    }
}

它如何運作

在這個程序中,我們同樣利用 Task 來進行異步操作。DoSomethingWithDelayAsync 方法包含了 Task.Delay 的調用,用於異步等待。使用 await t; 確保主線程在 DoSomethingWithDelayAsync 方法完成之前不會結束。當該方法完成後,程序將繼續執行並打印出 “Thread completed”。這種方式相比原始的 ThreadThread.Join 方法,更加簡潔,且更符合現代 .NET 應用程式的異步編程實踐。


中止線程 (Aborting a Thread)

在 .NET 7 中,Thread.Abort() 方法已經被淘汰,因為它可能導致應用程序中出現不可預測的錯誤和問題。取而代之,我們可以使用 CancellationToken 來優雅地取消任務。以下是對您提供的代碼進行 .NET 7 改寫的示例:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AbortingAThread
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting program...");

            CancellationTokenSource cts = new CancellationTokenSource();
            TaskprintTask = DoSomethingWithDelayAsync(cts.Token);

            // 啟動任務後稍作等待,然後取消它
            await Task.Delay(TimeSpan.FromSeconds(6));
            cts.Cancel();

            try
            {
                await printTask;
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("A thread has been gracefully aborted");
            }
        }

        static async Task DoSomethingWithDelayAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Starting...");
            for (int i = 1; i < 10; i++)
            {
                await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
                cancellationToken.ThrowIfCancellationRequested();
                Console.WriteLine(i);
            }
        }
    }
}

工作原理

在這個例子中,我們使用 CancellationToken 來控制異步任務的取消。當調用 cts.Cancel() 方法時,與 CancellationTokenSource 相關聯的任何任務都會收到取消請求。在 DoSomethingWithDelayAsync 方法中,我們在每次迴圈迭代前檢查取消標誌,並在收到取消請求時拋出 OperationCanceledException。這樣,我們可以優雅地中止任務,而不是使用 Thread.Abort() 強制中止,這是一種更安全且可控的方式。


確定線程狀態 (Determining Thread State)

在 .NET 7 環境中,我們可以通過檢查 Task 的狀態來確定異步操作的狀態。雖然這與傳統的 Thread 類別不同,但它提供了更多關於異步操作狀態的資訊。

using System;
using System.Threading.Tasks;

namespace DeterminingThreadState
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Task task = DoSomethingAsync();
            Console.WriteLine($"Task status: {task.Status}");

            await task;
            Console.WriteLine($"Task status: {task.Status}");
        }

        static async Task DoSomethingAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(2));
            Console.WriteLine("Task completed");
        }
    }
}

工作原理

在這個例子中,DoSomethingAsync 方法執行一個簡單的異步操作,其中包含了延遲。我們可以在任務開始前後檢查 Task 的狀態,以了解任務是否正在執行、已完成或者處於其他狀態。這種方法提供了一個簡單且有效的方式來追蹤異步操作的生命周期。—

線程優先級 (Thread Priority)

雖然在 .NET 7 中使用 Task 並不直接支援傳統意義上的線程優先級設置,但我們仍然可以通過控制執行緒的優先級來展示優先級概念。

範例程式

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadPriorityExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Current thread priority: {0}", Thread.CurrentThread.Priority);
            Console.WriteLine("Running on all cores available");
            await RunThreadsAsync();
            Console.WriteLine("Running on a single core");
            Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
            await RunThreadsAsync();
        }

        static async Task RunThreadsAsync()
        {
            var sample = new ThreadSample();

            Task highPriorityTask = Task.Run(() =>
            {
                Thread.CurrentThread.Priority = ThreadPriority.Highest;
                sample.CountNumbers();
            });

            Task lowPriorityTask = Task.Run(() =>
            {
                Thread.CurrentThread.Priority = ThreadPriority.Lowest;
                sample.CountNumbers();
            });

            await Task.WhenAll(highPriorityTask, lowPriorityTask);
        }

        class ThreadSample
        {
            private bool _isStopped = false;

            public void Stop()
            {
                _isStopped = true;
            }

            public void CountNumbers()
            {
                long counter = 0;
                while (!_isStopped)
                {
                    counter++;
                }

                Console.WriteLine($"Task with priority {Thread.CurrentThread.Priority} counted to {counter:N0}");
            }
        }
    }
}

工作原理

當主程序開始時,它分別啟動了兩個任務,一個設置為最高優先級,另一個為最低優先級。這些任務在所有可用核心或單一核心上執行,展示了不同優先級對計數任務的影響。


前景和背景線程 (Foreground and Background Threads)

在 .NET 7 中,我們使用 TaskCancellationToken 來模擬前景和背景線程的行為。

範例程式

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ForegroundBackgroundThreads
{
    class Program
    {
        static async Task Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task foregroundTask = Task.Run(() => DoWork("Foreground Task", cts.Token));

            // 背景任務
            Task backgroundTask = Task.Run(() => DoWork("Background Task", cts.Token), cts.Token);

            await foregroundTask; // 等待前景任務完成
            cts.Cancel(); // 取消背景任務
            Console.WriteLine("Foreground task completed. Program will terminate now.");
        }

        static async Task DoWork(string taskName, CancellationToken token)
        {
            try
            {
                for (int i = 0; i < 5; i++)
                {
                    token.ThrowIfCancellationRequested();
                    Console.WriteLine($"{taskName} is working...");
                    await Task.Delay(1000); // 模擬工作
                }
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine($"{taskName} has been canceled.");
            }
        }
    }
}

工作原理

這個程序創建了兩個任務:一個前景任務和一個背景任務。主線程等待前景任務完成後,取消背景任務。這種方法模擬了傳統線程模型中前景和背景線程的行為,展示了如何在現代 .NET 應用程序中有效地使用 TaskCancellationToken


傳遞參數給線程 (Passing Parameters to a Thread)

在 .NET 7 中,我們可以利用 Task.Run 和 lambda 表達式來傳遞參數給異步執行的任務。這提供了一種靈活的方式來將參數傳遞給線程。

範例程式

using System;
using System.Threading.Tasks;

namespace PassingParametersToThread
{
    class Program
    {
        static async Task Main(string[] args)
        {
            int iterations = 10;
            await Task.Run(() => PrintNumbers(iterations));
            
            string message = "Hello, world!";
            await Task.Run(() => PrintMessage(message));
        }

        static void PrintNumbers(int count)
        {
            for (int i = 0; i < count; i++)
            {
                Console.WriteLine($"Number: {i}");
            }
        }

        static void PrintMessage(string message)
        {
            Console.WriteLine(message);
        }
    }
}

工作原理

在這個範例中,我們使用 Task.Run 來啟動異步任務,並透過 lambda 表達式將參數傳遞給這些任務。PrintNumbersPrintMessage 方法接收參數並在異步執行的過程中使用這些參數。這種方法使得傳遞參數給線程變得更加簡單和直接。


使用 CSharp lock 關鍵字鎖定 (Locking with a C# lock Keyword)

在多線程環境中,使用 lock 關鍵字來保證同時只有一個線程可以訪問特定資源是一種常見的同步機制。

範例程式

using System;
using System.Threading.Tasks;

namespace LockKeywordExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var counter = new Counter();
            var tasks = new Task[10];

            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Run(() => UpdateCounter(counter));
            }

            await Task.WhenAll(tasks);
            Console.WriteLine($"Counter value: {counter.Value}");
        }

        static void UpdateCounter(Counter counter)
        {
            for (int i = 0; i < 1000; i++)
            {
                lock (counter)
                {
                    counter.Increment();
                }
            }
        }
    }

    class Counter
    {
        public int Value { get; private set; }

        public void Increment()
        {
            Value++;
        }
    }
}

工作原理

在此範例中,我們創建了一個 Counter 類,並在多個異步任務中同時訪問和修改它的值。為了確保同一時刻只有一個線程可以訪問 Counter 對象,我們在修改其值時使用了 lock 關鍵字。這確保了對 Counter 的訪問是線程安全的,防止了潛在的資源競爭問題。


使用 Monitor 和 lock (Locking with Monitor and lock Keyword)

在 .NET 中,Monitor 類提供了一種低層次的同步機制,而 lock 關鍵字則提供了一種更高層次、更易於使用的同步方法。這兩者都用於確保同時只有一個線程可以訪問特定的代碼區塊。

使用 Monitor 範例

using System;
using System.Threading;
using System.Threading.Tasks;

namespace MonitorExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var counter = new Counter();
            var tasks = new Task[10];

            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Run(() => UpdateCounterWithMonitor(counter));
            }

            await Task.WhenAll(tasks);
            Console.WriteLine($"Counter value: {counter.Value}");
        }

        static void UpdateCounterWithMonitor(Counter counter)
        {
            for (int i = 0; i < 1000; i++)
            {
                bool lockTaken = false;
                try
                {
                    Monitor.Enter(counter, ref lockTaken);
                    counter.Increment();
                }
                finally
                {
                    if (lockTaken)
                    {
                        Monitor.Exit(counter);
                    }
                }
            }
        }
    }

    class Counter
    {
        public int Value { get; private set; }

        public void Increment()
        {
            Value++;
        }
    }
}

使用 lock 關鍵字範例

using System;
using System.Threading.Tasks;

namespace LockKeywordExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var counter = new Counter();
            var tasks = new Task[10];

            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Run(() => UpdateCounterWithLock(counter));
            }

            await Task.WhenAll(tasks);
            Console.WriteLine($"Counter value: {counter.Value}");
        }

        static void UpdateCounterWithLock(Counter counter)
        {
            for (int i = 0; i < 1000; i++)
            {
                lock (counter)
                {
                    counter.Increment();
                }
            }
        }
    }

    class Counter
    {
        public int Value { get; private set; }

        public void Increment()
        {
            Value++;
        }
    }
}

工作原理

Monitor 範例使用 Monitor.EnterMonitor.Exit 方法明確地獲取和釋放鎖。這種方式提供了對鎖定行為的精細控制,但需要更多的代碼和處理異常的邏輯。

lock 關鍵字則是一種簡化的方法,它在背後使用 Monitor。使用 lock 會自動處理鎖的獲取和釋放,並且提供了一個清晰和簡潔的語法來保護共享資源。

這兩種方法都可以在多線程環境中安全地更新共享資源,防止資源競爭和其他多線程相關的問題。


您是對的,請允許我補充關於在 .NET 7 中處理線程異常的內容。

處理線程異常 (Handling Thread Exceptions)

在多線程應用程序中,異常管理是一個重要的考慮因素。在 .NET 7 中,我們可以利用 Task 來處理異步任務中的異常,這提供了一種更現代化且有效的方式來捕獲和處理異步操作中的錯誤。

範例程式

using System;
using System.Threading.Tasks;

namespace HandlingThreadException
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Task taskWithException = Task.Run(() => DoWorkThatFails());
            try
            {
                await taskWithException;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Caught an exception: {ex.Message}");
            }

            Console.WriteLine("Program completed");
        }

        static void DoWorkThatFails()
        {
            throw new InvalidOperationException("Something went wrong in the task.");
        }
    }
}

工作原理

在這個例子中,我們使用 Task.Run 啟動了一個異步任務,該任務將拋出一個異常。當我們使用 await 等待這個任務時,任何在任務中拋出的異常都會被捕獲並在 catch 塊中處理。這使得我們能夠在異步操作中優雅地處理異常,並且避免了異步代碼中未處理異常導致的應用程序崩潰。

使用這種方法,我們能夠在異步操作中捕獲和處理異常,並根據需要對其進行處理或者重新拋出。這是一種有效的錯誤處理機制,特別是在涉及多個異步操作和線程的複雜應用程序中。


結語

以上內容提供了一系列使用 C# 在 .NET 7 環境中進行多線程編程的基本指南和範例。從基礎的線程創建和管理到進階的同步和異步編程技巧,這些範例展示了在現代 .NET 應用中有效處理並行性的方法。通过這些範例,讀者可以獲得對 .NET 中多線程編程的深入理解,並能應用這些知識來構建更高效、可靠的應用程序。