c# 多线程

课程

bilibili Threading in C#

XiaoLi.Notes: 学习笔记

图解系统介绍 | 小林coding | Java面试学习

C# 中的多线程 - GKarch

一 概念

1.

为什么出现先打印主线程再打印新线程的情况吗?线程顺序是怎样的?包含主线程和非主线程的区分和线程执行安排(时间,资源,冲突等等)

一、主线程与非主线程的区分

  1. 主线程(Main Thread)

    • 是 Unity 程序的 "主执行线程",负责处理生命周期函数(StartUpdate 等)、渲染、UI 交互、物理计算等核心逻辑。

    • 你的代码中,Start 方法和直接调用的 ThreadMethod1(2) 都运行在主线程。

  2. 非主线程(子线程)

    • 通过 new Thread(...) 创建的是独立子线程,与主线程并行执行,不能直接操作 Unity 引擎 API (如 Debug.Log 实际会被 Unity 转发到主线程处理,但本身线程是独立的)。

    • 代码中 new Thread(()=>ThreadMethod1(1)).Start() 启动的是子线程。

二、线程执行顺序的本质:并发与调度

线程的执行顺序不固定 ,由操作系统的线程调度器决定,核心原因:

  1. 线程启动有开销 :创建子线程需要分配资源、初始化上下文,这个过程需要时间。而主线程中 ThreadMethod1(2) 是直接调用,几乎没有延迟,因此大概率先执行。

  2. 调度器的不确定性:操作系统调度器根据线程优先级、CPU 核心负载等动态分配执行时间片(毫秒级)。即使子线程启动很快,也可能因为调度器先给主线程分配时间片而晚执行。

    例如:

    • 可能先打印 arg=2(主线程),再打印 arg=1(子线程);

    • 极端情况下(如子线程启动极快且调度器优先调度),也可能先打印 arg=1

三、线程的时间、资源与冲突问题

  1. 时间安排

    • 线程是 "并发执行" 的,即宏观上同时运行,微观上 CPU 通过时间片轮转切换执行(单核)或多核并行(多核 CPU)。

    • 主线程和子线程的执行进度完全独立,没有固定的先后顺序,除非通过同步机制(如 lockManualResetEvent)强制控制。

  2. 资源竞争

    • 若多个线程(包括主线程和子线程)访问共享资源(如全局变量、类成员变量),可能导致数据不一致。例如:

      cs 复制代码
      private int count = 0;
      public void ThreadMethod1(int arg) {
          for (int i = 0; i < 1000; i++) {
              count++; // 多线程同时修改count,可能导致结果错误
          }
      }
    • 解决方式:用 lock 锁定共享资源,保证同一时间只有一个线程访问:

      cs 复制代码
      private object lockObj = new object();
      public void ThreadMethod1(int arg) {
          lock (lockObj) { // 加锁,避免资源竞争
              for (int i = 0; i < 1000; i++) {
                  count++;
              }
          }
      }
  3. Unity 中的线程限制

    • 子线程不能直接调用 Unity 引擎 API (如 Instantiatetransform.positionDebug.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 的工作流程可以概括为以下几个关键步骤

  1. ​编译​ ​:你用 C#、VB.NET 等语言编写的源代码,首先会被编译器编译成​​中间语言(IL)​ ​ 和​​元数据(Metadata)​ ​,并打包成​​程序集​​(.dll 或 .exe 文件)。

  2. ​加载​​:当运行程序时,CLR 会将这些程序集加载到内存中。

  3. ​验证​​:CLR 会对 IL 代码进行验证,确保其类型安全且符合规范。

  4. ​JIT 编译​ ​:当需要执行某段代码(如方法)时,CLR 的​​即时编译器(JIT Compiler)​​ 会将其编译成本地机器的原生代码。

  5. ​执行​​:编译后的原生代码被 CPU 执行,在此过程中,CLR 会持续管理内存、安全、异常处理等。

  6. ​垃圾回收​​: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)和 .NET Task,实现零开销的异步编程。

2. 与 Unity 原生异步对象的集成

cs 复制代码
// 等待资源加载完成
var asset = await Resources.LoadAsync<TextAsset>("foo");

// 等待网络请求完成
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;

// 等待场景加载完成
await SceneManager.LoadSceneAsync("scene2");
  • UniTask 可以直接 await Unity 原生的异步操作(如 Resources.LoadAsyncUnityWebRequestSceneManager.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 无缝互操作。

二 Task

https://blog.csdn.net/m0_53442125/article/details/140565129?fromshare=blogdetail&sharetype=blogdetail&sharerId=140565129&sharerefer=PC&sharesource=m0_73740524&sharefrom=from_link

相关推荐
IOT-Power2 小时前
Qt+C++ 控制软件架构实例
开发语言·c++·qt
MegaDataFlowers2 小时前
认识O(NlogN)的排序
java·开发语言·排序算法
小鸡吃米…2 小时前
调试线程应用程序
开发语言·python
卢锡荣2 小时前
LDR6500|超小封装 Type‑C DRP PD 控制芯片:边充边传,一芯极简,全能适配
开发语言·网络·人工智能·计算机外设·电脑
云深麋鹿2 小时前
C++ | 容器vector
开发语言·c++·容器
格林威2 小时前
工业相机图像高速存储(C#版):直接IO存储方法,附海康相机C#实战代码!
开发语言·人工智能·数码相机·c#·工业相机·海康相机·堡盟相机
下雨打伞干嘛2 小时前
手写Promise
开发语言·前端·javascript
Ronin3052 小时前
【Qt常用控件】输入类控件
开发语言·qt·常用控件·输入类控件
健康平安的活着2 小时前
java中事务@Transaction的正确使用和触发回滚机制【经典】
java·开发语言