C# 进阶之多线程

439 阅读20分钟

在日常开发中,C# 多线程的使用频率普通管理软件开发中使用并不多,只有在高并发系统会使用到,以下是一些常见的多线程概念和用法,以及它们的使用频率:

  1. 线程创建:使用Thread类或Task类创建线程,Task通常更常用,因为它提供了更高级的抽象。
  2. 启动线程:通过调用Start方法来启动线程。
  3. 线程同步:使用锁(lock)关键字、互斥体(Mutex)、信号量(Semaphore)等来确保线程安全。
  4. 线程池:使用ThreadPool来管理线程,以提高性能和资源利用率。
  5. 异步编程:使用asyncawait关键字来编写异步代码,以避免阻塞主线程。
  6. 并发集合:使用ConcurrentDictionaryConcurrentQueue等线程安全的集合来处理并发数据访问。
  7. 线程间通信:使用线程间通信机制,如事件(Event)、信号量、管道(Pipe)、消息队列等。
  8. 并行编程:使用Parallel类和PLINQ来简化并行化任务的处理。
  9. 取消操作:使用CancellationToken来取消异步操作。
  10. 线程调度:了解线程调度器的工作方式,以优化线程的执行。

异步编程和多线程是不同的概念,但它们都涉及到在应用程序中处理并发性和异步操作。

  1. 异步编程:异步编程是一种编程模型,它使开发者能够编写非阻塞的代码,以便在执行耗时的操作时不会阻塞主线程。在C#中,可以使用asyncawait关键字来编写异步代码,这通常涉及到等待异步任务的完成而不会阻止其他操作。异步编程用于处理I/O密集型操作,例如从磁盘读取文件、发出网络请求等,以便在等待这些操作完成时充分利用CPU
  2. 多线程:多线程是一种并发性的编程模型,它涉及到同时执行多个线程,这些线程可以并行执行不同的任务。每个线程都有自己的执行上下文,可以独立运行。多线程编程用于处理CPU密集型操作,例如计算密集型任务,可以将工作分配给多个线程,以提高性能。

虽然异步编程和多线程都涉及并发性,但它们的主要区别在于应用场景和实现方式。

异步编程通常用于处理I/O密集型操作,以提高响应性,而多线程用于并行执行CPU密集型任务,以提高性能。

在C#/.NET中,有多种方式可以创建线程,每种方式都适用于不同的场景。以下是一些常见的线程创建方式:

  1. Thread类: 使用System.Threading.Thread类可以创建和控制线程。这是传统的方式,但相对较低级,通常不是首选。
  2. Task类: System.Threading.Tasks.Task类是一种更高级的线程创建和管理方式,通常用于异步编程。它提供了更好的抽象和错误处理机制。
  3. ThreadPool 使用线程池(System.Threading.ThreadPool)可以管理线程的生命周期,并重复使用它们,以降低线程创建和销毁的开销。这在处理大量短期任务时很有用。
  4. Parallel类: System.Threading.Tasks.Parallel类提供了简化的方式来执行并行任务,通常与LINQ一起使用。
  5. async/await关键字: 异步编程是一种通过async/await关键字实现的方式,它允许您编写非阻塞的代码,而不必显式创建线程。这在处理异步操作时非常有用。
  6. 委托和异步委托: 使用委托和异步委托可以将方法异步执行。
  7. BackgroundWorker System.ComponentModel.BackgroundWorker是用于在后台执行任务并报告进度和完成事件的组件,通常在Windows Forms应用程序中使用。

最常用的线程创建方式取决于具体需求和应用程序类型。在现代C#应用程序中,Taskasync/await是首选的方式,因为它们提供了更好的抽象、异常处理和可读性。线程池也常用于管理线程的生命周期,特别是在处理大量并发任务时。如果需要更精细的线程控制,可以使用Thread类,但这应该是相对较少见的情况。

  1. Thread
 Thread thread = new Thread(() => {
     Console.WriteLine("Worker Thread: Doing some work...");
     // 模拟耗时操作
     Thread.Sleep(2000); 
     Console.WriteLine("Worker Thread Finished.");
 });
 ​
 // 启动线程
 thread.Start(); 
 Console.WriteLine("Main Thread: Waiting for the worker thread to finish...");
 ​
 // 主线程等待新线程完成
 thread.Join();
 Console.WriteLine("Main Thread Finished.");

这种方式可以使线程创建和定义执行方法变得更加简洁和清晰,特别适合一些小型的线程任务。但请注意,对于更复杂的线程操作,通常使用Taskasync/await会更便捷。

  1. Task
 Task<int> task = Task.Run(() => {
     Console.WriteLine("Worker Task: Doing some work...");
     // 模拟耗时操作
     Task.Delay(2000).Wait(); 
     Console.WriteLine("Worker Task Finished.");
     // 返回结果
     return 42; 
 });
 Console.WriteLine("Main Thread: Waiting for the worker task to finish...");
 ​
 // 等待任务完成并获取结果
 int result = await task;
 ​
 Console.WriteLine("Main Thread: Worker task result is " + result);
 Console.WriteLine("Main Thread Finished.");

Task提供了更高级的抽象,可以轻松处理异步操作,以及更好的异常处理和任务状态管理。这使得它成为在现代C#应用程序中进行线程编程的首选方式。如果需要并行执行多个任务,还可以使用Task.WhenAllTask.WhenAny等方法来协调多个任务。

Task.WhenAllTask.WhenAnyTask类提供的两个非常有用的方法,用于协调多个任务的执行。

Task.WhenAll:

  • Task.WhenAll接受一个Task数组或可枚举集合,等待所有的任务都完成。
  • 它返回一个新的任务,这个新任务将在所有输入任务都完成后完成,并且可以获得每个任务的结果。
  • 这对于并行执行多个独立任务以提高性能非常有用。
 var tasks = new Task<int>[]
 {
     Task.Run(() => 1),
     Task.Run(() => 2),
     Task.Run(() => 3)
 };
 // 等待所有任务完成
 await Task.WhenAll(tasks);
 ​
 // 获取所有任务的结果
 int[] results = tasks.Select(task => task.Result).ToArray();
 Console.WriteLine("All tasks completed. Results: " + string.Join(", ", results));

Task.WhenAny

  • Task.WhenAny接受一个Task数组或可枚举集合,等待任何一个任务完成。
  • 它返回一个新的任务,这个新任务将在任何一个输入任务完成时立即完成。
  • 这对于需要获取最快完成的任务的情况非常有用。
 var tasks = new Task<int>[]
 {
     Task.Delay(2000).ContinueWith(_ => 1),
     Task.Delay(1000).ContinueWith(_ => 2),
     Task.Delay(3000).ContinueWith(_ => 3)
 };
 ​
 // 等待任何一个任务完成
 Task<int> completedTask = await Task.WhenAny(tasks);
 Console.WriteLine("One task completed. Result: " + completedTask.Result);

这两个方法可以帮助您更好地管理多个任务的执行,无论是等待它们全部完成(Task.WhenAll)还是等待任何一个完成(Task.WhenAny)。根据您的需求,选择适当的方法来协调多个任务的执行。

  1. ThreadPool:
 // 将工作项添加到线程池,使用Lambda表达式,这里的state是"Hello from ThreadPool!"
 // ThreadPool.QueueUserWorkItem 方法用于将一个委托(Lambda表达式)添加到线程池队列中,然后线程池会自动为其分配一个线程来执行。
 // 这种方法适用于执行简单的异步任务。线程池会自动管理线程的生命周期,从而减少了手动线程管理的复杂性。
 ThreadPool.QueueUserWorkItem((state) => {
     string message = (string)state;
     Console.WriteLine("ThreadPool Thread: " + message);
     // 模拟耗时操作
     Thread.Sleep(1000);
     Console.WriteLine("ThreadPool Thread Finished.");
 }, "Hello from ThreadPool!");
 ​
 Console.WriteLine("Main Thread: Doing some work...");
 ​
 // 主线程可以继续执行其他操作
 ​
 // 等待一段时间以确保线程池任务完成
 Thread.Sleep(2000);
 ​
 Console.WriteLine("Main Thread Finished.");

ThreadPool.QueueUserWorkItem 方法用于将一个委托(WorkerMethod)添加到线程池队列中,然后线程池会自动为其分配一个线程来执行。这种方法适用于执行简单的异步任务。线程池会自动管理线程的生命周期,从而减少了手动线程管理的复杂性。

请注意,线程池适用于执行短期、非常耗时的任务。如果需要更多的控制或长时间运行的任务,可以考虑使用Task类和async/await模式。

  1. Parallel
 // 使用Parallel.ForEach并行处理集合元素:
 List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 List<Task> tasks = new List<Task>();
 ​
 Parallel.ForEach(numbers, number => {
     Task task = Task.Run(() => {
         Console.WriteLine($"Processing number {number} on thread {Task.CurrentId}");
     });
     tasks.Add(task);
 });
 ​
 await Task.WhenAll(tasks); // 等待所有任务完成
 ​
 Console.WriteLine("All processing is complete.");
 Parallel.Invoke( _ => {
     Console.WriteLine("Task 1 is running on thread " + Task.CurrentId);
 }, _ => {
     Console.WriteLine("Task 2 is running on thread " + Task.CurrentId);
 }, _ => {
     Console.WriteLine("Task 3 is running on thread " + Task.CurrentId);
 });
 ​
 Console.WriteLine("All tasks are complete.");

