例子:打开 Unity,即启动了一个 Unity 编辑器进程。
例子:在 Unity 中同时执行加载资源、UI绘制、脚本运行,这些操作可能运行在不同线程中。
比较项 | 进程(Process) | 线程(Thread) |
---|---|---|
概念 | 应用程序的运行实例 | 应用中执行任务的基本单位 |
内存空间 | 独立,不与其他进程共享 | 共享所属进程的内存空间 |
通信成本 | 高,需特殊机制(如管道、套接字等) | 低,可直接读写共享内存 |
创建/销毁开销 | 大,需分配/释放大量资源 | 小,仅管理执行栈和上下文 |
崩溃影响 | 不影响其他进程 | 可能导致整个进程崩溃 |
示例(Unity) | Unity 编辑器、游戏运行实例 | 资源加载线程、主线程、后台处理线程等 |
当我打开百度网盘时,操作系统为它创建了一个进程,分配独立的资源与内存。此时,我在百度网盘中一边下载文件,一边播放正在下载的视频,这两个任务实际上是由该进程内部的多个线程协同完成的。一个进程中可以包含多个线程,而线程必须依附于进程才能存在。进程是操作系统中用于资源管理的基本单位,而线程是任务执行和调度的最小单位,真正承担着程序的运行工作。
在刚刚已经提到了,一个进程中可以包含多个线程,那么多线程就是指在一个进程中同时运行多个线程,使程序能并发执行多个任务 ,通常适合处理一些复杂耗时的逻辑,比如加载文件,网络通信等。
csharpusing 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类来使用多线程,这是最基础也是最接近底层的方式,但是需要我们手动维护(创建,释放,调度,控制生命周期等),用起来还是非常繁琐和复杂的,可以使用别的方式来使用多线程,比如:
csharpusing System.Threading;
ThreadPool.QueueUserWorkItem(state => {
Console.WriteLine("线程池中的线程");
});
csharpusing System.Threading.Tasks;
Task.Run(() => {
Console.WriteLine("使用 Task 执行多线程");
});
在游戏客户端开发中,实际上前端大多数场景是用不上Task以及多线程的,为什么呢?
1. Unity设计本身就是一个以主线程为核心架构的引擎,其绝大多数 API 都必须在主线程调用(运行在多线程中会直接报错)。Task 和 ThreadPool 是 .NET 提供的通用异步/并发工具,但在 Unity 客户端中并不适用于日常逻辑,甚至可能导致严重的线程安全问题,以及产生大量GC。
2. 相比之下,UniTask 是专门为 Unity 场景设计的异步控制方案,零GC、高性能、安全且可读性强,是当前 Unity 异步开发的首选工具。除非你在做复杂的逻辑计算或后台处理(并且确保不会访问 Unity API),否则 Unity 客户端开发中几乎无需主动使用 Task 或 ThreadPool。
默认创建的线程就是前台线程,只要有一个前台线程在运行,进程就不会退出,**程序必须等所有前台线程执行完毕后,才会关闭**,适合处理一些关键任务(游戏主线程、渲染、下载)
需要我们手动设置`thread.IsBackground = true;`,不会阻止进程退出,一旦所有前台线程结束,即使后台线程还在执行,进程也会被强制终止,也就是说,**后台进程会随着所有前台进程的结束或整个进程的结束而随时被终止,不会等待其执行完成**,适合处理一些后台日志、定时检测等非必须完成的任务
锁是一种线程同步机制,用于防止多个线程同时访问共享资源,从而导致数据错误或程序崩溃。
在多线程并发环境中,如果多个线程同时读写同一变量或集合,容易出现:
锁的作用是让每次只有一个线程可以进入临界区,从而确保线程安全。
锁机制 | 用法示例 | 特点 |
---|---|---|
lock | lock(obj) { /* 访问共享资源 */ } | 最常用,简单易懂,自动释放锁 |
Monitor | Monitor.Enter/Exit | 提供更细粒度控制,如超时尝试 |
Mutex | 支持跨进程锁定 | 成本较高,不适合高频使用 |
SpinLock | 忙等锁,占 CPU | 极短任务,高性能场景使用 |
ReaderWriterLockSlim | 支持并发读、独占写 | 读多写少场景 |
csharpprivate object _lockObj = new object();
void SafeAdd(List<int> list, int value)
{
lock (_lockObj)
{
list.Add(value);
}
}
死锁是指两个或多个线程互相等待对方释放资源,造成永久阻塞,程序无法继续执行。
⚠ 这四个条件同时成立时,就可能导致死锁。
csharpobject 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");
}
}
}
类比说明:
锁就像红绿灯,控制线程(车辆)通行,防止撞车; 但如果每个路口都卡着一辆车互不相让,就会交通瘫痪——这就是死锁。
策略 | 描述 |
---|---|
✅ 统一加锁顺序 | 所有线程按相同顺序申请资源 |
✅ 尽量避免嵌套锁 | 不在一个锁里再加另一个锁 |
✅ 使用 TryEnter 超时机制 | 超时退出锁等待 |
✅ 缩小锁的作用范围 | 减少锁粒度、锁时长 |
✅ 使用无锁数据结构 | 如 ConcurrentQueue 、Interlocked |
本文作者:xuxuxuJS
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
预览: