线程&进程 异步方法
本文最后更新于16 天前,其中的信息可能已经过时,如有错误请留言

Thread & Process 线程和进程

启动程序时,系统会在内存中创建一个新的进程。进程是构成程序的资源的集合,这些资源包括虚地址空间、文件句柄等
在进程内部,系统创建了一个称为线程的内核对象,他代表了真正执行的程序
默认情况下一个进程只包含一个线程,在任意时刻,一个进程都可包含不同状态的多个线程,他们执行程序的不同部分,这些线程共享同一进程的内存与资源,所以常需要同步来避免数据冲突。
系统为处理器所执行的调度单元是线程而不是进程

虽然同一进程内的不同线程共享同一片虚拟地址空间,但这不等于“可以随便用彼此的资源”

  • 线程安全:并发读写会造成数据竞争、撕裂和不可复现的 Bug。需要锁、原子操作或无锁结构来保证一致性,否则运行一次一个结果。
  • 线程亲缘性(thread affinity):许多“资源”在底层绑定到创建它的线程或特定线程上下文:例如图形 API(OpenGL/Vulkan/Metal/DX 的上下文)、窗口系统、输入回调、音频设备、COM STA 等。
  • Unity 的主循环把引擎状态(场景、Transform、Renderer、Animator、Physics 同步点等)都集中在主线程推进。在非主线程访问经常会抛错或导致未定义行为。
  • 一致性与时序(帧驱动)Unity 每帧有严格的更新顺序(Input → Script Update → Physics → Animation → Rendering)。如果其他线程在错误的时刻修改引擎状态,会和物理/渲染的同步点冲突,破坏帧一致性。

    什么是异步:

    如果一个程序调用某个方法,并在等待方法执行所有处理后才继续执行,这样的方法我们称之为同步的
    相反异步方法在完成所有工作之前就返回到调用方法,这就需要用到async关键字声明的异步方法,和await表达式

    异步方法:

    异步方法具有如下特点:

    • 方法头中包含async方法修饰符
    • 包含一个或多个await表达式,表示可以异步完成的任务
    • 必须具备以下3种返回类型之一,void,Task,Task<T>, ValueTask<T>
    • 形参可以为任意类型、任意数量,但不能为out / ref参数
    • 按照约定,异步方法的名称应以Async为后缀
    • 除了方法之外,lambda表达式和匿名方法也可以作为异步对象

    Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个 Task 类型的对象。在这种情况下,如果异步方法中包含任何 return 语句,则它们不能返回任何东西。下面的代码来自于一个调用方法:

    Task someTask = DoStuff.CalculateSumAsync(5, 6);
    …
    someTask.Wait();

    Task<T>:任何返回 Task 类型的异步方法,其返回值必须为 T 类型或可以隐式转换为 T 的类型。调用方法将通过读取 Task 的 Result 属性来获取这个 T 类型的值

    Task<int> value = DoStuff.CalculateSumAsync(5, 6);
    Console.WriteLine($"Value: { value.Result }");

    ValueTask:这是一个值类型对象,它与 Task 类似,但用于任务结果可能已经可用的情况。因为它是一个值类型,所以它可以放在栈上,而不须像 Task 对象那样在堆上分配空间。因此,它在某些情况下可以提高性能。

    void:如果调用方法仅仅想执行异步方法,而不需要与它做进一步的交互(这种称为“调用并忘记”(fire and forget)),异步方法可以返回 void 类型。这时,与上一种情况类似,如果异步方法中包含任何 return 语句,则它们不能返回任何东西

    异步方法的控制流

    async(asynchronous异步)和 await:async该修饰符只是标识该方法包含一个或多个 await 表达式。也就是说,它本身并不能创建任何异步操作

    异步方法的结构包含三个不同的区域:

    • 第一个 await 表达式之前的部分:从方法开头到第一个 await 表达式之前的所有代码。这一部分应该只包含少量无须长时间处理的代码。
    • await 表达式:表示将被异步执行的任务。
    • 后续部分:await 表达式之后的方法中的其余代码。包括其执行环境,如所在线程信息、目前作用域内的变量值,以及当 await 表达式完成后重新执行时所需的其他信息

    图21-10 阐明了一个异步方法的控制流。它从第一个 await 表达式之前的代码开始,正常(同步地)执行直到遇见第一个 await。这一区域实际上在第一个 await 表达式处结束,此时 await 的任务还没有完成(大多数情况下如此)。当 await 的任务完成时,方法将继续同步执行。如果还有其他 await,就重复上述过程

    Thread Task使用案例

    单线程做菜:UI进程和做菜进程共用主线程,因此在做菜的过程中,无法拖动UI页面,只有做完菜后才能拖动UI界面,这就是单线程

    private void button1_Click(object sender, EventArgs e)
    {
        Thread.Sleep(3000);
        MessageBox.Show("素菜做好了", "友情提示");
        Thread.Sleep(5000);
        MessageBox.Show("荤菜做好了", "友情提示");
    }

    线程的构造方法需要传入一个无返回值无参/无返回值有一个参数的委托(即一个无返回值无参数的方法,所以可选择传入一个lambda表达式),并可选择在第二个参数自定义线程栈大小(单位:字节)

            public Thread(ParameterizedThreadStart start);
            public Thread(ThreadStart start);
            public Thread(ParameterizedThreadStart start, int maxStackSize);
            public Thread(ThreadStart start, int maxStackSize);

    Thread做菜:在做菜的过程中窗口可以移动

    1 个引用
    private void button2_Click(object sender, EventArgs e)
    {
        Thread t = new Thread(() =>
        {
            Thread.Sleep(3000);
            MessageBox.Show("素菜做好了", "友情提示");
            Thread.Sleep(5000);
            MessageBox.Show("荤菜做好了", "友情提示");
        });
        t.Start();
    }

    Task做菜:但在C#中更推荐用Task,Thread 类属于较低层次的抽象,它直接映射到操作系统的线程。使用 Thread 时,你要对线程的生命周期进行直接管理,像创建、启动、暂停、终止等操作。Task是在Thread之上的封装,因此使用Task用相同的语法创建线程会更方便管理

    private void button3_Click(object sender, EventArgs e)
    {
        Task.Run(() =>
        {
            Thread.Sleep(3000);
            MessageBox.Show("素菜做好了", "友情提示");
            Thread.Sleep(5000);
            MessageBox.Show("荤菜做好了", "友情提示");
        });
    }

    同时做了多道菜,这种多线程的优势就是能够利用CPU的多核,因此多线程编程可以大大的提高效率,

    private void button4_Click(object sender, EventArgs e)
    {
        Task.Run(() =>
        {
            Thread.Sleep(3000);
            MessageBox.Show("素菜做好了", "友情提示");
        });
        Task.Run(() =>
        {
            Thread.Sleep(5000);
            MessageBox.Show("荤菜做好了", "友情提示");
        });
    }

    等待当前线程结束后再执行主线程中的后面方法,需要将方法声明为异步方法,通过在函数签名中加入async实现这一点,并在Task前加关键字await

    1 个引用
    private async void button5_Click(object sender, EventArgs e)
    {
        await Task.Run(() =>
        {
            Thread.Sleep(3000);
            MessageBox.Show("素菜做好了", "友情提示");
        });
        await Task.Run(() =>
        {
            Thread.Sleep(5000);
            MessageBox.Show("荤菜做好了", "友情提示");
        });
        MessageBox.Show("菜都做好了,大家快来吃饭!", "提示");
    }

    但如果需要等待多个线程都结束,可以使用Task包装好的方法WhenAll(这似乎和Dotween一样,也是链式调用方法,可以不断在函数后调用新的方法),其需要将所有线程添加到一个列表中

    private void button5_Click(object sender, EventArgs e)
    {
        List<Task> ts = new List<Task>();
        ts.Add(Task.Run(() =>
        {
            Thread.Sleep(3000);
            MessageBox.Show("素菜做好了", "友情提示");
        }));
        ts.Add(Task.Run(() =>
        {
            Thread.Sleep(5000);
            MessageBox.Show("荤菜做好了", "友情提示");
        }));
    
        Task.WhenAll(ts).ContinueWith(t =>
        {
            MessageBox.Show("菜都做好了,大家快来吃饭!", "提示");
        });
    }

    await 表达式

    await表达式指定了一个异步执行的任务,默认情况下,这个任务在当前线程下异步执行

    await task

    task应为awaitable类型的实例,然而实际上并不需要构建自己的awaitable,相反应该使用Task类或ValueTask类,它们是awaitable类型
    尽管目前 BCL 中存在很多返回 Task 类型对象的方法,你仍然可能需要编写自己的方法,作为 await 表达式的任务。最简单的方式是在你的方法中使用 Task.Run 方法来创建一个 Task。关于 Task.Run,有一点非常重要,即它在不同的线程上运行你的方法。

    Task.Run 的一个签名如下,它以 Func<TReturn> 委托为参数。如第 20 章所述,Func<TReturn> 是一个预定义的委托,它不包含任何参数,返回值的类型为 TReturn:

    Task Run( Func<TReturn> func )

    因此,要将你的方法传递给 Task.Run 方法,需要基于该方法创建一个委托。下面的代码展示了三种实现方式。其中,Get10 与 Func<int> 委托兼容,因为它没有参数并且返回 int。
    □ 第一个实例(DoWorkAsync 方法的前两行)使用 Get10 创建名为 ten 的 Func<int> 委托。然后在下一行将该委托用于 Task.Run 方法。
    □ 第二个实例在 Task.Run 方法的参数列表中创建 Func<int> 委托。
    □ 第三个实例没有使用 Get10 方法,而是使用了 Get10 方法体的 return 语句,并将其用于与 Func<int> 委托兼容的 Lambda 表达式。该 Lambda 表达式将隐式转换为该委托

    using System;
    using System.Threading.Tasks;
    
    class MyClass
    {
        public int Get10()      // 与 Func<int> 兼容
        {
            return 10;
        }
    
        public async Task DoWorkAsync()
        {
            // 第一种写法:先用 Get10 创建委托,再传给 Task.Run
            Func<int> ten = new Func<int>(Get10);
            int a = await Task.Run(ten);
    
            // 第二种写法:直接在 Task.Run 参数中创建委托
            int b = await Task.Run(new Func<int>(Get10));
    
            // 第三种写法:用 Lambda 表达式(与 Func<int> 兼容)
            int c = await Task.Run(() => { return 10; });
    
            Console.WriteLine($"{ a } { b } { c }");
        }
    }
    
    class Program
    {
        static void Main()
        {
            Task t = (new MyClass()).DoWorkAsync();
            t.Wait();
        }
    }
    

    Thread常用函数

    方法/属性作用备注
    new Thread(Action)创建线程传入线程入口委托
    Start()启动线程必须先 Start 才会跑
    Join() / Join(ms)等线程结束主线程慎用,避免卡界面
    IsAlive是否仍在运行只读
    IsBackground后台线程标志后台线程不阻止进程退出
    Name / ManagedThreadId线程名 / ID便于调试
    Priority优先级罕用
    Sleep(ms)暂停当前线程粗略延时,非高精度
    Yield()让出时间片轻量“礼让”一下
    Thread.CurrentThread获取当前线程对象可读各属性
    学习笔记如有侵权,请提醒我,我会马上删除
    暂无评论

    发送评论 编辑评论

    
    				
    |´・ω・)ノ
    ヾ(≧∇≦*)ゝ
    (☆ω☆)
    (╯‵□′)╯︵┴─┴
     ̄﹃ ̄
    (/ω\)
    ∠( ᐛ 」∠)_
    (๑•̀ㅁ•́ฅ)
    →_→
    ୧(๑•̀⌄•́๑)૭
    ٩(ˊᗜˋ*)و
    (ノ°ο°)ノ
    (´இ皿இ`)
    ⌇●﹏●⌇
    (ฅ´ω`ฅ)
    (╯°A°)╯︵○○○
    φ( ̄∇ ̄o)
    ヾ(´・ ・`。)ノ"
    ( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
    (ó﹏ò。)
    Σ(っ °Д °;)っ
    ( ,,´・ω・)ノ"(´っω・`。)
    ╮(╯▽╰)╭
    o(*////▽////*)q
    >﹏<
    ( ๑´•ω•) "(ㆆᴗㆆ)
    😂
    😀
    😅
    😊
    🙂
    🙃
    😌
    😍
    😘
    😜
    😝
    😏
    😒
    🙄
    😳
    😡
    😔
    😫
    😱
    😭
    💩
    👻
    🙌
    🖕
    👍
    👫
    👬
    👭
    🌚
    🌝
    🙈
    💊
    😶
    🙏
    🍦
    🍉
    😣
    Source: github.com/k4yt3x/flowerhd
    颜文字
    Emoji
    小恐龙
    花!
    上一篇
    下一篇