C# 中使用线程、Task和 ThreadPool 的并发性
C# 中的并发性涉及使用线程和任务等功能在程序中同时执行任务。这就像让多个工人同时完成不同的工作。这在现代应用程序中至关重要,因为它使它们更快、响应更迅速。并发性可确保我们的应用程序平稳运行,快速响应用户操作,并明智地使用主机的功能,使它们可靠并准备好执行任何任务。
了解线程
线程的基础知识
线程在软件中扮演独立工作角色的角色,每个线程同时处理特定操作。它们的功能类似于并行的执行流,允许任务同时进行。在 C# 中,线程的创建和管理涉及定义这些并行流、指定它们应何时启动以及监督其生命周期,包括创建、执行和完成等关键状态。
线程同步
线程同步是顺利管理这些并行操作的关键要素。它确保线程有效地协调和通信,防止在多个线程访问共享资源时可能出现的冲突。
线程同步中可能会出现死锁等挑战,其中线程卡住等待彼此继续。在这些情况下,需要实施策略来防止死锁,确保线程之间的高效协作和应用程序的最佳性能。
探索任务
任务是可以独立于其他任务执行的工作单元。任务是轻量级的,可以由操作系统安排在不同的线程上并发运行。这使它们成为异步编程的理想选择,在异步编程中,可以同时执行多个任务而不会阻塞主线程。
与传统线程相比,任务具有以下几个优点:
轻量级:与线程相比,任务的资源密集程度更低,从而减少了开销并提高了性能。
托管执行:任务由运行时环境管理,运行时环境处理资源分配、调度和同步。
异常处理:任务提供了一种在异步代码中进行异常处理的结构化方法。
任务与线程:主要区别
虽然任务和线程都代表工作单元,但它们在几个关键方面有所不同:
线程模型:线程由操作系统管理,而任务由运行时环境管理。
资源管理:线程需要显式的资源管理,而任务则由运行时管理。
异常处理:基于线程的异常可能难以处理,而任务则提供了一种结构化的异常处理方法。
using System; using System.Threading.Tasks; class Program { static async Task Main() { // Creating a task Task task1= Task.Run(() => Console.WriteLine("Doing some work in a task.")); // Waiting for the task to complete await task1; Console.WriteLine("Task completed!"); } }
使用任务进行异步编程
和关键字是使用任务进行异步编程的基础。关键字将方法标记为异步,指示它可能包含异步操作。关键字用于暂停异步方法的执行,直到异步操作完成。
异步方法支持非阻塞 I/O 操作,允许主线程在异步操作进行时保持响应。这提高了应用程序的整体响应能力。
异步代码中的异常处理是使用 / 块处理的。在异步方法中引发的异常将传播到调用方,在那里可以捕获并适当地处理它们。
using System; using System.Threading.Tasks; class Program { static async Task Main() { Task myTask1 = Task.Run(async () => { Console.WriteLine("Chef 1 is preparing Dish 1"); await Task.Delay(10000); // Chef 1 takes 10 seconds to prepare Dish 1 Console.WriteLine("Dish 1 is ready!"); }); Task myTask2 = Task.Run(async () => { Console.WriteLine("Chef 2 is preparing Dish 2"); await Task.Delay(1000); // Chef 2 takes 1 second to prepare Dish 2 Console.WriteLine("Dish 2 is ready!"); }); Task myTask3 = Task.Run(async () => { Console.WriteLine("Chef 3 is preparing Dish 3"); await Task.Delay(1000); // Chef 3 takes 1 second to prepare Dish 3 Console.WriteLine("Dish 3 is ready!"); }); await Task.WhenAll(myTask1, myTask2, myTask3); Console.WriteLine("Manager: All dishes are ready! Task completed!"); } }
任务并行库 (TPL)
任务并行库 (TPL) 是一组类,用于简化 .NET 中的并行编程。它提供了几个用于管理任务和协调其执行的功能。
Parallel.ForEach 和 Parallel.For
Parallel.ForEach和 是用于为集合中的每个元素并行执行委托的方法。 保留迭代顺序,但不保证顺序。
using System; using System.Threading.Tasks; class Program { static void Main() { Chef[] chefs = { new Chef("Alice"), new Chef("Bob"), new Chef("Charlie"), new Chef("David") }; // Parallel.ForEach example Parallel.ForEach(chefs, chef => { chef.Cook(); }); // Parallel.For example Parallel.For(0, chefs.Length, i => { chefs[i].Cook(); }); } } class Chef { public string Name { get; } public Chef(string name) { Name = name; } public void Cook() { Console.WriteLine($"{Name} is cooking..."); Task.Delay(1000).Wait(); // Simulating cooking time Console.WriteLine($"{Name} finished cooking."); } }
在提供的 chef 示例中,使用 和 并行处理厨师数组。对于每个厨师,都会调用该方法,模拟烹饪活动。TPL 负责管理并行执行,允许厨师同时工作。需要注意的是,不能保证执行顺序与数组中元素的顺序相同。
并行 LINQ (PLINQ)
并行 LINQ (PLINQ) 是 LINQ 的扩展,支持并行执行 LINQ 查询。PLINQ 利用 TPL 在多个线程之间分配查询工作负载。
using System; using System.Linq; class Program { static void Main() { Chef[] chefs = { new Chef("Alice"), new Chef("Bob"), new Chef("Charlie"), new Chef("David") }; // PLINQ example var results = chefs.AsParallel().Select(chef => { chef.Cook(); return chef.Name; }).ToList(); Console.WriteLine("Cooking completed for chefs: " + string.Join(", ", results)); } } class Chef { public string Name { get; } public Chef(string name) { Name = name; } public void Cook() { Console.WriteLine($"{Name} is cooking..."); Task.Delay(1000).Wait(); // Simulating cooking time Console.WriteLine($"{Name} finished cooking."); } }
在 chef 示例中,PLINQ 应用于 chef 数组。该操作是并行执行的,其中调用每个厨师的方法。然后将结果收集到一个列表中。PLINQ 提供了一种并行化 LINQ 查询的便捷方法,而无需显式线程管理。
数据并行性
数据并行性是一种在多个线程之间分配数据密集型计算以提高性能的技术。TPL 提供了用于对数据进行分区和在每个分区上执行任务的机制。
using System; using System.Linq; class Program { static void Main() { int[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // Data parallelism example Parallel.ForEach(data, item => { Console.WriteLine($"Processing item: {item}, Thread: {Task.CurrentId}"); // Simulating some data-intensive computation Task.Delay(500).Wait(); }); } }
在此示例中,整数数组表示数据,并用于并发处理每个项目。该语句指示项的并行处理,并显示关联的线程 ID。 数据并行性对于可以在数据集的不同部分上独立执行任务的方案特别有用,从而可以有效利用可用资源。
ThreadPool 管理
ThreadPool 基础知识
ThreadPool 与单个线程:
-
使用 ThreadPool 对于具有许多短期任务或异步操作的方案非常有用,因为它避免了不断创建和销毁线程的开销。
-
单个线程由开发人员显式创建和管理。虽然它们提供了更多的控制,但管理大量线程可能会导致资源耗尽。
默认设置和配置:
-
ThreadPool 具有由运行时自动管理的默认设置。
-
您可以使用 and 等方法配置 ThreadPool,以设置最大和最小线程数,从而影响 ThreadPool 管理其资源的方式。
// Example: Configuring the ThreadPool ThreadPool.SetMinThreads(5, 5); ThreadPool.SetMaxThreads(20, 20);
ThreadPool 匮乏
当 ThreadPool 中的所有线程都被占用,并且新任务无法获取线程进行执行时,就会发生 ThreadPool 饥饿。ThreadPool 匮乏的原因可能包括长时间运行的任务、同步阻塞操作或任务数量与可用线程之间的不平衡。
检测和诊断 ThreadPool 饥饿:
-
监视 ThreadPool 的可用线程、待处理任务和其他指标。
-
如果报告零个可用线程,则表示可能存在 ThreadPool 匮乏。
-
分析工具和性能监控可以帮助诊断和识别导致饥饿的模式。
您可以使用以下代码创建线程池匮乏场景:
using System; using System.Threading; class Program { private static int totalOrders = 100; private static int completedOrders = 0; private static object lockObject = new object(); static void Main() { // Set the maximum number of chefs (threads) in the kitchen (thread pool) to 10 ThreadPool.SetMaxThreads(10, 10); Console.WriteLine("Press Enter to start the simulation..."); Console.ReadLine(); // Simulate a restaurant scenario with chefs (thread pool) handling orders (tasks) for (int i = 0; i < totalOrders; i++) { ThreadPool.QueueUserWorkItem(ProcessOrder, i); } // Monitor the completion of orders (tasks) while (true) { lock (lockObject) { if (completedOrders == totalOrders) { Console.WriteLine("All orders completed. Press Enter to exit."); break; } // Check for kitchen (thread pool) starvation int pendingOrders = totalOrders - completedOrders; // Get kitchen (thread pool) information int maxChefs, minChefs, availableChefs; ThreadPool.GetMaxThreads(out maxChefs, out _); ThreadPool.GetMinThreads(out minChefs, out _); ThreadPool.GetAvailableThreads(out availableChefs, out _); Console.WriteLine($"Kitchen (Thread pool) information - MaxChefs: {maxChefs}, MinChefs: {minChefs}, AvailableChefs: {availableChefs}"); Console.WriteLine($"Pending orders: {pendingOrders}"); // Check for kitchen (thread pool) starvation if (availableChefs == 0) { Console.WriteLine("Kitchen (Thread pool) starvation detected!"); } } Thread.Sleep(100); // Wait for a short duration before checking again } Console.ReadLine(); } static void ProcessOrder(object orderContext) { Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} is cooking order {orderContext}"); // Simulate cooking time Thread.Sleep(1000); Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} completed cooking order {orderContext}"); lock (lockObject) { completedOrders++; } } }
使用 async/await 解决 ThreadPool 饥饿问题:
-
利用异步编程可以帮助缓解 ThreadPool 。
-
通过使用 ,线程在异步操作期间不会被阻塞,从而允许它们被释放回 ThreadPool。async/await
-
这对于 I/O 绑定操作特别有效,在这些操作中,线程不会主动计算,而是等待外部资源。
您可以通过将 COTO 转换为以下格式来解决上述线程池饥饿问题:
using System; using System.Threading.Tasks; class Program { private static int totalOrders = 100; private static int completedOrders = 0; private static object lockObject = new object(); static async Task Main() { // Set the maximum number of chefs (threads) in the kitchen (thread pool) to 10 ThreadPool.SetMaxThreads(10, 10); Console.WriteLine("Press Enter to start the simulation..."); Console.ReadLine(); // Simulate a restaurant scenario with chefs (thread pool) handling orders (tasks) asynchronously var orderTasks = new Task[totalOrders]; for (int i = 0; i < totalOrders; i++) { orderTasks[i] = ProcessOrderAsync(i); } // Wait for all orders to be completed await Task.WhenAll(orderTasks); Console.WriteLine("All orders completed. Press Enter to exit."); Console.ReadLine(); } static async Task ProcessOrderAsync(int orderNumber) { Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} is cooking order {orderNumber}"); // Simulate asynchronous cooking time await Task.Delay(1000); Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} completed cooking order {orderNumber}"); lock (lockObject) { completedOrders++; } MonitorThreadPool(); } static void MonitorThreadPool() { lock (lockObject) { // Check for kitchen (thread pool) starvation int pendingOrders = totalOrders - completedOrders; // Get kitchen (thread pool) information int maxChefs, minChefs, availableChefs; ThreadPool.GetMaxThreads(out maxChefs, out _); ThreadPool.GetMinThreads(out minChefs, out _); ThreadPool.GetAvailableThreads(out availableChefs, out _); Console.WriteLine($"Kitchen (Thread pool) information - MaxChefs: {maxChefs}, MinChefs: {minChefs}, AvailableChefs: {availableChefs}"); Console.WriteLine($"Pending orders: {pendingOrders}"); // Check for kitchen (thread pool) starvation if (availableChefs == 0) { Console.WriteLine("Kitchen (Thread pool) starvation detected!"); } } } }
ThreadPool 匮乏,即使在使用 时,也可能由于各种原因而发生,理解和解决这些问题对于维护应用程序性能至关重要。使用 时,确保异步方法确实是非阻塞的,这一点很重要。如果方法中存在同步阻塞操作,它们可能会占用线程并导致 ThreadPool 匮乏。您应仔细检查异步方法,以消除任何可能妨碍线程有效利用的阻塞操作。
// Incorrect: Blocking operation inside async method static async Task MyAsyncMethod() { // This synchronous operation can lead to ThreadPool starvation SomeBlockingOperation(); await SomeAsyncOperation(); } // Correct: Non-blocking operations inside async method static async Task MyAsyncMethod() { // Asynchronous operation await SomeAsyncOperation(); }
此外,方法内部长时间运行的操作或受 CPU 限制的操作仍可能带来挑战。如果此类操作不是真正的异步操作,则可能导致 ThreadPool 匮乏。为了解决这个问题,您应该将 CPU 密集或长时间运行的操作移出上下文,仅在必要时使用各种机制。
// Incorrect: Long-running CPU-bound operation inside async method static async Task MyAsyncMethod() { // This CPU-bound operation can lead to ThreadPool starvation await Task.Run(() => SomeLongRunningOperation()); } // Correct: Moving long-running operation outside async method static async Task MyAsyncMethod() { // Asynchronous operation without blocking threads await SomeAsyncOperation(); } // Long-running operation moved outside async method static void SomeLongRunningOperation() { // CPU-bound operation }
ThreadPool 配置是另一个关键因素。显式设置使用 或使用 的线程不足可能会限制可用线程数,从而导致 ThreadPool 匮乏。根据应用程序的需求正确配置 ThreadPool 对于获得最佳性能至关重要。
// Incorrect: Configuring the ThreadPool with insufficient threads ThreadPool.SetMinThreads(2, 2); ThreadPool.SetMaxThreads(5, 5); // Correct: Adjusting ThreadPool settings ThreadPool.SetMinThreads(10, 10); ThreadPool.SetMaxThreads(50, 50);
外部因素(例如外部服务或依赖项的问题)也可能导致 ThreadPool 匮乏。网络或 I/O 瓶颈可能会导致线程等待资源。识别和解决这些外部因素对于整体系统的稳定性至关重要。
高级线程和任务注意事项
线程安全
线程安全在并发编程中的重要性
在并发编程中,当多个线程可以访问和修改共享资源时,确保线程安全对于避免数据损坏至关重要。该语句通常用于同步对共享数据的访问。在以下示例中,类使用 a 安全地递增共享计数器:
using System; using System.Threading; class SharedResource { private int counter = 0; private object lockObject = new object(); public void IncrementCounter() { lock (lockObject) { counter++; Console.WriteLine($"Counter: {counter}, Thread ID: {Thread.CurrentThread.ManagedThreadId}"); } } } class Program { static void Main() { SharedResource sharedResource = new SharedResource(); // Simulating multiple threads incrementing the counter for (int i = 0; i < 5; i++) { new Thread(() => sharedResource.IncrementCounter()).Start(); } Console.ReadLine(); } }
不可变类型及其作用
不可变类型,其状态在创建后无法修改,本质上提供线程安全性。在此示例中,将创建一个具有不可变属性的类:
using System; public class ImmutableData { public int Value { get; } public ImmutableData(int value) { Value = value; } } class Program { static void Main() { ImmutableData immutableData = new ImmutableData(42); // The state of immutableData cannot be modified after creation Console.WriteLine($"Immutable Data Value: {immutableData.Value}"); } }
ThreadLocal<T> 用于每个线程的数据
ThreadLocal<T>是一个有用的类,用于在不同步的情况下管理每线程数据。在以下示例中,a 用于存储每个线程的唯一值:
using System; using System.Threading; class Program { static ThreadLocal<int> threadLocalValue = new ThreadLocal<int>(() => 0); static void SetThreadLocalValue(int newValue) { threadLocalValue.Value = newValue; } static int GetThreadLocalValue() { return threadLocalValue.Value; } static void Main() { // Simulating multiple threads with unique per-thread values for (int i = 1; i <= 3; i++) { new Thread(() => { SetThreadLocalValue(i); Console.WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}, ThreadLocal Value: {GetThreadLocalValue()}"); }).Start(); } Console.ReadLine(); } }
背景线程与前景线程
区分背景线程和前景线程
.NET 中的线程分为后台线程和前台线程。前台线程使应用程序保持活动状态,直到它们完成,而后台线程不会阻止应用程序终止。默认情况下,ThreadPool 创建的线程是后台线程。在此示例中,我们创建前台线程和后台线程:
using System; using System.Threading; class Program { static void Main() { Thread foregroundThread = new Thread(() => { Console.WriteLine("Foreground Thread"); }); Thread backgroundThread = new Thread(() => { Console.WriteLine("Background Thread"); }); foregroundThread.Start(); // Foreground thread backgroundThread.IsBackground = true; // Set as background thread backgroundThread.Start(); Console.WriteLine("Main Thread Exiting"); } }
对应用程序终止的影响
前台线程使应用程序保持活动状态,直到它们完成。在此示例中,应用程序将等待前台线程完成,然后退出,但不会等待后台线程:
using System; using System.Threading; class Program { static void Main() { Thread foregroundThread = new Thread(() => { Console.WriteLine("Foreground Thread"); Thread.Sleep(3000); // Simulating work }); Thread backgroundThread = new Thread(() => { Console.WriteLine("Background Thread"); Thread.Sleep(2000); // Simulating work }); foregroundThread.Start(); // Foreground thread backgroundThread.IsBackground = true; // Set as background thread backgroundThread.Start(); Console.WriteLine("Main Thread Exiting"); } }
线程和任务优先级
调整线程和任务优先级
可以使用 调整线程和任务优先级。在此示例中,我们创建一个高优先级和低优先级线程:
using System; using System.Threading; class Program { static void Main() { Thread highPriorityThread = new Thread(() => { Thread.CurrentThread.Priority = ThreadPriority.Highest; Console.WriteLine("High-Priority Thread"); }); Thread lowPriorityThread = new Thread(() => { Thread.CurrentThread.Priority = ThreadPriority.Lowest; Console.WriteLine("Low-Priority Thread"); }); highPriorityThread.Start(); lowPriorityThread.Start(); Console.WriteLine("Main Thread Exiting"); } }
对整体系统性能的影响
虽然调整线程或任务优先级会影响它们的调度顺序,但考虑对整体系统性能的影响至关重要。大量调整可能会影响调度程序的公平性,并可能导致优先级倒置。建议明智地使用优先级调整。
学习 C# 并发(包括线程、任务和 ThreadPool)是构建响应式应用程序的关键。了解并行执行、最佳实践和高级注意事项可确保开发更顺畅,并增强整体 C# 并发体验。