C#.Net-多线程-进阶篇-学习笔记
一、Parallel 并行编程
1.1 Parallel.Invoke
基本用法
csharp
// 一次执行多个委托
Parallel.Invoke(
() => { DoWork1(); },
() => { DoWork2(); },
() => { DoWork3(); }
);
特点
- 可以传入多个委托
- 会开启线程执行(可能是新线程,也可能是主线程参与)
- 会阻塞主线程,相当于主线程等待所有子线程执行结束
限制线程数量
csharp
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 3; // 最多开启3个线程
Parallel.Invoke(options,
() => { DoWork1(); },
() => { DoWork2(); },
() => { DoWork3(); },
() => { DoWork4(); },
() => { DoWork5(); }
);
不阻塞界面的方式
csharp
Task.Run(() =>
{
Parallel.Invoke(
() => { DoWork1(); },
() => { DoWork2(); },
() => { DoWork3(); }
);
});
1.2 Parallel.For
基本用法
csharp
// 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
基本用法
csharp
List<int> dataList = new List<int> { 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无法捕获多线程异常
csharp
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等待
csharp
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:多个任务的异常处理
csharp
try
{
List<Task> tasks = new List<Task>();
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 异常类型优先级
csharp
catch (AggregateException aex) // 具体异常类型,优先匹配
{
// 处理多线程异常
}
catch (Exception ex) // 抽象异常类型,其次匹配
{
// 处理其他异常
}
三、线程取消(CancellationToken)
3.1 线程取消原理
核心概念
- 线程无法从外部取消,只能自己取消自己
- 线程取消本质是向外抛出异常
- 使用CancellationTokenSource统一管理
3.2 标准的线程取消
基本用法
csharp
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 实际应用场景
场景:多个任务,一个失败全部取消
csharp
try
{
List<Task> tasks = new List<Task>();
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 什么是线程安全
定义
- 一段业务逻辑,单线程执行和多线程执行的结果完全一致,就是线程安全
- 否则就是线程不安全
线程不安全示例
csharp
List<int> list = new List<int>();
// 单线程:结果是10000
for (int i = 0; i < 10000; i++)
{
list.Add(i);
}
Console.WriteLine(list.Count); // 10000
// 多线程:结果不确定
List<int> list2 = new List<int>();
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:加锁(不推荐)
csharp
private readonly static object lockObj = new object();
List<int> list = new List<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (lockObj) // 加锁,独占资源
{
list.Add(i);
}
});
}
注意事项
- 锁对象定义:
private readonly static object lockObj = new object(); - 不要锁String类型
- 不要锁this
- 加锁会影响性能,相当于反多线程
方法2:分块/分区执行(推荐)
csharp
List<int> list1 = new List<int>();
List<int> list2 = new List<int>();
List<int> list3 = new List<int>();
int total = 10000;
int part1 = 3000;
int part2 = 6000;
int part3 = 10000;
List<Task> tasks = new List<Task>();
// 分三块,每块单线程执行(线程安全)
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:使用线程安全集合(推荐)
csharp
// 线程安全的集合类型
BlockingCollection<int> blockingList = new BlockingCollection<int>();
ConcurrentBag<int> concurrentBag = new ConcurrentBag<int>();
ConcurrentDictionary<string, int> concurrentDict = new ConcurrentDictionary<string, int>();
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
// 使用示例
ConcurrentBag<int> bag = new ConcurrentBag<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
bag.Add(i); // 线程安全,无需加锁
});
}
Thread.Sleep(5000);
Console.WriteLine(bag.Count); // 10000
4.3 线程安全总结
| 方法 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 加锁 | 简单直接 | 性能差,本质上是把多线程退化成串行 | ⭐ |
| 分块执行 | 性能好,设计优雅 | 需要设计分区逻辑 | ⭐⭐⭐⭐⭐ |
| 线程安全集合 | 简单,性能好 | 需要替换数据结构 | ⭐⭐⭐⭐⭐ |
⚠️ 加锁虽然能解决问题,但锁的范围内代码实际上是串行执行的,等于抵消了多线程的并发优势,只在没有更好方案时才考虑。
五、中间变量问题
5.1 问题现象
csharp
// 错误示例:所有线程输出的i都是5
for (int i = 0; i < 5; i++)
{
Task.Run(() =>
{
Console.WriteLine($"i = {i}"); // 输出:5, 5, 5, 5, 5
});
}
5.2 问题原因
- Task开启线程有延迟(虽然很短)
- 开启多线程不会阻塞主线程
- 循环很快,当线程真正执行时,循环已结束
- 此时i已经变成5
5.3 解决方案
csharp
// 正确示例:使用中间变量
for (int i = 0; i < 5; i++)
{
int k = i; // 每次循环创建新的变量
Task.Run(() =>
{
Console.WriteLine($"i = {i}, k = {k}");
// i输出5,k输出0,1,2,3,4
});
}
原理
- 每次循环创建新的变量k
- 每个线程使用的是各自循环内的k
- k的值在创建时就已确定
六、实战案例:双色球开奖系统
6.1 需求分析
规则
- 6个红球(01-33,不重复)
- 1个蓝球(01-16)
- 点击开始,所有球同时跳动变化
- 点击停止,显示最终结果
技术要点
- 7个球 = 7个线程
- 每个线程独立运行,相互不影响
- 红球需要去重
- 避免死锁
6.2 核心代码
csharp
private bool isRunning = true;
private List<Task> taskList = new List<Task>();
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. 随机数去重
csharp
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
csharp
// 子线程不能直接操作UI,需要委托给主线程
this.Invoke(new Action(() =>
{
label.Text = num;
}));
3. 避免死锁
csharp
// 错误:主线程等待子线程,子线程需要主线程更新UI
Task.WaitAll(taskList.ToArray()); // 死锁!
// 正确:使用回调
Task.Factory.ContinueWhenAll(taskList.ToArray(), tasks =>
{
// 不阻塞主线程
});
七、小结
- Parallel:控制线程数量,避免泛滥
- 异常处理:使用 AggregateException,必须 Wait 才能捕获
- 线程取消:CancellationTokenSource 统一管理
- 线程安全:优先使用分块或线程安全集合,加锁是最后手段
- 中间变量:循环中开启线程时,必须用临时变量捕获循环值
- 死锁:避免主线程和子线程相互等待