| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 游戏开发 -> 进程、线程、协程、并发、并行、串行、unity的协程全程讲解------------知识点6 -> 正文阅读 |
|
[游戏开发]进程、线程、协程、并发、并行、串行、unity的协程全程讲解------------知识点6 |
先了解一下并行和串行 和并发的概念注意: 并发是指同时有很多事要做,(10000个任务分给6个核心,肯定会出现并发现象),你可以串行处理也可以并行处理。 因此并发和并行是相关的,但是是不同的两个概念。
并发现象可以通过一些手段去处理以此提高性能,也可以通过并行的方法去解决!你用并行去解决,并不代表它一定是通过并行解决的,因为你只管写,用不用是操作系统决定的,可能用的还是串行的方式解决的,自己很大可能是并行。
进程
进程具有的特征: 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。 线程线程是操作系统级别的 协程协程是用户、编辑器级别的 区别进程与线程的区别 多线程程序只要有一个线程死掉,整个进程也死掉了 这是为什么? 线程和协程区别
总结一个软件一般是开一个进程(因为开多个进程,那么通信过于麻烦,而且消耗大),所以一个进程里开多个线程,线程有个问题就是,可能同时访问到同一个数据,所以有每个线程里又可以开多个协程。
看一下这个文章下面贴一个大神写的东西,讲解了进程、线程、协程的发展由来 于是,事情就越发复杂化,越发不可解决了。 其实,这类问题有一个很简单的解决办法,那就是——追根溯源。 彻底追到根子上,看看进程、线程、协程究竟是怎么回事——这看起来是绕了弯路,大家想解决实际问题呢,你这不识相的却跑去挖什么纯理论去了…… 不过,如果您肯耐着性子,跟我到它们诞生的那个时刻看一看,一切就迎刃而解了。 进程是什么,我们都知道。这里就不多解释了。 然后,我们还知道,早年的Windows 3.x是非抢夺式多任务。也叫协作式多任务。 这种多任务方式存在一个很大的弊端,那就是必须等一个进程主动让出执行权,其它进程才有机会执行。 如果当前进程不让(比如陷入死循环、比如调用非阻塞API循环死等总是不来的网络报文、比如用错误的接口循环死等读取硬件故障的磁盘),那么整个系统就会陷入瘫痪。 从Windows 95开始,微软切换到了抢夺式多任务:每个进程给你一个时间片,用完就强制休眠;甚至时间片未到但更紧急的事件出现了,也会强制低优先级进程休眠。 抢夺式多任务的确比非抢夺式多任务可靠得多。一个水货写的程序把事情搞砸了,其他人可以不受影响。系统会强制剥夺它的执行权,确保正常程序仍然有机会执行。 亦因此,Windows 95是一个里程碑。它标志着一个真正支持多进程的操作系统出现了。 记住下面这两个概念。它们很重要。 协作式多任务:多个进程独立运行,但每个进程都要发扬风格,不执行规模过大的计算;或者执行规模较大的计算时,每隔一段时间主动调用一下OS提供的特定API,让出控制权给其它进程。 总之,人之初,性本善。每个人都替别人着想,世界就会很美好。 那万一出个恶人、病人呢? 世界崩塌了。 抢夺式多任务:系统里跑的程序,有的是坏人写的,也有的会意外病倒。操作系统要监控所有进程,公平分配CPU时间片等资源给每个应用;如果一个应用用完了自己的份额,那么操作系统就要强制暂停它的执行,保存它的执行现场,把CPU安排给另一个进程——从而避免坏进程/病态进程影响系统的正常运行。 现在,操作系统把你们都当坏人防着。你就是故意写流氓软件,也不可能轻易就把别人“憋死”了。 无论是协作式多任务还是抢夺式多任务,外在表现上都是“用户可以同时运行多个程序,可以前台开着字处理软件,后台放着音乐,另外还有个聊天工具藏在幕后……” 但随着计算机技术的发展,多CPU系统越发普及;就连桌面CPU都悄悄开始双核化。 那么这时候,我们就会想到很多很酷的应用场景。比如说,可以一边从网上下载电影,一边开个视频播放器观看。 但这样就出现了很多新问题:如果下载电影的进程速度更慢,视频播放器读到尚未填充有效数据的区域怎么办? 或者,电影下载进程下了100k数据,一校验,是坏的。结果视频播放器快手快脚拿过去就放;刚放了一帧,电影下载进程作废了这段数据,把读指针跳回到100k前;而电影播放器进程呢,它还保留着一个无效的读指针…… 这时候,程序员就不得不做很多的同步工作。成本过高 这可不是一个小工程。你得先约定共享内存/共享文件格式,约定控制数据存储位置(当前有效数据首尾指针等信息);做好约定,确保双方都能找到锁;锁是读写锁还是简单的mutex……等等等等。 除非同一个团队做,不然想要配合默契,显然是极难极难的。 但既然是同一个团队做,其实没必要搞成两个进程,没必要动用复杂的进程间通讯机制——没错,两个进程可以分别在两个CPU核心上跑,更充分的利用硬件资源;但操作系统也可以允许一个进程存在两个执行绪啊。 于是,线程诞生。 有了进程设计的经验,线程自然一开始就搞的非常完善:进程和线程都要在OS里面注册,这才能接受OS的调度,充分利用多颗CPU核心(或者,某些CPU有多线程支持,一颗CPU核心可以用不同的逻辑电路同时执行两个线程)。 两者的区别是,进程持有资源,一旦退出,进程申请的各种资源都会被OS强制回收;而线程依附于进程,资源不和它绑定。 不仅如此,从一开始,OS就汲取了过去的教训,把线程也做成了抢夺式多任务。 但直接把抢夺式多任务思路延续到线程,问题就来了。 而同一个进程里面的一组线程,它们必然来自于同一个设计团队。哪怕他们用了第三方库,其中的线程也全都在这个团队控制之下。因此“水平良莠不齐”“存在恶意”也就无从谈起了。 一旦不需要对付“水货”和“坏分子”,抢夺式多任务带来的好处就没那么重要了;而“抢夺”造成的“执行时序紊乱”问题越发突出。 但一旦存在抢夺,A线程就可能在刚刚执行到“修改了链表的末指针、但尚未来得及修改最后一块的前向指针”时,被OS强制剥夺执行权;而B线程负责播放,它刚读到这块信息,用户点了“回退5秒钟”,于是它循着A线程尚未来得及修改正确的前向指针,跑不知哪里去了…… 哪怕在单核单线程CPU上跑,(注意多线程是可以new出来的,并不收到物理核心的限制,单核也可以在代码中创出多线程,只是效果不是真正的多线程)这都会造成各种意想不到的执行序紊乱问题。 因此,程序员们不得不在使用共享数据时加锁,确保自己不会把事情搞砸。 此时,非抢占式多任务的好处就出来了:大家都一家人,都想齐心合力把事情做好;因此,当“我”事情没做完而且并不会耽误太久时,你们就应该等我;而一旦我事情做完了、或者需要等待网络信号/磁盘准备好时,“我”也会痛快的主动交出控制权。 这个做法,使得协作式多任务之间执行权的交接点极为明晰;那么只要逻辑考虑清楚了,锁就是完全没必要的——反正不会抢夺嘛,事情没告一段落我就不会交执行权;交执行权之前确保不存在“悬置的、未确定未提交的修改”,脏读脏写就杜绝了。 因此,协程这个概念的提出,使得程序逻辑更为清晰,执行更加可控。 协程实质上是一种在用户空间实现的协作式多线程架构。 它不能让OS知道自己的存在,无法利用多核CPU/CPU的多线程支持;但这恰恰是它的优点。 注意,我在这里的措辞是“协程不能让OS知道自己的存在”。 这是因为,OS并没有协程支持;如果你想让OS知道你的存在,那么它就会把你当线程调度——于是抢占式多任务就又回来了,“协程”这个“协”字就名不副实了。 为什么说这个“无法在CPU上并行”的束缚恰恰是协程的优点呢? 因为它是协作式多任务,不存在执行绪紊乱的可能。 没错,每次执行中,协程之间的具体执行顺序可能千变万化;但协程执行权切换却只会发生在用户明确放弃执行权之后——比如你明确执行了yield语句时。 当然,如果你非要先修改链表后向指针、改完了yield一下然后才去修改链表前向指针,那谁都救不了你。 记住,除非你确定现在的共享数据不怕被其它协程查看/更改,否则不要在共享数据修改完成前随便放弃你的执行权。 当然,多数情况下,使用协程是为了满足“开个小差做点别的”的同时,不希望阻塞主要执行绪。这种简单应用场景多半也没有什么数据需要共享。 一旦挖到根子,是不是一下子所有的一切都清晰起来了呢? 现在,让我们回头看看这些讨论吧。 1、如果资源存在相互依赖,线程是否有必要存在? 答:那要看什么依赖。 比如,我遇到过的一个案例:一组线程负责从磁盘上加载大量日志(可达数百G);第二组线程分头分析日志;第三组线程把日志分析后得到的结果通过网络发送出去。 那么,在这个场景里,虽然线程组2严重依赖于线程组1载入的数据,线程组3又完全依赖于线程组2的输出内容;但使用线程是绝对有必要的。 这是因为,线程组1是磁盘密集型任务,不占用多少CPU;而线程组2是CPU密集型任务,它和IO无关;最后的线程组3呢,它专心和网卡打交道…… 在这个典型的生产者-消费者模型里,三组线程齐头并进,就可以把服务器的磁盘、CPU、网卡同时利用起来,最大化执行效率。 当然,当初的设计者闹了个大笑话。他让线程组1先载入若干G数据,载入完毕之前禁止线程组2运行;等载入结束,线程组1停止运行,等线程组2分析数据;分析完,所有线程组2的线程全部停止执行了,才启动线程组3发报;等线程组3忙完,这才再次启动线程组1。 这个设计很可笑。 该并行的,他给弄的彻底不并行了,磁盘不忙完,CPU只能干瞪眼;CPU没搞定,网卡只能空闲。 不该并行的,他却强制并行了:磁盘读取时,一大窝线程乱纷纷你抢我夺,严重拖慢效率;忙完了,不用抢了,让磁盘闲着,交给线程组2,又是一窝蜂的争夺内存/CPU访问权;然后磁盘CPU都闲着,看一窝新的线程你争我夺的折腾网卡…… 所以你看,压根不是线程好协程坏或者协程穿没穿衣服的问题。 问题的关键点在于,究竟在哪些地方并行可以提高效率?哪些地方并行反而损失效率?如何做出一个精确、智能的设计,使得框架可以自动安排合理数目的线程,把磁盘、CPU、网卡同时利用起来? 显然,多路IO场景下,协程已经可以同时发起多个读取请求;那么如果系统有多块网卡、多块磁盘,OS自然会并行利用它——因为这些接口本来就是异步的(调用同步接口会导致整个进程被挂起,别这样做),OS会自动给它排队,能并行就安排并行了。 但是,想充分利用CPU核心,你就必须用线程。 比如前面的案例中,第一三两组线程就可以用协程替代;但第二组线程就必须是线程。且一三两组协程都应该在一个单独的线程里,不能共享第二组线程。 2、回调地狱问题 这货和协程没什么关系。也就是写起来更好看罢了。 事实上,在这个示例中,改成协程反而会导致语义改变,引出时序相关bug来: foreach session: 这段的语义本来是,v1先做io,得到good结果后,v2再做io,以此类推。 如果机械的改成协程,那么就成了v1~v4同时io,然后因为不满足时序要求大量失败。 除非v1~v4本就可以并行;但此时用线程/协程都一样。只是协程写起来更简单一点罢了。 协程不是状态机。除非你精心设计了它的状态。“看起来像”和“是”差了十万八千里。 总结:协程是一种抛弃了在CPU上并行执行能力的、协作式多任务的执行框架。 这个设计使得你可以像线程一样使用它,却无需担心棘手的数据相关问题。 因为它的执行权交接在你的控制之下,你不交出控制权,别人就不能强插一脚。 借助“遇到等待主动交控制权”这个诀窍,你可以用协程避免一个单线程程序阻塞。 只要你记得在合适时机主动交出控制权,不要调用系统提供的、可能阻塞的API(而是使用协程库提供的非阻塞版、或者使用协程库推荐写法),你甚至可以让磁盘、CPU、网络等不同设备并行运行——这本就是操作系统给你提供一个虚拟的、可并行界面的背后原理。你的操作系统原理学的扎实,那么到这里就不会迷惑。 你可以用线程做“领班”,借助多线程充分利用CPU;同时又在每条线程内部,借助协程并行IO、或者无阻塞的执行互不相关的一组任务——比如,累加一大堆数据,同时每隔100ms更新主界面上的显示。 但要注意,不要在不同线程间共享同一个协程控制器,那会把抢夺式多任务的“执行权随时切换”这个“恶魔”带进协程空间,破坏掉“协作式多任务”这个基本保证。 除非你的协程库允许你这么做。 但哪怕协程库允许,你最好还是不要这么做。因为为了保证协程语义,共享了协程控制器的线程们很可能被这个库用锁给“传染”成“协程”——除非协程库作者给你详细说明,告诉你怎样做才能既不受共享数据被破坏之害、又能享受真正的并行之利。 随便提一句,不要用这种“强大”的协程库。 这种库的作者多半喜欢无意义的炫技。对真正有需求的人来说,自己造轮子可比用这种脱裤子放屁的“高级功能”简单直白太多了。 抽象到这种程度,无论实现还是接口都会变得太过复杂,是对“依赖倒置原则”的严重违背——不仅不能体现其技术水平,反倒暴露出不懂接口设计的缺陷来。 还是开头那段话:技术问题,最好返璞归真。 离根子越近,花里胡哨的东西越少,封装越简单、越清晰、越质朴,它才越可靠、越好用。 反之,拉进来的东西越多,就代表这人的头脑越不清醒,出问题的可能就越大——比如说协程拉进来状态机/回调地狱的,显然就对协程的本质缺乏了解。 最后,出一道思考题。把它做出来,你才会真正明白线程和协程的本质区别。 我曾提到,写一个网络代理软件实质上就是简单的把一个网卡过来的数据转给另一个网卡(也可以是虚拟网络设备,比如tun/tap设备)。而想要这个数据转发高效、低延迟,就应该把它写成单线程。 这是因为,如果你分别用多个线程处理多个网卡的收发,那么一旦网络繁忙,且CPU也比较忙的话,那么很可能其中一条线程就要满负荷跑满一个时间片;在这个线程被剥夺执行权之前,另一个线程可能得不到执行机会。于是造成数据经过代理后ping值不稳定问题。 而用单线程搞呢,你可以给它一个较高的优先级,使得有网络报文它就立即被唤醒执行;不把报文处理完就不交控制权。 那么,只要你程序写对了,它就一定能用最高的效率完成数据转发工作。 那么,这道思考题就是:不允许使用协程,你如何在一个普通的单线程C程序里,用一个while循环,做到多块网卡并行工作,既不阻塞自己、又会在没有报文时主动交出执行权、不空耗时间片呢? (一个拿了实时优先级的程序空耗时间片可是个非常非常严重的问题,随时可能让整个OS崩掉的那种。) 这个问题很简单。查查socket相关资料,推敲推敲各个接口参数,你自然就知道该怎么办了(提示:需要综合硬件中断原理、OS调度原理等知识)。 但它极其重要。 能想通这个,关于协程的讨论才会有的放矢。 这句话什么意思,首先,线程是为了实现并行,一个大数据,需要处理3小时,通过多线程处理后可能只需要1小时,但是会有一个问题,线程是抢夺式的,两个线程可能会在同一时刻,访问到同一个数据,那么会导致冲突,比如,A线程里执行到第2行代码的时候下一步直接去执行了B线程的第2行数据,然后又访问了A线程的第3行,这样会出现很多问题,所以就出现了协程,协程是协作式的,不会像多个线程一样,系统控制时间片,多个线程同一刻(多核)或者同一时间段(单核单线程)去疯狂,他是由你控制的,你可以拿着不放,处理完自己的事情后再交出执行权。这样就会避免那些问题 Unity中的协程、线程线程 Unity3D中的子线程无法运行Unity SDK(开发者工具包,软件包、软件框架)和API(应用程序编程接口,函数库)。 限制原因:大多数游戏引擎都是主循环结构,游戏中逻辑更新和画面更新的时间点要求有确定性,必须按照帧序列严格保持同步,否则就会出现游戏中的对象不同步的现象。虽然多线程也能保证这个效果,但是引用多线程,会加大同步处理的难度与游戏的不稳定性。 但是多线程也是有好处的,如果不是画面更新,也不是常规的逻辑更新(指包括AI、物理碰撞、角色控制这些),而是一些其他后台任务,比如大量耗时的数据计算、网络请求、复杂密集的I/O操作,则可以将这个独立出来做成一个工作线程,这需要写Unity游戏的Native扩展。 协程 线程是操作系统级别的概念,现代操作系统都支持并实现线程,线程的调度对应用开发者是透明的,开发者无法预期某线程在何时被调度执行。基于此,一般那种随机出现的BUG,多与线程调度相关。 而协程Coroutine是编译器级别的,本质是一个线程时间片去执行代码段。它通过相关的代码使得代码段能够实现分段式的执行,显式调用yield函数后才被挂起,重新开始的地方是yield挂起的位置,每一次执行协程会跑到下一个yield语句。协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。 在Unity3D中,协程是可自行停止运行 (yield),直到给定的 YieldInstruction 结束再继续运行的函数。协程 (Coroutines) 的不同用途: ·
(7)yield break - 直接跳出协程,对某些判定失败必须跳出的时候,比如加载AssetBundle的时候,WWW失败了,后边加载bundle没有必要了,这时候可以yield break跳出。 值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足。 以下为Unity3D的生命周期循环图 c#代码示例
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年3日历 | -2025/3/25 20:12:02- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |