一、Parallel 并行编程1.1 Parallel.Invoke基本用法// 一次执行多个委托 Parallel.Invoke( () { DoWork1(); }, () { DoWork2(); }, () { DoWork3(); } );特点可以传入多个委托会开启线程执行(可能是新线程也可能是主线程参与)会阻塞主线程相当于主线程等待所有子线程执行结束限制线程数量ParallelOptions options new ParallelOptions(); options.MaxDegreeOfParallelism 3; // 最多开启3个线程 Parallel.Invoke(options, () { DoWork1(); }, () { DoWork2(); }, () { DoWork3(); }, () { DoWork4(); }, () { DoWork5(); } );不阻塞界面的方式Task.Run(() { Parallel.Invoke( () { DoWork1(); }, () { DoWork2(); }, () { DoWork3(); } ); });1.2 Parallel.For基本用法// 100个任务限制3个线程执行 ParallelOptions options new ParallelOptions(); options.MaxDegreeOfParallelism 3; Parallel.For(0, 100, options, index { Console.WriteLine($index: {index}, 线程ID: {Thread.CurrentThread.ManagedThreadId:00}); DoWork(index); });核心价值控制线程数量避免线程泛滥100个任务不会开启100个线程3个线程平摊100个任务既提高性能又不过度消耗资源1.3 Parallel.ForEach基本用法Listint dataList new Listint { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; ParallelOptions options new ParallelOptions(); options.MaxDegreeOfParallelism 3; // 3个线程处理10个任务 Parallel.ForEach(dataList, options, item { Console.WriteLine($处理: {item}, 线程ID: {Thread.CurrentThread.ManagedThreadId:00}); ProcessData(item); });应用场景批量数据处理文件批量操作大量计算任务1.4 Parallel总结优点基于Task的封装使用简单自动控制线程数量避免线程过多导致资源浪费使用建议有大量独立任务时使用需要控制并发数量时使用任务之间无依赖关系时使用二、多线程异常处理2.1 异常捕获问题普通try-catch无法捕获多线程异常try { Task task Task.Run(() { int i 0; int j 10; int k j / i; // 除以0异常 }); // 异常被吞掉捕获不到 } catch (Exception ex) { Console.WriteLine(ex.Message); // 不会执行 }2.2 正确的异常捕获方法1使用Wait等待try { Task task Task.Run(() { int i 0; int j 10; int k j / i; }); task.Wait(); // 必须等待才能捕获异常 } catch (AggregateException aex) // 多线程异常类型 { Console.WriteLine(aex.Message); foreach (var innerEx in aex.InnerExceptions) { Console.WriteLine(innerEx.Message); } } catch (Exception ex) { Console.WriteLine(ex.Message); }方法2多个任务的异常处理try { ListTask tasks new ListTask(); for (int i 0; i 20; i) { string keyword $Task_{i}; tasks.Add(Task.Run(() { Thread.Sleep(new Random().Next(50, 100)); if (keyword Task_6) throw new Exception(Task_6异常); if (keyword Task_9) throw new Exception(Task_9异常); if (keyword Task_12) throw new Exception(Task_12异常); })); } Task.WaitAll(tasks.ToArray()); } catch (AggregateException aex) { // 遍历所有内部异常 foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } }2.3 异常类型优先级catch (AggregateException aex) // 具体异常类型优先匹配 { // 处理多线程异常 } catch (Exception ex) // 抽象异常类型其次匹配 { // 处理其他异常 }三、线程取消(CancellationToken)3.1 线程取消原理核心概念线程无法从外部取消只能自己取消自己线程取消本质是向外抛出异常使用CancellationTokenSource统一管理3.2 标准的线程取消基本用法CancellationTokenSource cts new CancellationTokenSource(); // cts.IsCancellationRequested 默认为 false // cts.Cancel() 执行后变为 true(只能从false变true不可逆) Task task Task.Run(() { // 检查是否取消 if (cts.IsCancellationRequested) { Console.WriteLine(任务被取消); return; } // 或者直接抛出异常 cts.Token.ThrowIfCancellationRequested(); // 执行任务 DoWork(); }, cts.Token); // 取消任务 cts.Cancel();3.3 实际应用场景场景多个任务一个失败全部取消try { ListTask tasks new ListTask(); CancellationTokenSource cts new CancellationTokenSource(); for (int i 0; i 100; i) { string keyword $Task_{i}; tasks.Add(Task.Run(() { Thread.Sleep(new Random().Next(10, 300)); // 任务开始前检查 cts.Token.ThrowIfCancellationRequested(); try { if (keyword Task_6) { throw new Exception(Task_6异常); } // 执行任务 DoWork(); } catch (Exception) { cts.Cancel(); // 发生异常取消所有任务 throw; } // 任务结束前检查 cts.Token.ThrowIfCancellationRequested(); }, cts.Token)); // 传入Token未开始的任务不再开启 } Task.WaitAll(tasks.ToArray()); } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } }3.4 线程取消的三种情况已经结束的线程无法取消(管不住)正在执行的线程可以取消抛出异常结束还未开始的线程不再开启直接跳过四、线程安全4.1 什么是线程安全定义一段业务逻辑单线程执行和多线程执行的结果完全一致就是线程安全否则就是线程不安全线程不安全示例Listint list new Listint(); // 单线程结果是10000 for (int i 0; i 10000; i) { list.Add(i); } Console.WriteLine(list.Count); // 10000 // 多线程结果不确定 Listint list2 new Listint(); for (int i 0; i 10000; i) { Task.Run(() { list2.Add(i); }); } Thread.Sleep(5000); Console.WriteLine(list2.Count); // 可能是9937、9976、9982等4.2 解决线程安全的方法方法1加锁(不推荐)private readonly static object lockObj new object(); Listint list new Listint(); for (int i 0; i 10000; i) { Task.Run(() { lock (lockObj) // 加锁独占资源 { list.Add(i); } }); }注意事项锁对象定义private readonly static object lockObj new object();不要锁String类型不要锁this加锁会影响性能相当于反多线程方法2分块/分区执行(推荐)Listint list1 new Listint(); Listint list2 new Listint(); Listint list3 new Listint(); int total 10000; int part1 3000; int part2 6000; int part3 10000; ListTask tasks new ListTask(); // 分三块每块单线程执行(线程安全) tasks.Add(Task.Run(() { for (int i 0; i part1; i) { list1.Add(i); } })); tasks.Add(Task.Run(() { for (int i part1; i part2; i) { list2.Add(i); } })); tasks.Add(Task.Run(() { for (int i part2; i part3; i) { list3.Add(i); } })); Task.WaitAll(tasks.ToArray()); // 单线程汇总 list1.AddRange(list2); list1.AddRange(list3); Console.WriteLine(list1.Count); // 10000方法3使用线程安全集合(推荐)// 线程安全的集合类型 BlockingCollectionint blockingList new BlockingCollectionint(); ConcurrentBagint concurrentBag new ConcurrentBagint(); ConcurrentDictionarystring, int concurrentDict new ConcurrentDictionarystring, int(); ConcurrentQueueint concurrentQueue new ConcurrentQueueint(); ConcurrentStackint concurrentStack new ConcurrentStackint(); // 使用示例 ConcurrentBagint bag new ConcurrentBagint(); for (int i 0; i 10000; i) { Task.Run(() { bag.Add(i); // 线程安全无需加锁 }); } Thread.Sleep(5000); Console.WriteLine(bag.Count); // 100004.3 线程安全总结方法优点缺点推荐度加锁简单直接性能差本质上是把多线程退化成串行⭐分块执行性能好设计优雅需要设计分区逻辑⭐⭐⭐⭐⭐线程安全集合简单性能好需要替换数据结构⭐⭐⭐⭐⭐⚠️ 加锁虽然能解决问题但锁的范围内代码实际上是串行执行的等于抵消了多线程的并发优势只在没有更好方案时才考虑。五、中间变量问题5.1 问题现象// 错误示例所有线程输出的i都是5 for (int i 0; i 5; i) { Task.Run(() { Console.WriteLine($i {i}); // 输出5, 5, 5, 5, 5 }); }5.2 问题原因Task开启线程有延迟(虽然很短)开启多线程不会阻塞主线程循环很快当线程真正执行时循环已结束此时i已经变成55.3 解决方案// 正确示例使用中间变量 for (int i 0; i 5; i) { int k i; // 每次循环创建新的变量 Task.Run(() { Console.WriteLine($i {i}, k {k}); // i输出5k输出0,1,2,3,4 }); }原理每次循环创建新的变量k每个线程使用的是各自循环内的kk的值在创建时就已确定六、实战案例双色球开奖系统6.1 需求分析规则6个红球(01-33不重复)1个蓝球(01-16)点击开始所有球同时跳动变化点击停止显示最终结果技术要点7个球 7个线程每个线程独立运行相互不影响红球需要去重避免死锁6.2 核心代码private bool isRunning true; private ListTask taskList new ListTask(); private static object lockObj new object(); private void btnStart_Click(object sender, EventArgs e) { isRunning true; taskList.Clear(); // 遍历所有Label控件 foreach (var control in gboSSQ.Controls) { if (control is Label label) { taskList.Add(Task.Run(() { if (label.Name.Contains(Blue)) // 蓝球 { while (isRunning) { int index new RandomHelper().GetRandomNumberDelay(0, 16); string num blueNums[index]; this.Invoke(new Action(() { label.Text num; })); } } else // 红球 { while (isRunning) { int index new RandomHelper().GetRandomNumberDelay(0, 33); string num redNums[index]; lock (lockObj) { var currentNums GetCurrentRedNumbers(); if (!currentNums.Contains(num)) { this.Invoke(new Action(() { label.Text num; })); } } } } })); } } // 所有任务完成后的回调 Task.Factory.ContinueWhenAll(taskList.ToArray(), tasks { this.Invoke(new Action(() { ShowResult(); isRunning true; })); }); } private void btnStop_Click(object sender, EventArgs e) { isRunning false; // 不能在这里Wait会死锁 // Task.WaitAll(taskList.ToArray()); // 死锁 }6.3 关键技术点1. 随机数去重public class RandomHelper { public int GetRandomNumber(int min, int max) { Guid guid Guid.NewGuid(); int seed DateTime.Now.Millisecond; // 使用GUID增加随机性 foreach (char c in guid.ToString()) { seed GetSeedIncrement(c); } Random random new Random(seed); return random.Next(min, max); } }2. 跨线程更新UI// 子线程不能直接操作UI需要委托给主线程 this.Invoke(new Action(() { label.Text num; }));3. 避免死锁// 错误主线程等待子线程子线程需要主线程更新UI Task.WaitAll(taskList.ToArray()); // 死锁 // 正确使用回调 Task.Factory.ContinueWhenAll(taskList.ToArray(), tasks { // 不阻塞主线程 });七、小结Parallel控制线程数量避免泛滥异常处理使用 AggregateException必须 Wait 才能捕获线程取消CancellationTokenSource 统一管理线程安全优先使用分块或线程安全集合加锁是最后手段中间变量循环中开启线程时必须用临时变量捕获循环值死锁避免主线程和子线程相互等待
10-C#.Net-多线程-进阶篇-学习笔记
一、Parallel 并行编程1.1 Parallel.Invoke基本用法// 一次执行多个委托 Parallel.Invoke( () { DoWork1(); }, () { DoWork2(); }, () { DoWork3(); } );特点可以传入多个委托会开启线程执行(可能是新线程也可能是主线程参与)会阻塞主线程相当于主线程等待所有子线程执行结束限制线程数量ParallelOptions options new ParallelOptions(); options.MaxDegreeOfParallelism 3; // 最多开启3个线程 Parallel.Invoke(options, () { DoWork1(); }, () { DoWork2(); }, () { DoWork3(); }, () { DoWork4(); }, () { DoWork5(); } );不阻塞界面的方式Task.Run(() { Parallel.Invoke( () { DoWork1(); }, () { DoWork2(); }, () { DoWork3(); } ); });1.2 Parallel.For基本用法// 100个任务限制3个线程执行 ParallelOptions options new ParallelOptions(); options.MaxDegreeOfParallelism 3; Parallel.For(0, 100, options, index { Console.WriteLine($index: {index}, 线程ID: {Thread.CurrentThread.ManagedThreadId:00}); DoWork(index); });核心价值控制线程数量避免线程泛滥100个任务不会开启100个线程3个线程平摊100个任务既提高性能又不过度消耗资源1.3 Parallel.ForEach基本用法Listint dataList new Listint { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; ParallelOptions options new ParallelOptions(); options.MaxDegreeOfParallelism 3; // 3个线程处理10个任务 Parallel.ForEach(dataList, options, item { Console.WriteLine($处理: {item}, 线程ID: {Thread.CurrentThread.ManagedThreadId:00}); ProcessData(item); });应用场景批量数据处理文件批量操作大量计算任务1.4 Parallel总结优点基于Task的封装使用简单自动控制线程数量避免线程过多导致资源浪费使用建议有大量独立任务时使用需要控制并发数量时使用任务之间无依赖关系时使用二、多线程异常处理2.1 异常捕获问题普通try-catch无法捕获多线程异常try { Task task Task.Run(() { int i 0; int j 10; int k j / i; // 除以0异常 }); // 异常被吞掉捕获不到 } catch (Exception ex) { Console.WriteLine(ex.Message); // 不会执行 }2.2 正确的异常捕获方法1使用Wait等待try { Task task Task.Run(() { int i 0; int j 10; int k j / i; }); task.Wait(); // 必须等待才能捕获异常 } catch (AggregateException aex) // 多线程异常类型 { Console.WriteLine(aex.Message); foreach (var innerEx in aex.InnerExceptions) { Console.WriteLine(innerEx.Message); } } catch (Exception ex) { Console.WriteLine(ex.Message); }方法2多个任务的异常处理try { ListTask tasks new ListTask(); for (int i 0; i 20; i) { string keyword $Task_{i}; tasks.Add(Task.Run(() { Thread.Sleep(new Random().Next(50, 100)); if (keyword Task_6) throw new Exception(Task_6异常); if (keyword Task_9) throw new Exception(Task_9异常); if (keyword Task_12) throw new Exception(Task_12异常); })); } Task.WaitAll(tasks.ToArray()); } catch (AggregateException aex) { // 遍历所有内部异常 foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } }2.3 异常类型优先级catch (AggregateException aex) // 具体异常类型优先匹配 { // 处理多线程异常 } catch (Exception ex) // 抽象异常类型其次匹配 { // 处理其他异常 }三、线程取消(CancellationToken)3.1 线程取消原理核心概念线程无法从外部取消只能自己取消自己线程取消本质是向外抛出异常使用CancellationTokenSource统一管理3.2 标准的线程取消基本用法CancellationTokenSource cts new CancellationTokenSource(); // cts.IsCancellationRequested 默认为 false // cts.Cancel() 执行后变为 true(只能从false变true不可逆) Task task Task.Run(() { // 检查是否取消 if (cts.IsCancellationRequested) { Console.WriteLine(任务被取消); return; } // 或者直接抛出异常 cts.Token.ThrowIfCancellationRequested(); // 执行任务 DoWork(); }, cts.Token); // 取消任务 cts.Cancel();3.3 实际应用场景场景多个任务一个失败全部取消try { ListTask tasks new ListTask(); CancellationTokenSource cts new CancellationTokenSource(); for (int i 0; i 100; i) { string keyword $Task_{i}; tasks.Add(Task.Run(() { Thread.Sleep(new Random().Next(10, 300)); // 任务开始前检查 cts.Token.ThrowIfCancellationRequested(); try { if (keyword Task_6) { throw new Exception(Task_6异常); } // 执行任务 DoWork(); } catch (Exception) { cts.Cancel(); // 发生异常取消所有任务 throw; } // 任务结束前检查 cts.Token.ThrowIfCancellationRequested(); }, cts.Token)); // 传入Token未开始的任务不再开启 } Task.WaitAll(tasks.ToArray()); } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } }3.4 线程取消的三种情况已经结束的线程无法取消(管不住)正在执行的线程可以取消抛出异常结束还未开始的线程不再开启直接跳过四、线程安全4.1 什么是线程安全定义一段业务逻辑单线程执行和多线程执行的结果完全一致就是线程安全否则就是线程不安全线程不安全示例Listint list new Listint(); // 单线程结果是10000 for (int i 0; i 10000; i) { list.Add(i); } Console.WriteLine(list.Count); // 10000 // 多线程结果不确定 Listint list2 new Listint(); for (int i 0; i 10000; i) { Task.Run(() { list2.Add(i); }); } Thread.Sleep(5000); Console.WriteLine(list2.Count); // 可能是9937、9976、9982等4.2 解决线程安全的方法方法1加锁(不推荐)private readonly static object lockObj new object(); Listint list new Listint(); for (int i 0; i 10000; i) { Task.Run(() { lock (lockObj) // 加锁独占资源 { list.Add(i); } }); }注意事项锁对象定义private readonly static object lockObj new object();不要锁String类型不要锁this加锁会影响性能相当于反多线程方法2分块/分区执行(推荐)Listint list1 new Listint(); Listint list2 new Listint(); Listint list3 new Listint(); int total 10000; int part1 3000; int part2 6000; int part3 10000; ListTask tasks new ListTask(); // 分三块每块单线程执行(线程安全) tasks.Add(Task.Run(() { for (int i 0; i part1; i) { list1.Add(i); } })); tasks.Add(Task.Run(() { for (int i part1; i part2; i) { list2.Add(i); } })); tasks.Add(Task.Run(() { for (int i part2; i part3; i) { list3.Add(i); } })); Task.WaitAll(tasks.ToArray()); // 单线程汇总 list1.AddRange(list2); list1.AddRange(list3); Console.WriteLine(list1.Count); // 10000方法3使用线程安全集合(推荐)// 线程安全的集合类型 BlockingCollectionint blockingList new BlockingCollectionint(); ConcurrentBagint concurrentBag new ConcurrentBagint(); ConcurrentDictionarystring, int concurrentDict new ConcurrentDictionarystring, int(); ConcurrentQueueint concurrentQueue new ConcurrentQueueint(); ConcurrentStackint concurrentStack new ConcurrentStackint(); // 使用示例 ConcurrentBagint bag new ConcurrentBagint(); for (int i 0; i 10000; i) { Task.Run(() { bag.Add(i); // 线程安全无需加锁 }); } Thread.Sleep(5000); Console.WriteLine(bag.Count); // 100004.3 线程安全总结方法优点缺点推荐度加锁简单直接性能差本质上是把多线程退化成串行⭐分块执行性能好设计优雅需要设计分区逻辑⭐⭐⭐⭐⭐线程安全集合简单性能好需要替换数据结构⭐⭐⭐⭐⭐⚠️ 加锁虽然能解决问题但锁的范围内代码实际上是串行执行的等于抵消了多线程的并发优势只在没有更好方案时才考虑。五、中间变量问题5.1 问题现象// 错误示例所有线程输出的i都是5 for (int i 0; i 5; i) { Task.Run(() { Console.WriteLine($i {i}); // 输出5, 5, 5, 5, 5 }); }5.2 问题原因Task开启线程有延迟(虽然很短)开启多线程不会阻塞主线程循环很快当线程真正执行时循环已结束此时i已经变成55.3 解决方案// 正确示例使用中间变量 for (int i 0; i 5; i) { int k i; // 每次循环创建新的变量 Task.Run(() { Console.WriteLine($i {i}, k {k}); // i输出5k输出0,1,2,3,4 }); }原理每次循环创建新的变量k每个线程使用的是各自循环内的kk的值在创建时就已确定六、实战案例双色球开奖系统6.1 需求分析规则6个红球(01-33不重复)1个蓝球(01-16)点击开始所有球同时跳动变化点击停止显示最终结果技术要点7个球 7个线程每个线程独立运行相互不影响红球需要去重避免死锁6.2 核心代码private bool isRunning true; private ListTask taskList new ListTask(); private static object lockObj new object(); private void btnStart_Click(object sender, EventArgs e) { isRunning true; taskList.Clear(); // 遍历所有Label控件 foreach (var control in gboSSQ.Controls) { if (control is Label label) { taskList.Add(Task.Run(() { if (label.Name.Contains(Blue)) // 蓝球 { while (isRunning) { int index new RandomHelper().GetRandomNumberDelay(0, 16); string num blueNums[index]; this.Invoke(new Action(() { label.Text num; })); } } else // 红球 { while (isRunning) { int index new RandomHelper().GetRandomNumberDelay(0, 33); string num redNums[index]; lock (lockObj) { var currentNums GetCurrentRedNumbers(); if (!currentNums.Contains(num)) { this.Invoke(new Action(() { label.Text num; })); } } } } })); } } // 所有任务完成后的回调 Task.Factory.ContinueWhenAll(taskList.ToArray(), tasks { this.Invoke(new Action(() { ShowResult(); isRunning true; })); }); } private void btnStop_Click(object sender, EventArgs e) { isRunning false; // 不能在这里Wait会死锁 // Task.WaitAll(taskList.ToArray()); // 死锁 }6.3 关键技术点1. 随机数去重public class RandomHelper { public int GetRandomNumber(int min, int max) { Guid guid Guid.NewGuid(); int seed DateTime.Now.Millisecond; // 使用GUID增加随机性 foreach (char c in guid.ToString()) { seed GetSeedIncrement(c); } Random random new Random(seed); return random.Next(min, max); } }2. 跨线程更新UI// 子线程不能直接操作UI需要委托给主线程 this.Invoke(new Action(() { label.Text num; }));3. 避免死锁// 错误主线程等待子线程子线程需要主线程更新UI Task.WaitAll(taskList.ToArray()); // 死锁 // 正确使用回调 Task.Factory.ContinueWhenAll(taskList.ToArray(), tasks { // 不阻塞主线程 });七、小结Parallel控制线程数量避免泛滥异常处理使用 AggregateException必须 Wait 才能捕获线程取消CancellationTokenSource 统一管理线程安全优先使用分块或线程安全集合加锁是最后手段中间变量循环中开启线程时必须用临时变量捕获循环值死锁避免主线程和子线程相互等待