Parallel类都会自动处理线程的创建和管理,以充分利用系统的多核处理能力,简化了并行编程的任务。请根据需求选择适当的方法来创建并行线程。

  1. 使用async/await关键字
 // 使用Task.Run创建异步任务
 await Task.Run(async () => {
     Console.WriteLine("Async task is running on thread " + Task.CurrentId);
 ​
     // 模拟一个耗时操作,例如等待1秒钟
     await Task.Delay(1000);
     Console.WriteLine("Async task is complete on thread " + Task.CurrentId);
 });
 Console.WriteLine("Main thread continues on thread " + Task.CurrentId);
  1. 使用委托创建线程
 // 使用委托创建线程:
 // 首先,定义一个委托类型,该委托可以引用你想要在线程中执行的方法。
 // 然后,使用委托的BeginInvoke方法来异步调用这个方法再使用EndInvoke来获取异步操作的结果。
 delegate int MathOperation(int a, int b);
 ​
 void Main() {
     // 创建一个表示加法操作的委托实例
     MathOperation add = (x, y) => x + y;
 ​
     // 异步调用委托,传递 Lambda 表达式作为回调方法
     IAsyncResult result = add.BeginInvoke(10, 5, (ar) => {
         // 异步操作完成后,在 Lambda 表达式内部处理结果
         if (ar.AsyncState is MathOperation mathOperation) {
             int callbackResult = mathOperation.EndInvoke(ar);
             Console.WriteLine("Callback method: Result of add operation: " + callbackResult);
             
             // 使用委托本身再次调用委托,执行另一个操作
             int multiplyResult = mathOperation(5, 2);
             Console.WriteLine("Callback method: Result of multiply operation: " + multiplyResult);
         }
     }, add);
 ​
     // 主线程继续执行其他工作
     Console.WriteLine("Main thread is working...");
 ​
     // 等待异步操作完成
     result.AsyncWaitHandle.WaitOne();
 ​
     // 主线程完成后显示消息
     Console.WriteLine("Main thread is complete.");
 }

在线程编程中,线程同步是确保多个线程安全地协作的关键概念。C# 提供了多种线程同步机制来帮助开发人员处理并发问题。以下是一些常见的线程同步机制:

  1. 互斥锁(Mutex :互斥锁用于确保只有一个线程可以访问共享资源。它是一种重量级的同步机制,适用于跨进程同步。使用 System.Threading.Mutex 类来创建和管理互斥锁。
  2. 信号量(Semaphore :信号量用于控制对共享资源的访问,允许多个线程同时访问资源,但受限于信号量的数量。使用 System.Threading.Semaphore 类来创建和管理信号量。
  3. 自旋锁(SpinLock :自旋锁是一种低级别的同步机制,它在等待共享资源时会忙等待(自旋),适用于非常短暂的临界区。使用 System.Threading.SpinLock 类来创建和管理自旋锁。
  4. 互斥体锁(Mutex :互斥体锁(不同于互斥锁)是一种用于跨线程同步的同步机制,它用于确保只有一个线程可以访问共享资源。使用 System.Threading.Mutex 类来创建和管理互斥体锁。
  5. Monitor(锁)System.Threading.Monitor 类提供了 lock 关键字的底层支持,用于确保对临界区的互斥访问。lock 关键字是一种高级别的同步机制,通常用于同步访问共享资源。
  6. 信号量SlimSemaphoreSlimSystem.Threading.SemaphoreSlimSemaphore 的轻量级版本,用于控制对共享资源的访问。它比 Semaphore 更轻量,适用于单个进程内的线程同步。
  7. 手动重置事件(ManualResetEventSystem.Threading.ManualResetEvent 用于在线程之间发送信号,允许等待线程在事件被手动设置时继续执行。
  8. 自动重置事件(AutoResetEventSystem.Threading.AutoResetEvent 与手动重置事件类似,但在信号被等待线程接收后会自动重置。
  9. CountdownEventSystem.Threading.CountdownEvent 用于等待多个线程完成工作,当计数为零时,它会发出信号。
  10. Task Parallel LibraryTPLTPL 提供了一种高级别的并行编程模型,允许使用 Task 对象来表示异步操作,通过 WaitWaitAll 等方法进行同步。

这些是 C# 中常见的线程同步机制。选择适当的同步机制取决于具体需求和场景,不同的场景可能需要不同的同步方法。

正确的线程同步可以避免竞态条件和数据争用,确保多线程应用程序的稳定性和可靠性。

在多线程环境下查询大量数据时,可以将查询任务分配给多个线程来加速查询过程。每个线程可以并行执行独立的查询任务,然后将结果合并起来。但是,多线程查询需要小心谨慎,以避免竞争条件和线程安全问题。

 static async Task Main() {
     int totalRecords = 100000; // 假设有100,000条记录需要查询
     int batchSize = 1000;     // 每个批次查询1000条记录
     List<int> results = new List<int>();
 ​
     // 计算需要多少个批次
     int numBatches = (int)Math.Ceiling((double)totalRecords / batchSize);
 ​
     // 使用并行任务查询数据
     await Task.WhenAll(Enumerable.Range(0, numBatches).Select(async batch => {
         int start = batch * batchSize;
         int end = Math.Min(start + batchSize, totalRecords);
 ​
         List<int> batchResults = await QueryDataAsync(start, end);
 ​
         lock (results) {
             results.AddRange(batchResults);
         }                                                          
     }));
 ​
     Console.WriteLine($"Total records retrieved: {results.Count}");
 }
 ​
 static async Task<List<int>> QueryDataAsync(int start, int end)
 {
     // 模拟查询数据库,返回一批数据
     await Task.Delay(100);
     return Enumerable.Range(start, end - start).ToList();
 }

互斥锁(Mutex)是一种用于线程同步的重要同步机制,用于确保在任何给定时间只有一个线程能够访问共享资源。互斥锁是一种全局锁,适用于多线程和多进程之间的同步。

以下是互斥锁的一些关键特性和用法:

  1. 资源保护:主要用于保护共享资源免受多线程访问时的竞争条件。只有持有互斥锁的线程可以访问被保护的资源,其他线程必须等待锁被释放后才能访问。

  2. 单一持有:互斥锁是一种独占锁,每次只能有一个线程持有锁。如果一个线程已经持有了互斥锁,其他线程必须等待该线程释放锁后才能获取锁。

  3. 阻塞和等待:如果一个线程尝试获取一个已经被其他线程持有的互斥锁,它会被阻塞,直到锁被释放。这确保了互斥锁是一种有效的同步机制,可以避免竞态条件。

  4. 跨进程同步:互斥锁可以用于不同进程之间的同步,这种类型的互斥锁称为“跨进程互斥锁”。它允许不同进程之间协调访问共享资源。

  5. 使用示例:在C#中,你可以使用 System.Threading.Mutex 类来创建和使用互斥锁。以下是一个简单示例:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static Mutex mutex = new Mutex();
 ​
     static void Main()
     {
         for (int i = 0; i < 5; i++)
         {
             Thread thread = new Thread(DoWork);
             thread.Start(i);
         }
 ​
         Console.ReadLine();
     }
 ​
     static void DoWork(object id)
     {
         mutex.WaitOne();
         try
         {
             Console.WriteLine($"Thread {id} is working...");
             // 执行需要同步的工作
         }
         finally
         {
             mutex.ReleaseMutex();
         }
     }
 }

在这个示例中,我们使用 Mutex 来确保每个线程能够安全地执行工作。只有一个线程可以同时持有 mutex,其他线程必须等待。互斥锁是一种重要的线程同步机制,用于确保多线程访问共享资源的线程安全性。但需要小心使用,以避免潜在的死锁问题和性能影响。

信号量(Semaphore)是一种线程同步机制,允许多个线程同时访问一定数量的资源。它是一种常用于控制并发访问的同步工具,用于限制资源的并行访问。

以下是关于信号量的一些基本概念和使用方法:

  1. 信号量计数:信号量维护一个整数值,称为信号量计数。这个计数表示当前可用的资源数量。
  2. 等待和释放资源:线程可以尝试等待信号量,并在等待时阻塞,直到信号量计数大于零。当线程获得资源后,信号量计数减一。线程可以释放资源,使信号量计数加一。
  3. 控制并发访问:通过适当地初始化信号量的计数,你可以控制并发访问共享资源的数量。例如,如果你希望同时有最多三个线程访问某个资源,你可以初始化信号量为3。
  4. 阻塞和非阻塞:线程可以使用 WaitOne 方法来等待信号量,该方法会阻塞线程直到信号量计数大于零。如果你希望线程尝试等待信号量但不阻塞,可以使用 WaitOne 的重载方法。同样,Release 方法用于释放信号量。
  5. 超时处理:你可以为 WaitOne 方法设置超时,以避免线程永远阻塞在等待信号量的状态。
  6. 适用于资源池和并发限制:信号量通常用于资源池、线程池、并发限制和其他需要控制并发访问的场景。

以下是一个简单的示例,演示如何使用 Semaphore 控制并发访问:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static Semaphore semaphore = new Semaphore(3, 3); // 允许最多3个线程同时访问资源
 ​
     static void Main()
     {
         for (int i = 0; i < 5; i++)
         {
             Thread thread = new Thread(DoWork);
             thread.Start(i);
         }
 ​
         Console.ReadLine();
     }
 ​
     static void DoWork(object id)
     {
         Console.WriteLine($"Thread {id} is waiting for the semaphore...");
         semaphore.WaitOne();
 ​
         try
         {
             Console.WriteLine($"Thread {id} has acquired the semaphore and is working...");
             // 执行需要同步的工作
         }
         finally
         {
             semaphore.Release();
             Console.WriteLine($"Thread {id} has released the semaphore.");
         }
     }
 }

在这个示例中,我们创建了一个允许最多3个线程同时访问资源的信号量。每个线程在开始工作前,先等待信号量。只有当信号量计数大于零时,线程才能获取信号量并执行工作。完成工作后,线程释放信号量,使其计数加一。

这种方式可以确保最多有3个线程同时访问共享资源,其余线程会等待。信号量是一种非常有用的工具,可用于控制并发访问,避免资源的过度竞争和不必要的等待。

自旋锁(SpinLock)是一种轻量级的线程同步机制,与传统的互斥锁(Mutex)不同,它不会将线程挂起等待资源的释放,而是会一直尝试获取锁(自旋)。

自旋锁的优点在于对于短时间内的锁请求,自旋通常比线程挂起和恢复的开销小,因此对于竞争不激烈的情况,它可以提供更好的性能。但是,对于长时间的锁请求,自旋锁可能会导致浪费CPU资源,因此在使用时需要小心谨慎。

在 C# 中,你可以使用 System.Threading.SpinLock 类来创建和使用自旋锁。以下是一个简单的示例:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static SpinLock spinLock = new SpinLock();
 ​
     static void Main()
     {
         for (int i = 0; i < 5; i++)
         {
             Thread thread = new Thread(DoWork);
             thread.Start(i);
         }
 ​
         Console.ReadLine();
     }
 ​
     static void DoWork(object id)
     {
         bool lockTaken = false;
 ​
         try
         {
             spinLock.Enter(ref lockTaken);
 ​
             Console.WriteLine($"Thread {id} has acquired the spin lock and is working...");
             // 执行需要同步的工作
         }
         finally
         {
             if (lockTaken)
             {
                 spinLock.Exit();
                 Console.WriteLine($"Thread {id} has released the spin lock.");
             }
         }
     }
 }

在这个示例中,我们使用了 SpinLock 来创建一个自旋锁。每个线程在开始工作前,尝试获取自旋锁。如果自旋锁当前不可用,线程会一直自旋等待,直到成功获取锁。完成工作后,线程释放锁。

需要注意以下几点:

  • 我们使用 SpinLock.Enter 方法来获取自旋锁,它会返回一个布尔值,表示锁是否被获取。
  • 在使用完锁后,我们使用 SpinLock.Exit 方法来释放锁。
  • 自旋锁的使用需要格外小心,因为自旋锁会一直尝试获取锁,可能会导致线程长时间占用CPU资源。因此,自旋锁适用于短时间内的锁请求,竞争不激烈的情况。对于长时间的锁请求,传统的互斥锁可能更合适。

互斥体锁(Mutex)是一种用于线程同步的同步机制,类似于互斥锁(Mutex),但不同的是,互斥体锁是一种内核对象,可用于跨线程和跨进程同步。

互斥体锁在多线程和多进程之间同步访问共享资源时非常有用,因为它可以确保在任何给定时间只有一个线程或进程可以访问被保护的资源。

以下是关于互斥体锁的一些基本概念和使用方法:

  1. 资源保护:互斥体锁主要用于保护共享资源免受多线程或多进程访问时的竞态条件。只有持有互斥体锁的线程或进程可以访问被保护的资源,其他线程或进程必须等待锁被释放后才能访问。

  2. 单一持有:互斥体锁是一种独占锁,每次只能有一个线程或进程持有锁。如果一个线程或进程已经持有了互斥体锁,其他线程或进程必须等待该线程或进程释放锁后才能获取锁。

  3. 阻塞和等待:线程或进程可以尝试等待互斥体锁,并在等待时被阻塞,直到锁被释放。这确保了互斥体锁是一种有效的同步机制,可以避免竞态条件。

  4. 跨线程和跨进程同步:互斥体锁可以用于同步不同线程之间或不同进程之间的资源访问。

  5. 使用示例:在C#中,你可以使用 System.Threading.Mutex 类来创建和使用互斥体锁。以下是一个简单的示例:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static Mutex mutex = new Mutex();
 ​
     static void Main()
     {
         for (int i = 0; i < 5; i++)
         {
             Thread thread = new Thread(DoWork);
             thread.Start(i);
         }
 ​
         Console.ReadLine();
     }
 ​
     static void DoWork(object id)
     {
         bool mutexAcquired = false;
 ​
         try
         {
             // 等待互斥体锁并尝试获取它
             mutexAcquired = mutex.WaitOne();
 ​
             if (mutexAcquired)
             {
                 Console.WriteLine($"Thread {id} has acquired the mutex and is working...");
                 // 执行需要同步的工作
             }
             else
             {
                 Console.WriteLine($"Thread {id} failed to acquire the mutex.");
             }
         }
         finally
         {
             if (mutexAcquired)
             {
                 // 释放互斥体锁
                 mutex.ReleaseMutex();
                 Console.WriteLine($"Thread {id} has released the mutex.");
             }
         }
     }
 }

在这个示例中,我们使用 Mutex 来创建互斥体锁。每个线程在开始工作前,尝试等待互斥体锁,然后尝试获取它。如果互斥体锁当前不可用,线程会等待,直到成功获取锁。完成工作后,线程释放互斥体锁。

互斥体锁在跨线程和跨进程同步方面非常强大,但需要谨慎使用,以避免潜在的死锁问题和性能影响。

Monitor 是C#中的一种线程同步机制,也是一种用于保护共享资源的锁定机制。Monitor 提供了一种方式来确保在任何给定时间只有一个线程可以访问被保护的资源,以避免竞态条件。

以下是关于 Monitor 的一些基本概念和使用方法:

  1. 资源保护Monitor 主要用于保护共享资源,确保多线程访问这些资源时不会导致数据不一致性或竞态条件。只有持有 Monitor 锁的线程可以访问被保护的资源。

  2. 单一持有Monitor 是一种独占锁,每次只能有一个线程持有锁。如果一个线程已经持有了 Monitor 锁,其他线程必须等待该线程释放锁后才能获取锁。

  3. 阻塞和等待:线程可以尝试获取 Monitor 锁,如果锁已被其他线程持有,尝试获取锁的线程会被阻塞,直到锁被释放。这确保了 Monitor 是一种有效的同步机制,可以避免竞态条件。

  4. 等待和通知Monitor 还提供了等待和通知机制,允许线程等待某些条件的发生,并在条件满足时被唤醒。这可以用于实现生产者-消费者模式等情况。

  5. 使用示例:以下是一个简单的示例,演示如何使用 Monitor 进行线程同步:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static object lockObject = new object(); // 用于锁定的对象static void Main()
     {
         for (int i = 0; i < 5; i++)
         {
             Thread thread = new Thread(DoWork);
             thread.Start(i);
         }
 ​
         Console.ReadLine();
     }
 ​
     static void DoWork(object id)
     {
         lock (lockObject) // 使用 lock 关键字锁定共享资源
         {
             Console.WriteLine($"Thread {id} is working...");
             // 执行需要同步的工作
         }
     }
 }

在这个示例中,我们使用 lock 关键字来锁定一个对象 lockObject,以确保在任何给定时间只有一个线程可以访问被保护的资源。完成工作后,线程释放锁。

总之,Monitor 是C#中用于线程同步的常用工具,可确保共享资源的线程安全性。但需要注意,如果不正确使用,它可能导致死锁问题,因此在编写多线程代码时需要小心谨慎。

SemaphoreSlim 是C#中的一种轻量级信号量实现,用于线程同步和控制并发访问资源。与标准的 Semaphore 不同,SemaphoreSlim 是一种基于用户模式的同步构造,具有较低的开销,适用于在同一进程内控制线程并发的情况。

以下是关于 SemaphoreSlim 的一些基本概念和使用方法:

  1. 信号量计数SemaphoreSlim 维护一个整数值,表示可用的许可数或信号量计数。线程可以尝试等待信号量,并在等待时阻塞,直到信号量计数大于零。
  2. 等待和释放:线程可以调用 Wait 方法来等待信号量,并调用 Release 方法来释放信号量。每次调用 Release 方法会增加信号量计数,允许更多的线程通过 Wait 方法获取信号量。
  3. 超时处理SemaphoreSlim 支持设置超时,允许线程等待一段时间后放弃等待信号量。
  4. 适用于资源池和限制并发SemaphoreSlim 常用于限制对共享资源的并发访问,资源池、线程池等场景。

以下是一个简单的示例,演示如何使用 SemaphoreSlim

 using System;
 using System.Threading;
 using System.Threading.Tasks;
 ​
 class Program
 {
     static SemaphoreSlim semaphore = new SemaphoreSlim(3, 3); // 允许最多3个线程同时访问资源
 ​
     static async Task Main()
     {
         for (int i = 0; i < 5; i++)
         {
             _ = DoWorkAsync(i);
         }
 ​
         Console.ReadLine();
     }
 ​
     static async Task DoWorkAsync(int id)
     {
         await semaphore.WaitAsync();
         
         try
         {
             Console.WriteLine($"Thread {id} has acquired the semaphore and is working...");
             // 执行需要同步的工作
         }
         finally
         {
             semaphore.Release();
             Console.WriteLine($"Thread {id} has released the semaphore.");
         }
     }
 }

在这个示例中,我们创建了一个 SemaphoreSlim,允许最多3个线程同时访问资源。每个线程在开始工作前,使用 WaitAsync 方法等待信号量。只有在信号量计数大于零时,线程才能获取信号量并执行工作。完成工作后,线程释放信号量。

SemaphoreSlim 可以提供轻量级的线程同步,适用于许多并发控制场景。需要注意,SemaphoreSlim 不是全局锁,只能在同一进程内控制线程的并发,不能跨进程使用。

手动重置事件(ManualResetEvent)是C#中的一种同步构造,用于在线程之间进行通信和同步操作。它提供了一种机制,允许一个或多个线程等待某个事件的发生,并在事件发生后通知等待线程继续执行。

以下是关于 ManualResetEvent 的一些基本概念和使用方法:

  1. 事件状态ManualResetEvent 维护一个事件状态,可以是打开或关闭。当事件状态为打开时,等待事件的线程可以继续执行;当事件状态为关闭时,等待事件的线程将被阻塞。
  2. 信号和等待ManualResetEvent 提供了 Set 方法用于将事件状态设置为打开(信号),以及 Reset 方法用于将事件状态设置为关闭。等待事件的线程可以使用 WaitOne 方法等待事件状态变为打开。
  3. 等待和通知:等待事件的线程可以使用 WaitOne 方法等待事件的发生。一旦事件状态变为打开,等待线程将被唤醒,并继续执行。这允许线程之间的同步操作。
  4. 适用场景ManualResetEvent 常用于多线程协调和同步的场景,如线程等待某个条件的发生或线程池等待任务完成。

以下是一个简单的示例,演示如何使用 ManualResetEvent 进行线程同步:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
 ​
     static void Main()
     {
         Thread thread1 = new Thread(Worker);
         Thread thread2 = new Thread(Worker);
 ​
         thread1.Start();
         thread2.Start();
 ​
         // 主线程等待一段时间后设置事件状态为打开
         Thread.Sleep(2000);
         manualResetEvent.Set();
 ​
         thread1.Join();
         thread2.Join();
 ​
         Console.WriteLine("Both threads have completed.");
     }
 ​
     static void Worker()
     {
         Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is waiting...");
         manualResetEvent.WaitOne();
         Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is working...");
     }
 }

在这个示例中,我们创建了一个 ManualResetEvent 并初始化为关闭状态(即 false)。两个线程 thread1thread2 同时启动,但在执行工作之前都会等待 manualResetEvent 的状态变为打开。主线程等待一段时间后,通过调用 Set 方法将事件状态设置为打开,此时等待的线程将被唤醒并继续执行。

ManualResetEvent 是一种强大的同步工具,用于线程之间的通信和协调。需要注意,一旦事件状态变为打开,它会一直保持打开状态,直到通过调用 Reset 方法将其重新关闭。

自动重置事件(AutoResetEvent)是C#中的一种同步构造,与手动重置事件(ManualResetEvent)类似,但具有不同的行为。自动重置事件允许一个或多个线程等待某个事件的发生,但一旦事件发生,它会自动将事件状态重置为关闭,从而只允许一个等待线程继续执行。

以下是关于 AutoResetEvent 的一些基本概念和使用方法:

  1. 事件状态AutoResetEvent 维护一个事件状态,可以是打开或关闭。当事件状态为打开时,等待事件的线程可以继续执行。与手动重置事件不同,一旦有一个等待线程继续执行,事件状态会自动重置为关闭。
  2. 信号和等待AutoResetEvent 提供了 Set 方法用于将事件状态设置为打开(信号),以及 Reset 方法用于将事件状态设置为关闭。等待事件的线程可以使用 WaitOne 方法等待事件状态变为打开。
  3. 自动重置:与手动重置事件不同,AutoResetEvent 在有等待线程时会自动将事件状态重置为关闭。这意味着只有一个等待线程可以继续执行,其他等待线程必须再次等待事件的发生。
  4. 适用场景AutoResetEvent 常用于需要多个线程等待某个事件的发生,但只允许一个线程继续执行的场景,例如控制线程池中任务的执行。

以下是一个简单的示例,演示如何使用 AutoResetEvent 进行线程同步:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
 ​
     static void Main()
     {
         Thread thread1 = new Thread(Worker);
         Thread thread2 = new Thread(Worker);
 ​
         thread1.Start();
         thread2.Start();
 ​
         // 主线程等待一段时间后设置事件状态为打开
         Thread.Sleep(2000);
         autoResetEvent.Set();
 ​
         thread1.Join();
         thread2.Join();
 ​
         Console.WriteLine("Both threads have completed.");
     }
 ​
     static void Worker()
     {
         Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is waiting...");
         autoResetEvent.WaitOne();
         Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is working...");
     }
 }

在这个示例中,我们创建了一个 AutoResetEvent 并初始化为关闭状态(即 false)。两个线程 thread1thread2 同时启动,但在执行工作之前都会等待 autoResetEvent 的状态变为打开。主线程等待一段时间后,通过调用 Set 方法将事件状态设置为打开,此时只有一个等待的线程能够继续执行。与手动重置事件不同,AutoResetEvent 自动将事件状态重置为关闭,因此只有一个线程能够通过等待。

AutoResetEvent 可以用于协调多个线程对某个事件的响应,但要注意只有一个线程能够继续执行。

CountdownEvent 是C#中的一种同步构造,用于线程协调和控制,特别适用于在一个线程等待多个其他线程完成任务的场景。它允许一个线程等待多个线程完成某个操作,直到计数减至零。

以下是关于 CountdownEvent 的一些基本概念和使用方法:

  1. 计数初始化CountdownEvent 在创建时需要指定一个初始计数值。计数值表示需要等待的操作数量。
  2. 信号和等待:其他线程可以通过调用 Signal 方法来递减计数。一旦计数减至零,等待 CountdownEvent 的线程将被唤醒,继续执行。等待线程可以使用 Wait 方法等待计数减至零。
  3. 递减计数:每次调用 Signal 方法都会递减计数。如果计数减至零,所有等待线程都会被唤醒。
  4. 适用场景CountdownEvent 常用于等待多个线程完成某个操作后才继续执行,例如等待多个任务完成后合并它们的结果。

以下是一个简单的示例,演示如何使用 CountdownEvent 进行线程同步:

 using System;
 using System.Threading;
 ​
 class Program
 {
     static CountdownEvent countdownEvent = new CountdownEvent(3); // 初始化计数为3
 ​
     static void Main()
     {
         Thread thread1 = new Thread(Worker);
         Thread thread2 = new Thread(Worker);
         Thread thread3 = new Thread(Worker);
 ​
         thread1.Start();
         thread2.Start();
         thread3.Start();
 ​
         // 主线程等待所有线程完成
         countdownEvent.Wait();
 ​
         Console.WriteLine("All threads have completed.");
     }
 ​
     static void Worker()
     {
         Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is working...");
         // 执行需要同步的工作
 ​
         // 完成工作后递减计数
         countdownEvent.Signal();
     }
 }

