编辑
2023-06-27
编程语言
00

目录

一、什么是进程,什么是线程
1. 进程
2. 线程
3. 两者的区别
4. 结合实际场景进行理解
二、多线程
多线程在C#中的应用
ThreadPool
Task
三、前后台线程
1. 前台线程
2. 后台线程
四、锁和死锁
1. 锁
定义
为什么需要锁?
C#常见锁机制
2. 死锁
定义
死锁发生的四个必要条件
死锁示例
3. 锁与死锁的关系
4. 如何避免死锁?

一、什么是进程,什么是线程

1. 进程

  • 进程(Process)是程序运行时的最外层容器,代表一个独立运行的应用程序实例
  • 操作系统为每个进程分配独立的内存空间和系统资源
  • 进程之间相互隔离,互不干扰。

例子:打开 Unity,即启动了一个 Unity 编辑器进程。


2. 线程

  • 线程(Thread)是进程内的执行单位,代表程序中的一条执行路径;
  • 同一个进程可以包含多个线程,线程共享进程的内存和资源
  • 多线程可以提高程序的并发性与响应能力。

例子:在 Unity 中同时执行加载资源、UI绘制、脚本运行,这些操作可能运行在不同线程中。


3. 两者的区别

  • 线程必须依附在进程中,不能单独存在;
  • 每个进程至少有一个主线程;
  • 一个进程可包含多个线程协同完成任务;
  • 进程是容器,线程是执行者。
比较项进程(Process)线程(Thread)
概念应用程序的运行实例应用中执行任务的基本单位
内存空间独立,不与其他进程共享共享所属进程的内存空间
通信成本高,需特殊机制(如管道、套接字等)低,可直接读写共享内存
创建/销毁开销大,需分配/释放大量资源小,仅管理执行栈和上下文
崩溃影响不影响其他进程可能导致整个进程崩溃
示例(Unity)Unity 编辑器、游戏运行实例资源加载线程、主线程、后台处理线程等

4. 结合实际场景进行理解

当我打开百度网盘时,操作系统为它创建了一个进程,分配独立的资源与内存。此时,我在百度网盘中一边下载文件,一边播放正在下载的视频,这两个任务实际上是由该进程内部的多个线程协同完成的。一个进程中可以包含多个线程,而线程必须依附于进程才能存在。进程是操作系统中用于资源管理的基本单位,而线程是任务执行和调度的最小单位,真正承担着程序的运行工作。


二、多线程

在刚刚已经提到了,一个进程中可以包含多个线程,那么多线程就是指在一个进程中同时运行多个线程,使程序能并发执行多个任务 ,通常适合处理一些复杂耗时的逻辑,比如加载文件,网络通信等。


多线程在C#中的应用

csharp
using System.Threading;//引用多线程命名空间 void Run() { Console.WriteLine("运行在线程中"); } //声明创建一个线程 Thread thread = new Thread(Run); //线程执行 thread.Start(); //设置为后台线程 newThread.IsBackground = true; //线程休眠 Thread.Sleep(1000); //关闭和释放一个线程 newThread.Abort(); newThread = null;

以上的举例只是通过使用C#中的Theard类来使用多线程,这是最基础也是最接近底层的方式,但是需要我们手动维护(创建,释放,调度,控制生命周期等),用起来还是非常繁琐和复杂的,可以使用别的方式来使用多线程,比如:

ThreadPool

csharp
using System.Threading; ThreadPool.QueueUserWorkItem(state => { Console.WriteLine("线程池中的线程"); });
  1. 系统自动管理线程复用,适合短时间小任务;
  2. 不支持取消、延迟、返回值、无法手动控制线程生命周期、不适合长期任务

Task

csharp
using System.Threading.Tasks; Task.Run(() => { Console.WriteLine("使用 Task 执行多线程"); });
  1. 使用简单,可接收返回值,内部维护,系统自动调度
  2. 支持多线程并发任务执行,并结合 async/await 实现异步处理;

在游戏客户端开发中,实际上前端大多数场景是用不上Task以及多线程的,为什么呢?

1. Unity设计本身就是一个以主线程为核心架构的引擎,其绝大多数 API 都必须在主线程调用(运行在多线程中会直接报错)。Task 和 ThreadPool 是 .NET 提供的通用异步/并发工具,但在 Unity 客户端中并不适用于日常逻辑,甚至可能导致严重的线程安全问题,以及产生大量GC。

2. 相比之下,UniTask 是专门为 Unity 场景设计的异步控制方案,零GC、高性能、安全且可读性强,是当前 Unity 异步开发的首选工具。除非你在做复杂的逻辑计算或后台处理(并且确保不会访问 Unity API),否则 Unity 客户端开发中几乎无需主动使用 Task 或 ThreadPool。


三、前后台线程

1. 前台线程

默认创建的线程就是前台线程,只要有一个前台线程在运行,进程就不会退出,**程序必须等所有前台线程执行完毕后,才会关闭**,适合处理一些关键任务(游戏主线程、渲染、下载)

2. 后台线程

需要我们手动设置`thread.IsBackground = true;`,不会阻止进程退出,一旦所有前台线程结束,即使后台线程还在执行,进程也会被强制终止,也就是说,**后台进程会随着所有前台进程的结束或整个进程的结束而随时被终止,不会等待其执行完成**,适合处理一些后台日志、定时检测等非必须完成的任务

四、锁和死锁

1. 锁

定义

锁是一种线程同步机制,用于防止多个线程同时访问共享资源,从而导致数据错误或程序崩溃。

为什么需要锁?

在多线程并发环境中,如果多个线程同时读写同一变量或集合,容易出现:

  • 数据竞争(Race Condition)
  • 状态不一致(脏数据)
  • 程序崩溃(并发修改集合)

锁的作用是让每次只有一个线程可以进入临界区,从而确保线程安全。

C#常见锁机制

锁机制用法示例特点
locklock(obj) { /* 访问共享资源 */ }最常用,简单易懂,自动释放锁
MonitorMonitor.Enter/Exit提供更细粒度控制,如超时尝试
Mutex支持跨进程锁定成本较高,不适合高频使用
SpinLock忙等锁,占 CPU极短任务,高性能场景使用
ReaderWriterLockSlim支持并发读、独占写读多写少场景
csharp
private object _lockObj = new object(); void SafeAdd(List<int> list, int value) { lock (_lockObj) { list.Add(value); } }

2. 死锁

定义

死锁是指两个或多个线程互相等待对方释放资源,造成永久阻塞,程序无法继续执行。

死锁发生的四个必要条件

  1. 互斥:资源不能被多个线程同时占用。
  2. 占有且等待:线程持有资源的同时申请其他资源。
  3. 不可抢占:资源不能被强制回收,只能线程自己释放。
  4. 循环等待:多个线程之间形成资源的循环等待链。

⚠ 这四个条件同时成立时,就可能导致死锁。

死锁示例

csharp
object lockA = new object(); object lockB = new object(); void Thread1() { lock (lockA) { Thread.Sleep(100); lock (lockB) { Console.WriteLine("线程1获得A和B"); } } } void Thread2() { lock (lockB) { Thread.Sleep(100); lock (lockA) { Console.WriteLine("线程2获得B和A"); } } }
  • 两个线程互相持有对方需要的锁 → 死锁

3. 锁与死锁的关系

  • 锁是为了解决线程安全问题的工具
  • 但死锁恰恰是锁使用不当造成的副作用
  • 也就是说:加锁≠防止死锁,不当加锁 = 死锁根源

类比说明:

锁就像红绿灯,控制线程(车辆)通行,防止撞车; 但如果每个路口都卡着一辆车互不相让,就会交通瘫痪——这就是死锁。


4. 如何避免死锁?

策略描述
统一加锁顺序所有线程按相同顺序申请资源
尽量避免嵌套锁不在一个锁里再加另一个锁
使用 TryEnter 超时机制超时退出锁等待
缩小锁的作用范围减少锁粒度、锁时长
使用无锁数据结构ConcurrentQueueInterlocked

本文作者:xuxuxuJS

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.14.8