09-C#.Net-多线程-基础篇-学习笔记
一、核心概念理解
1.1 进程与线程
进程(Process)
- 计算机中的虚拟记录,描述应用程序运行时消耗的各种资源集合
- 包含:CPU + 内存 + 磁盘IO + 网络资源
- 类比:一个公司的整体运作
线程(Thread)
- 程序进程中执行具体动作的最小执行流
- 从点击按钮到网络通信,都是线程在执行
- 类比:公司中的具体员工
- 特点:线程依附于进程存在,进程消失则线程也消失
句柄(Handle)
- long类型的数字,对应程序中的某个小部件
- 是操作程序的最小单位标识
1.2 多线程的原理
CPU与多线程
- 例如:6核12线程的CPU
- 通过逻辑切分,将每个核心的执行时间分片
- 每毫秒可执行的动作被切分成10000份
执行特点
- 宏观:多个动作在一段时间内看似同时执行完毕(并发)
- 微观:同一时刻只能处理一件事(串行)
- 操作系统不断调度,动作A开始用某一分片,结束时可能用另一分片
二、同步与异步
2.1 同步方法(Sync)
特点
private void DoSomethingSync()
{
// 线性执行,从上往下依次执行
Console.WriteLine("步骤1");
Thread.Sleep(1000);
Console.WriteLine("步骤2");
Thread.Sleep(1000);
Console.WriteLine("步骤3");
}
优点
- 符合人类思维逻辑,代码易读
- 有序执行,结果可预测
- 消耗计算机资源少
缺点
- 执行慢,会卡顿界面
- 只有一个线程(主线程/UI线程)参与计算
- CPU忙碌时无暇处理其他任务
生活类比
- 真心请人吃饭:Richard请Vn吃饭,Vn说还要忙一会儿,Richard说"那我等你,忙完一起去"
2.2 异步方法(Async)
特点
private void DoSomethingAsync()
{
// 开启新线程执行
Task.Run(() =>
{
Console.WriteLine("步骤1");
Thread.Sleep(1000);
});
Task.Run(() =>
{
Console.WriteLine("步骤2");
Thread.Sleep(1000);
});
// 主线程不等待,继续执行
Console.WriteLine("主线程继续");
}
优点
- 执行快,不卡顿界面
- 多个线程并发执行
- 提高程序响应能力
缺点
- 无序执行,结果不可预测
- 消耗更多计算机资源
- 编程复杂度增加
生活类比
- 客气请人吃饭:Richard客气地请Vn吃饭,Vn说还要忙一会儿,Richard说"那你先忙,我先去吃了"
核心理念
- 以资源换时间:使用大量资源,降低时间成本
三、C#中的多线程实现
3.1 Thread(最原始)
Thread thread = new Thread(() =>
{
Console.WriteLine("Thread执行");
});
thread.Start();
thread.IsBackground = true; // 后台线程:进程结束,线程随之消失
thread.IsBackground = false; // 前台线程:进程结束,线程执行完才消失
3.2 ThreadPool(线程池)
- 管理一组可重用的线程
- 避免频繁创建和销毁线程的开销
// 将工作项加入线程池队列
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine($"ThreadPool执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
});
// 带参数版本
ThreadPool.QueueUserWorkItem(state =>
{
string msg = (string)state;
Console.WriteLine($"参数: {msg}");
}, "hello");
⚠️ ThreadPool 无法精确控制线程,也不支持返回值和取消,实际开发中优先使用 Task。
3.3 Task(推荐使用)
Task的优势
- .NET Framework 3.0时代实现
- C#中多线程操作的最佳实现
- 丰富的API,满足各种开发场景
- Task操作的线程来自线程池
Task开启方式
// 方式1:new Task + Start
Task task = new Task(() =>
{
Console.WriteLine("Task执行");
});
task.Start();
// 方式2:Task.Run(推荐)
Task.Run(() =>
{
Console.WriteLine("Task执行");
});
// 方式3:TaskFactory
TaskFactory factory = new TaskFactory();
factory.StartNew(() =>
{
Console.WriteLine("Task执行");
});
// 方式4:带返回值
Task<int> task = Task.Run(() =>
{
return DateTime.Now.Year;
});
线程标识
// 通过线程ID确定是否为同一线程
int threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"当前线程ID: {threadId:00}");
四、Task的核心方法
4.1 等待方法
Wait() - 单个任务等待
Task task = Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("任务完成");
});
task.Wait(); // 主线程等待,会卡顿界面
task.Wait(1000); // 限时等待,过时不候
task.Wait(TimeSpan.FromMilliseconds(1100)); // 使用TimeSpan
WaitAll() - 等待所有任务
List<Task> tasks = new List<Task>();
tasks.Add(Task.Run(() => DoWork1()));
tasks.Add(Task.Run(() => DoWork2()));
tasks.Add(Task.Run(() => DoWork3()));
Task.WaitAll(tasks.ToArray()); // 等待所有任务完成
WaitAny() - 等待任意一个任务
Task.WaitAny(tasks.ToArray()); // 任意一个任务完成即返回
4.2 回调方法(不阻塞)
ContinueWith() - 单个任务回调
Task task = Task.Run(() =>
{
Thread.Sleep(3000);
return "结果";
});
task.ContinueWith(t =>
{
Console.WriteLine($"任务完成,结果:{t.Result}");
});
ContinueWhenAny() - 任意一个完成时回调
TaskFactory factory = new TaskFactory();
factory.ContinueWhenAny(tasks.ToArray(), completedTask =>
{
Console.WriteLine("有一个任务完成了");
// 不卡顿界面,用户体验好
});
ContinueWhenAll() - 所有完成时回调
factory.ContinueWhenAll(tasks.ToArray(), completedTasks =>
{
Console.WriteLine("所有任务都完成了");
});
4.3 Delay() - 延迟执行
// Thread.Sleep - 阻塞主线程
Thread.Sleep(3000); // 主线程卡顿3秒
// Task.Delay - 不阻塞,配合ContinueWith使用
Task.Delay(3000).ContinueWith(t =>
{
Console.WriteLine("3秒后执行");
// 可能是新线程,也可能是主线程执行
});
五、应用场景
5.1 适合使用多线程的场景
场景1:多个独立任务并发执行
// 同时查询接口、缓存、数据库
List<Task> tasks = new List<Task>();
tasks.Add(Task.Run(() => QueryFromApi()));
tasks.Add(Task.Run(() => QueryFromCache()));
tasks.Add(Task.Run(() => QueryFromDatabase()));
Task.WaitAll(tasks.ToArray());
场景2:项目实战 - 多人协作开发
// 每个开发人员对应一个线程
TaskFactory factory = new TaskFactory();
List<Task> devTasks = new List<Task>();
devTasks.Add(factory.StartNew(() => Coding("张三", "权限模块")));
devTasks.Add(factory.StartNew(() => Coding("李四", "支付模块")));
devTasks.Add(factory.StartNew(() => Coding("王五", "订单模块")));
// 任意一人完成,准备环境
factory.ContinueWhenAny(devTasks.ToArray(), t =>
{
Console.WriteLine("有人完成了,开始准备环境");
});
// 所有人完成,庆祝
factory.ContinueWhenAll(devTasks.ToArray(), t =>
{
Console.WriteLine("所有人完成,一起吃饭庆祝");
});
5.2 实际业务场景
场景1:数据查询优先级
- 同时查询缓存、数据库、第三方接口
- 使用WaitAny,哪个先返回数据就用哪个
- 其他线程自动放弃
场景2:首页数据加载
- 首页包含:考勤信息、年度Top10、季度Top10、公告、通知
- 每个模块开启一个线程并发查询
- 使用WaitAll等待所有数据,组装后返回前端
- 提高查询性能
5.3 不适合使用多线程的场景
- 单个数据库查询(只有一个动作)
- 简单的顺序操作
- 需要严格顺序的业务逻辑
六、父子线程
Task parentTask = new Task(() =>
{
Console.WriteLine("父线程开始");
Task childTask1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("子线程1");
});
Task childTask2 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("子线程2");
});
childTask1.Start();
childTask2.Start();
// 如果不等待子线程,父线程可能先结束
// childTask1.Wait();
// childTask2.Wait();
Console.WriteLine("父线程结束");
});
parentTask.Start();
parentTask.Wait(); // 主线程等待父线程
注意事项
- 父线程等待时,子线程可能还未执行完
- 需要在父线程中显式等待子线程
- 否则父线程结束,子线程内容可能未执行完
七、实用技巧
7.1 线程ID格式化
// 格式化为两位数,方便查看
string threadId = Thread.CurrentThread.ManagedThreadId.ToString("00");
Console.WriteLine($"线程ID: {threadId}");
7.2 时间戳格式化
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
Console.WriteLine($"时间: {timestamp}");
7.3 性能测试
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
// 执行任务
DoSomething();
stopwatch.Stop();
Console.WriteLine($"耗时: {stopwatch.ElapsedMilliseconds}ms");
八、小结
- 同步方法:有序、慢、阻塞、资源消耗少
- 异步方法:无序、快、不阻塞、资源消耗多
- Task 是首选:功能强大、API 丰富、来自线程池
- Wait 系列:阻塞主线程,会卡顿界面
- Continue 系列:不阻塞,用户体验好
- 合理使用多线程:并发场景才使用,不要滥用
本文来自博客园,作者:龙猫•ᴥ•,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/19740022

浙公网安备 33010602011771号