课程
bilibili Threading in C#


一 概念
1.


为什么出现先打印主线程再打印新线程的情况吗?线程顺序是怎样的?包含主线程和非主线程的区分和线程执行安排(时间,资源,冲突等等)
一、主线程与非主线程的区分
-
主线程(Main Thread):
-
是 Unity 程序的 "主执行线程",负责处理生命周期函数(
Start、Update等)、渲染、UI 交互、物理计算等核心逻辑。 -
你的代码中,
Start方法和直接调用的ThreadMethod1(2)都运行在主线程。
-
-
非主线程(子线程):
-
通过
new Thread(...)创建的是独立子线程,与主线程并行执行,不能直接操作 Unity 引擎 API (如Debug.Log实际会被 Unity 转发到主线程处理,但本身线程是独立的)。 -
代码中
new Thread(()=>ThreadMethod1(1)).Start()启动的是子线程。
-
二、线程执行顺序的本质:并发与调度
线程的执行顺序不固定 ,由操作系统的线程调度器决定,核心原因:
-
线程启动有开销 :创建子线程需要分配资源、初始化上下文,这个过程需要时间。而主线程中
ThreadMethod1(2)是直接调用,几乎没有延迟,因此大概率先执行。 -
调度器的不确定性:操作系统调度器根据线程优先级、CPU 核心负载等动态分配执行时间片(毫秒级)。即使子线程启动很快,也可能因为调度器先给主线程分配时间片而晚执行。
例如:
-
可能先打印
arg=2(主线程),再打印arg=1(子线程); -
极端情况下(如子线程启动极快且调度器优先调度),也可能先打印
arg=1。
-
三、线程的时间、资源与冲突问题
-
时间安排:
-
线程是 "并发执行" 的,即宏观上同时运行,微观上 CPU 通过时间片轮转切换执行(单核)或多核并行(多核 CPU)。
-
主线程和子线程的执行进度完全独立,没有固定的先后顺序,除非通过同步机制(如
lock、ManualResetEvent)强制控制。
-
-
资源竞争:
-
若多个线程(包括主线程和子线程)访问共享资源(如全局变量、类成员变量),可能导致数据不一致。例如:
csprivate int count = 0; public void ThreadMethod1(int arg) { for (int i = 0; i < 1000; i++) { count++; // 多线程同时修改count,可能导致结果错误 } } -
解决方式:用
lock锁定共享资源,保证同一时间只有一个线程访问:csprivate object lockObj = new object(); public void ThreadMethod1(int arg) { lock (lockObj) { // 加锁,避免资源竞争 for (int i = 0; i < 1000; i++) { count++; } } }
-
-
Unity 中的线程限制:
-
子线程不能直接调用 Unity 引擎 API (如
Instantiate、transform.position、Debug.Log虽然能运行但内部会被转发,不建议在子线程频繁使用)。 -
若子线程需要修改游戏对象状态,需通过 "主线程委托队列" 间接执行(例如用
UnityWebRequest的回调、或自定义Update中执行的队列)。
-
函数和线程有啥区别
定义与概念
- 函数:是一段具有特定功能、可以被重复调用的代码块。它是为了将复杂的程序逻辑进行模块化拆分,方便代码的复用、维护和阅读。在编程语言中,通过函数名和参数列表来调用函数,函数执行完成后将结果返回给调用者 。例如在 C 语言中:
cs
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 5);
return 0;
}
这里add函数实现了两个整数相加的功能,在main函数中调用add函数并获取计算结果。
- 线程:是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源,比如内存空间、文件句柄等。线程可以并发或并行执行,从而提高程序的执行效率。例如在 Java 中创建一个简单的线程:
java
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程正在执行");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
通过创建Thread类的子类并重写run方法,定义线程执行的逻辑,然后调用start方法启动线程。
执行方式
- 函数:函数是顺序执行的,当在一个函数中调用另一个函数时,程序会暂停当前函数的执行,转而执行被调用的函数,直到被调用函数执行完毕并返回结果,才会继续执行当前函数后续的代码。例如:
python
def func1():
print("func1开始执行")
func2()
print("func1继续执行")
def func2():
print("func2执行")
func1()
输出结果是func1开始执行 -> func2执行 -> func1继续执行 ,体现了函数的顺序执行特性。
- 线程:线程是并发或并行执行的。多个线程可以同时运行(在多核处理器上实现并行,在单核处理器上通过时间片轮转实现并发)。例如在多线程程序中,一个线程在进行网络请求等待响应时,其他线程可以继续执行其他任务,而不需要等待该线程完成网络请求,从而提高了程序整体的执行效率。
资源占用
-
函数:函数本身不单独占用系统资源(除了运行时占用的栈空间等少量资源),它在调用时使用调用者所在的上下文环境,比如在一个函数中访问的变量,默认是调用该函数的作用域中的变量 。
-
线程:线程需要占用一定的系统资源,包括线程栈(用于存储局部变量、函数调用信息等)、程序计数器(记录线程执行的位置)等。多个线程共享所属进程的内存空间等资源,这也带来了资源竞争和同步的问题,需要额外的机制(如锁)来处理。
生命周期
-
函数:函数的生命周期从被调用开始,到执行完毕返回结果结束。每次调用函数时,都会创建新的栈帧(包含局部变量等信息),函数返回后栈帧被销毁。
-
线程 :线程的生命周期相对复杂,包括创建、就绪、运行、阻塞、终止等状态。线程可以通过特定的方法(如 Java 中的
start方法)启动,在执行过程中可能因为等待资源等原因进入阻塞状态,当满足条件后又回到就绪或运行状态,最后执行完毕或被终止。
相互关系
函数可以作为线程执行的代码载体,通常在线程启动时,会指定一个函数(在 Java 中是run方法 ,在 C++ 中可以是自定义的函数对象等)作为线程执行的逻辑。例如在 C++ 中使用标准库创建线程:
cpp
#include <iostream>
#include <thread>
void threadFunction() {
std::cout << "线程正在执行" << std::endl;
}
int main() {
std::thread myThread(threadFunction);
myThread.join();
return 0;
}






2. CLR
CLR(Common Language Runtime,公共语言运行时)是微软 .NET Framework 的核心组件,它提供了一个托管代码执行环境,负责管理 .NET 应用程序的运行。你可以把它理解为 .NET 程序的"操作系统"或"虚拟机"。
| 功能类别 | 核心作用 | 具体表现或优势 |
|---|---|---|
| 代码管理 | 负责 .NET 代码的整个执行过程 | 将源代码编译为中间语言(IL),运行时通过即时编译(JIT)生成特定平台的本地机器码并执行 |
| 内存管理 | 自动处理内存的分配和释放 | 通过垃圾回收器(GC)自动回收不再使用的对象内存,有效避免内存泄漏和悬挂指针等问题 |
| 类型安全 | 确保代码的类型安全 | 通过公共类型系统(CTS)定义标准数据类型,并在加载和执行代码时进行类型安全检查,保证不同语言编写的对象可以正确交互 |
| 安全性 | 提供代码执行的安全环境 | 通过代码访问安全性(CAS)等技术控制代码的执行权限,根据代码来源和签名等方式确保安全 |
| 跨语言集成 | 支持多种编程语言在 .NET 平台上的集成和互操作 | 使不同语言(如 C#、VB.NET、F#)编写的组件可以无缝通信和协作 |
| 线程与异常管理 | 提供统一的异常处理模型和支持多线程编程 | 自动处理线程同步和资源竞争问题,并提供 try-catch-finally等结构化异常处理机制 |
工作原理
CLR 的工作流程可以概括为以下几个关键步骤
-
编译 :你用 C#、VB.NET 等语言编写的源代码,首先会被编译器编译成中间语言(IL) 和元数据(Metadata) ,并打包成程序集(.dll 或 .exe 文件)。
-
加载:当运行程序时,CLR 会将这些程序集加载到内存中。
-
验证:CLR 会对 IL 代码进行验证,确保其类型安全且符合规范。
-
JIT 编译 :当需要执行某段代码(如方法)时,CLR 的即时编译器(JIT Compiler) 会将其编译成本地机器的原生代码。
-
执行:编译后的原生代码被 CPU 执行,在此过程中,CLR 会持续管理内存、安全、异常处理等。
-
垃圾回收:CLR 的垃圾回收器会周期性地自动回收不再使用的内存。
二 原理

Unity线程
一 UniTask
1. 官方说明文档
GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.
cs
// extension awaiter/methods can be used by this namespace
using Cysharp.Threading.Tasks;
// You can return type as struct UniTask<T>(or UniTask), it is unity specialized lightweight alternative of Task<T>
// zero allocation and fast excution for zero overhead async/await integrate with Unity
async UniTask<string> DemoAsync()
{
// You can await Unity's AsyncObject
var asset = await Resources.LoadAsync<TextAsset>("foo");
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
await SceneManager.LoadSceneAsync("scene2");
// .WithCancellation enables Cancel, GetCancellationTokenOnDestroy synchornizes with lifetime of GameObject
// after Unity 2022.2, you can use `destroyCancellationToken` in MonoBehaviour
var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());
// .ToUniTask accepts progress callback(and all options), Progress.Create is a lightweight alternative of IProgress<T>
var asset3 = await Resources.LoadAsync<TextAsset>("baz").ToUniTask(Progress.Create<float>(x => Debug.Log(x)));
// await frame-based operation like a coroutine
await UniTask.DelayFrame(100);
// replacement of yield return new WaitForSeconds/WaitForSecondsRealtime
await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
// yield any playerloop timing(PreUpdate, Update, LateUpdate, etc...)
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
// replacement of yield return null
await UniTask.Yield();
await UniTask.NextFrame();
// replacement of WaitForEndOfFrame
#if UNITY_2023_1_OR_NEWER
await UniTask.WaitForEndOfFrame();
#else
// requires MonoBehaviour(CoroutineRunner))
await UniTask.WaitForEndOfFrame(this); // this is MonoBehaviour
#endif
// replacement of yield return new WaitForFixedUpdate(same as UniTask.Yield(PlayerLoopTiming.FixedUpdate))
await UniTask.WaitForFixedUpdate();
// replacement of yield return WaitUntil
await UniTask.WaitUntil(() => isActive == false);
// special helper of WaitUntil
await UniTask.WaitUntilValueChanged(this, x => x.isActive);
// You can await IEnumerator coroutines
await FooCoroutineEnumerator();
// You can await a standard task
await Task.Run(() => 100);
// Multithreading, run on ThreadPool under this code
await UniTask.SwitchToThreadPool();
/* work on ThreadPool */
// return to MainThread(same as `ObserveOnMainThread` in UniRx)
await UniTask.SwitchToMainThread();
// get async webrequest
async UniTask<string> GetTextAsync(UnityWebRequest req)
{
var op = await req.SendWebRequest();
return op.downloadHandler.text;
}
var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));
// concurrent async-wait and get results easily by tuple syntax
var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
// shorthand of WhenAll, tuple can await directly
var (google2, bing2, yahoo2) = await (task1, task2, task3);
// return async-value.(or you can use `UniTask`(no result), `UniTaskVoid`(fire and forget)).
return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}
2. 方法
2. 1 基础概念与优势
-
async UniTask<string>:UniTask 是 struct 类型(值类型),相比Task更轻量,几乎无内存分配,适合 Unity 性能敏感场景。返回值支持泛型(这里是string),提供类型安全。 -
核心目标:替代 Unity 传统协程(
IEnumerator)和 .NETTask,实现零开销的异步编程。
2. 与 Unity 原生异步对象的集成
cs
// 等待资源加载完成
var asset = await Resources.LoadAsync<TextAsset>("foo");
// 等待网络请求完成
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
// 等待场景加载完成
await SceneManager.LoadSceneAsync("scene2");
-
UniTask 可以直接
awaitUnity 原生的异步操作(如Resources.LoadAsync、UnityWebRequest、SceneManager.LoadSceneAsync),无需手动封装协程。 -
相比传统协程的
yield return,代码更线性,可读性更高。
3. 生命周期绑定与取消操作
cs
// 将异步操作与当前组件生命周期绑定,组件销毁时自动取消任务
var asset2 = await Resources.LoadAsync<TextAsset>("bar")
.WithCancellation(this.GetCancellationTokenOnDestroy());
-
GetCancellationTokenOnDestroy():获取与当前MonoBehaviour绑定的取消令牌,当组件被销毁时,自动取消异步操作,避免空引用错误。 -
WithCancellation():为异步操作添加取消支持,增强代码安全性。
4. 进度回调
cs
// 监听异步操作的进度(如资源加载进度)
var asset3 = await Resources.LoadAsync<TextAsset>("baz")
.ToUniTask(Progress.Create<float>(x => Debug.Log(x)));
-
ToUniTask():将 Unity 原生异步操作转换为 UniTask,并支持进度回调。 -
Progress.Create<float>():轻量级进度接口,替代 .NET 原生的IProgress<T>,减少内存分配。
5. 帧循环与时间相关的等待
cs
// 等待指定帧数(替代 yield return new WaitForEndOfFrame + 计数)
await UniTask.DelayFrame(100);
// 等待指定时间(替代 WaitForSeconds,支持忽略时间缩放)
await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
// 在指定的 PlayerLoop 时机继续(如 PreLateUpdate、FixedUpdate 等)
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
// 等待下一帧(替代 yield return null)
await UniTask.Yield();
await UniTask.NextFrame();
// 等待帧结束(替代 WaitForEndOfFrame)
await UniTask.WaitForEndOfFrame(this); // 需要传入 MonoBehaviour(2023 前版本)
// 等待 FixedUpdate 阶段(替代 WaitForFixedUpdate)
await UniTask.WaitForFixedUpdate();
-
这些方法完美适配 Unity 的帧循环和时间系统,比传统协程的
yield return语法更直观。 -
支持细粒度控制代码执行时机(如在
PreLateUpdate还是FixedUpdate阶段继续)。
6. 条件等待
cs
// 等待条件满足(替代 WaitUntil)
await UniTask.WaitUntil(() => isActive == false);
// 等待值变化(简化版 WaitUntil)
await UniTask.WaitUntilValueChanged(this, x => x.isActive);
-
WaitUntil:等待 lambda 表达式返回true时继续。 -
WaitUntilValueChanged:监听对象的某个属性(如isActive),当值变化时继续,简化条件判断代码。
7. 与其他异步类型的兼容性
// 等待传统协程(IEnumerator)
await FooCoroutineEnumerator();
// 等待 .NET 原生 Task(如多线程任务)
await Task.Run(() => 100);
- UniTask 可与 Unity 传统协程、.NET
Task无缝互操作,无需担心兼容性问题。
8. 线程切换
cs
// 切换到线程池(后台线程)执行耗时操作
await UniTask.SwitchToThreadPool();
/* 这里的代码在后台线程执行(不能操作 Unity 主线程对象) */
// 切回主线程(操作 UI 或 GameObject)
await UniTask.SwitchToMainThread();
-
方便地在主线程和后台线程间切换,解决 Unity 中 "主线程才能操作游戏对象" 的限制。
-
适合将密集计算(如数据解析)放到后台线程,避免卡顿。
9. 并行任务与结果聚合
cs
// 并发执行多个网络请求
var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));
// 等待所有任务完成,并通过元组获取结果(简洁语法)
var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
// 更简洁的写法:元组直接 await
var (google2, bing2, yahoo2) = await (task1, task2, task3);
-
并行执行:多个异步任务同时进行,提升效率(如同时加载多个资源或发起多个网络请求)。
-
结果聚合 :通过元组语法一次性获取所有任务的结果,比传统
Task.WhenAll更直观。
总结
UniTask 的核心能力:
-
零 GC 开销:适合移动设备等性能敏感场景。
-
深度适配 Unity:完美结合 Unity 生命周期、帧循环和异步操作。
-
简洁语法 :通过
async/await替代繁琐的协程yield return。 -
强大兼容性 :与传统协程、
Task无缝互操作。