10-C#

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 问题原因

  1. Task开启线程有延迟(虽然很短)
  2. 开启多线程不会阻塞主线程
  3. 循环很快,当线程真正执行时,循环已结束
  4. 此时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 统一管理
  • 线程安全:优先使用分块或线程安全集合,加锁是最后手段
  • 中间变量:循环中开启线程时,必须用临时变量捕获循环值
  • 死锁:避免主线程和子线程相互等待
相关推荐
似水明俊德2 小时前
14-C#
开发语言·c#
勇敢牛牛_2 小时前
【aiway】基于 Rust 开发的 API + AI 网关
开发语言·后端·网关·ai·rust
khddvbe2 小时前
C++中的代理模式实战
开发语言·c++·算法
计算机安禾2 小时前
【C语言程序设计】第31篇:指针与函数
c语言·开发语言·数据结构·c++·算法·leetcode·visual studio
kaikaile19952 小时前
庞加莱截面计算MATLAB程序
开发语言·matlab
ECT-OS-JiuHuaShan2 小时前
朱梁万有递归元定理,解构西方文明中心论幻觉
开发语言·人工智能·php
Aubrey-J2 小时前
练习开发Skill——网页内容抓取Skill(website-content-fetch)
开发语言·人工智能
handler012 小时前
基础算法:分治
c语言·开发语言·c++·笔记·学习·算法·深度优先
2501_924952693 小时前
设计模式在C++中的实现
开发语言·c++·算法