在这个示例中,我们创建了一个 CountdownEvent 并初始化计数为3。三个线程 thread1thread2thread3 同时启动,各自执行工作后调用 Signal 方法递减计数。主线程通过调用 Wait 方法等待计数减至零,一旦所有线程完成工作,主线程继续执行。

CountdownEvent 可以用于等待多个线程完成某个操作,以协调和同步它们的执行。它提供了一种简单而强大的方式来实现这种线程协调。

Task Parallel Library`TPL)是C#中用于并行和并发编程的一组库和框架,它提供了一种高级的方式来管理和执行多个任务。TPL使得并行编程变得更加容易,并允许你以更高层次的抽象方式管理任务和线程。

以下是关于TPL的一些基本概念和重要特性:

  1. 任务(Task)TPL引入了Task类,用于表示一个异步操作或工作单元。任务是一种高级的抽象,它封装了线程管理和同步细节,让你更容易编写并行代码。
  2. 任务并行性TPL允许你创建和管理多个任务,这些任务可以并行执行,从而提高应用程序的性能。你可以使用Task.RunTask.Factory.StartNewasync/await等方式创建任务。
  3. 任务调度TPL具有内置的任务调度器,它能够智能地分配任务到可用的线程池线程。这确保了任务的有效调度和利用系统资源。
  4. 任务组合TPL提供了许多任务组合的方法,如Task.WhenAllTask.WhenAny,以便等待多个任务的完成或任何一个任务的完成。
  5. 取消任务TPL支持任务的取消,允许你在不需要完成的情况下停止任务的执行。
  6. 异常处理TPL提供了异常处理机制,允许你捕获和处理任务中的异常。
  7. 并行循环TPL还提供了Parallel.ForEachParallel.For等方法,用于并行迭代集合或执行循环操作。

以下是一个简单的示例,演示如何使用TPL创建和执行任务:

 using System;
 using System.Threading.Tasks;
 ​
 class Program
 {
     static void Main()
     {
         // 创建并行任务
         Task<int> task1 = Task.Run(() => ComputeSum(1, 100));
         Task<int> task2 = Task.Run(() => ComputeSum(101, 200));
         Task<int> task3 = Task.Run(() => ComputeSum(201, 300));
 ​
         // 等待所有任务完成
         Task.WhenAll(task1, task2, task3).ContinueWith(completedTasks =>
         {
             // 计算总和
             int totalSum = completedTasks.Result[0] + completedTasks.Result[1] + completedTasks.Result[2];
             Console.WriteLine($"Total Sum: {totalSum}");
         });
 ​
         Console.ReadLine();
     }
 ​
     static int ComputeSum(int start, int end)
     {
         int sum = 0;
         for (int i = start; i <= end; i++)
         {
             sum += i;
         }
         return sum;
     }
 }

在这个示例中,我们创建了三个并行任务,分别计算从1到100、101到200和201到300的和。然后,我们使用Task.WhenAll等待所有任务完成,并在所有任务完成后计算总和。

TPL使得编写并行代码变得更加容易,同时提供了一些强大的工具和特性,以帮助你管理和优化并行操作。它是C#中处理并行性和异步编程的首选方式之一。