当前位置:   article > 正文

进程、线程、协程、并发、并行、串行、unity的协程全程讲解------------知识点6_unity 并发

unity 并发

先了解一下并行和串行 和并发的概念

注意:
并发是对需求侧的描述,并行、串行才是对实现侧的描述,这两根本不是同一个范畴的东西,更不可能是互斥的关系。一定不要被同时段(并发)、同时刻(并行)那些老被拿出来一起比较的概念,弄混了本质

并发是指同时有很多事要做,(10000个任务分给6个核心,肯定会出现并发现象),你可以串行处理也可以并行处理。
并行是指同时做多件事。

因此并发和并行是相关的,但是是不同的两个概念

并发:并发(同一时间段,而不是同时)(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。注意这是一种现象,同时运行多个程序或多个任务需要被处理的现象。在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象。并发描述的是多进程同时运行的现象(多核的时候)。但实际上,对于单核心CPU来说,同一时刻只能运行一个进程。所以,这里的"同时运行"表示的不是真的同一时刻有多个进程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占CPU的,而是执行一会停一会。
**注意!**分为两种:
线程和进程都可用于实现并发。
1、多核的时候,多进程、线程同时运行的现象算并发
2、单核的时候,多进程、线程在同一段时间内来回切换也算并发
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。如:打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。

并发现象可以通过一些手段去处理以此提高性能,也可以通过并行的方法去解决!你用并行去解决,并不代表它一定是通过并行解决的,因为你只管写,用不用是操作系统决定的,可能用的还是串行的方式解决的,自己很大可能是并行。

并行:当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,可以同时通过多进程/多线程的方式取得多个任务,并以多进程或多线程的方式同时执行这些任务,这种方式我们称之为并行(Parallel)。
其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU。单核是无法实现真正的并行的,注意(某些CPU有多线程支持,一颗CPU核心可以用不同的逻辑电路同时执行两个线程)

串行:
串行表示所有任务都一一按先后顺序进行。完成A才会进行B
数据少的时候,串行效率是
在这里插入图片描述
有一个很好的博客说明 关于串行和并行、并发的讲解
有这么一句话
当程序中写下多进程或多线程代码时,这意味着的是并发而不是并行。并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。也就是说,并行与否程序员无法控制,只能让操作系统决定。(可以看出并发是一种现象,)

进程

一个进程好比是一个程序,它是 资源分配的最小单位 。同一时刻执行的进程数不会超过核心数。不过如果问单核CPU能否运行多进程?答案又是肯定的。(这里其实是并发)单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程。举个简单的例子,就算是十年前的单核CPU的电脑,也可以聊QQ的同时看视频。电脑中有许多进程需要处于「同时」开启的状态,而利用CPU在进程间的快速切换,可以实现「同时」运行多个程序。而进程切换则意味着需要保留进程切换前的状态,以备切换回去的时候能够继续接着工作。所以进程拥有自己的地址空间,全局变量,文件描述符,各种硬件等等资源。操作系统通过调度CPU去执行进程的记录、回复、切换等等。

进程具有的特征:
1)动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
2)并发性:任何进程都可以同其他进程一起并发执行;
3)独立性:进程是系统进行资源分配和调度的一个独立单位;
4)结构性:进程由程序、数据和进程控制块三部分组成。
3.2 为什么要有多进程?
多进程目的:提高cpu的使用率。
一个例子:一个用户现在既想使用打印机,又想玩游戏。
假设只有一个进程(先不谈多线程):从操作系统的层面看,我们使用打印机的步骤有如下:
1)使用CPU执行程序,去硬盘读取需要打印的文件,然后CPU会长时间的等待,直到硬盘读写完成;2)使用CPU执行程序,让打印机打印这些内容,然后CPU会长时间的等待,等待打印结束。在这样的情况下:其实CPU的使用率其实非常的低。打印一个文件从头到尾需要的时间可能是1分钟,而cpu使用的时间总和可能加起来只有几秒钟。而后面如果单进程执行游戏的程序的时候,CPU也同样会有大量的空闲时间。使用多进程后:当CPU在等待硬盘读写文件,或者在等待打印机打印的时候,CPU可以去执行游戏的程序,这样CPU就能尽可能高的提高使用率。再具体一点说,其实也提高了效率。因为在等待打印机的时候,这时候显卡也是闲置的,如果用多进程并行的话,游戏进程完全可以并行使用显卡,并且与打印机之间也不会互相影响。
进程,直观点说:保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。

操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。

线程

线程是操作系统级别的
是进程的一个实体,是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间的通信是要比进程间的通信,代价小很多,所以一个软件,用多线程可以大大缩短处理一个事情的时间。

协程

协程是用户、编辑器级别的
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 协程在子程序内部可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。

区别

进程与线程的区别
进程是CPU资源分配的基本单位,线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。
进程拥有自己的资源空间,一个进程包含若干个线程,线程与CPU资源分配无关,多个线程共享同一进程内的资源。
线程的调度与切换比进程快很多。所以一个软件或一大堆数据一般是多线程处理,
1.线程是进程的一部分,线程也被称为轻量级进程,一个进程至少有一个线程(主线程)。
2. 进程拥有独立的内存单元,而属于这个进程的线程共享该内存(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;以及进程所拥有的全部资源(线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间)。
3. 一个进程崩溃不会影响其他进程,一个线程挂掉会影响其他线程甚至导致整个进程挂掉(因为2)。
4. 进程切换开销大,线程切换开销小(因为2)。
5. 进程的并发性没有线程高

多线程程序只要有一个线程死掉,整个进程也死掉了 这是为什么?
每个进程都有自己的一个单独的内存空间,属于这个进程的线程共享进程内存,该线程如果持有互斥资源得不到释放,会影响整个进程的正常执行,所以干脆就让整个进程挂掉
为什么操作系统不用协程
无论是空间还是时间性能都比进程(线程)好这么多,那么Linus为啥不把它在操作系统里实现了多好?操作系统为了实现实时性更好的目的,对一些优先级比较高的进程是会抢占其它进程的CPU的。而协程无法实现这一点,还得依赖于挡前使用CPU的协程主动释放,于操作系统的实现目的不相吻合。所以协程的高效是以牺牲可抢占性为代价的。

线程和协程区别


协程缺点:无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

总结

一个软件一般是开一个进程(因为开多个进程,那么通信过于麻烦,而且消耗大),所以一个进程里开多个线程,线程有个问题就是,可能同时访问到同一个数据,所以有每个线程里又可以开多个协程。

首先,线程是为了实现并行,一个大数据,需要处理3小时,通过多线程处理后可能只需要1小时,但是会有一个问题,线程是抢夺式的,两个线程可能会在同一时刻,访问到同一个数据,那么会导致冲突,比如,A线程里执行到第2行代码的时候下一步直接去执行了B线程的第2行数据,然后又访问了A线程的第3行,这样会出现很多问题,所以就出现了协程,协程是协作式的,不会像多个线程一样,系统控制时间片,多个线程同一刻(多核)或者同一时间段(单核单线程)去疯狂,他是由你控制的,你可以拿着不放,处理完自己的事情后再交出执行权。这样就会避免那些问题

看一下这个文章

下面贴一个大神写的东西,讲解了进程、线程、协程的发展由来
进程、线程、协程的发展由来
这里直接粘贴一下
很多技术上的东西,你拦腰来一下或者在末梢上抓几个典型,就容易简单问题复杂化。这篇文章的问题就在于此:作者拦腰从“用协程优化线程”这个奇怪的中间应用场景开始,没头没尾开始“揭露真相”;然后又有很多人从末梢开始,引入“协程本质上是状态机”、“协程用于解决callback hell”、“协程在IO密集型业务中才能发挥威力”等莫名其妙却又言之凿凿的结论。

于是,事情就越发复杂化,越发不可解决了。

其实,这类问题有一个很简单的解决办法,那就是——追根溯源。

彻底追到根子上,看看进程、线程、协程究竟是怎么回事——这看起来是绕了弯路,大家想解决实际问题呢,你这不识相的却跑去挖什么纯理论去了……

不过,如果您肯耐着性子,跟我到它们诞生的那个时刻看一看,一切就迎刃而解了。

进程是什么,我们都知道。这里就不多解释了。

然后,我们还知道,早年的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就汲取了过去的教训,把线程也做成了抢夺式多任务

但直接把抢夺式多任务思路延续到线程,问题就来了。
OS里面,不同进程是诸多水平参差不齐、目的各异(甚至本就存心做流氓软件)的人设计的。因此设定一个硬杠杠,时机成熟强制剥夺其执行权,这是极其必要的。

而同一个进程里面的一组线程,它们必然来自于同一个设计团队。哪怕他们用了第三方库,其中的线程也全都在这个团队控制之下。因此“水平良莠不齐”“存在恶意”也就无从谈起了。

一旦不需要对付“水货”和“坏分子”,抢夺式多任务带来的好处就没那么重要了;而“抢夺”造成的“执行时序紊乱”问题越发突出。
抢夺式多任务的坏处
比如,A线程负责从网上下载内容;每下载一段、校验无误,它就要更新一下共享内存(这可能仅仅是一个把一块数据挂到链表末尾的操作)——如果不存在“抢夺”,那么什么时候它把内容更新完了,什么时候主动让出控制权,那就不会有任何问题

但一旦存在抢夺,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()
if v1.is_good:
v2 = io()
if v2.is_good:
v3 = io()
if v3.is_good:
v4 = io()
if v4.is_good:
handle(v1, v2, v3, v4)

这段的语义本来是,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调度原理等知识)。

但它极其重要。

能想通这个,关于协程的讨论才会有的放矢。
看完上面的文章后,注意总结:
线程 的发明主要是实现 Parallelism(并行) 问题,减少数据处理时间,,而协程 的发明主要是为了解决 Concurrency(并发)引起的数据冲突问题 。

这句话什么意思,首先,线程是为了实现并行,一个大数据,需要处理3小时,通过多线程处理后可能只需要1小时,但是会有一个问题,线程是抢夺式的,两个线程可能会在同一时刻,访问到同一个数据,那么会导致冲突,比如,A线程里执行到第2行代码的时候下一步直接去执行了B线程的第2行数据,然后又访问了A线程的第3行,这样会出现很多问题,所以就出现了协程,协程是协作式的,不会像多个线程一样,系统控制时间片,多个线程同一刻(多核)或者同一时间段(单核单线程)去疯狂,他是由你控制的,你可以拿着不放,处理完自己的事情后再交出执行权。这样就会避免那些问题

Unity中的协程、线程

线程 
  Unity3D是以生命周期主线程循环进行游戏开发。支持多线程,只是其他线程不能访问Unity相关对象的内容,强行写会报错。unity,私自开的线程记得关闭,因为,如果你不关闭,即使你停止运行,也会自己去调用线程后续的操作,它是与unity软件共存亡的

Unity3D中的子线程无法运行Unity SDK(开发者工具包,软件包、软件框架)和API(应用程序编程接口,函数库)。

限制原因:大多数游戏引擎都是主循环结构,游戏中逻辑更新和画面更新的时间点要求有确定性,必须按照帧序列严格保持同步,否则就会出现游戏中的对象不同步的现象虽然多线程也能保证这个效果,但是引用多线程,会加大同步处理的难度与游戏的不稳定性

但是多线程也是有好处的,如果不是画面更新,也不是常规的逻辑更新(指包括AI、物理碰撞、角色控制这些),而是一些其他后台任务,比如大量耗时的数据计算、网络请求、复杂密集的I/O操作,则可以将这个独立出来做成一个工作线程,这样就不会卡顿主线程,也能更快的运行完。计算完成就把结果放到公共内存区域,主线程去取。这需要写Unity游戏的Native扩展。

协程
  对于Unity3D,它是生命周期主线程循环的设计,它更倾向于使用Time slicing(时间分片)的Coroutine(协程)去完成异步任务,融合到生命周期中。

线程是操作系统级别的概念,现代操作系统都支持并实现线程,线程的调度对应用开发者是透明的,开发者无法预期某线程在何时被调度执行。基于此,一般那种随机出现的BUG,多与线程调度相关。

而协程Coroutine是编译器级别的,本质是一个线程时间片去执行代码段。它通过相关的代码使得代码段能够实现分段式的执行,显式调用yield函数后才被挂起,重新开始的地方是yield挂起的位置,每一次执行协程会跑到下一个yield语句。协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。因为unity是一个循环的过程,就是Update不同的执行,这一帧发现协程

在Unity3D中,协程是可自行停止运行 (yield),直到给定的 YieldInstruction 结束再继续运行的函数。协程 (Coroutines) 的不同用途: ·

   (1) yield return null - 这一帧到此暂停,下一帧再从暂停处继续,常用于循环中。
   这个可以用来创建10w个物体啥的,用这个,比如for循环里10w个,
   然后里面每到1000得倍数,就可执行一次yield return null ,那么就等于一帧就创建1000个
   注意这里可以返回各种值, yield return 1 , yield return 2 啥的,都是等待1帧

   (2) yield return new WaitForEndOfFrame - 等到这一帧的cameras和GUI渲染结束后再从此处继续,
   即等到这帧的末尾再往下运行。这行之后的代码还是在当前帧运行,是在下一帧开始前执行,跟return null很相似。

   (3) yield return new WaitForFixedUpdate - 在下一次执行FixedUpdate的时候继续执行这段代码,
   即等一次物理引擎的更新。

   (4) yield return new WaitForSeconds(3.0f) - 等待3秒,然后继续从此处开始,常用于做定时器。

   (5) yield return WWW - 等待直至异步下载完成。

   (6) yield return StartCoroutine(methodName) - 等待另一个协程执行完。这是把协程串联起来的关键,
   常用于让多个协程按顺序逐个运行。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

(7)yield break - 直接跳出协程,对某些判定失败必须跳出的时候,
  比如加载AssetBundle的时候,WWW失败了,后边加载bundle没有必要了,这时候可以yield break跳出。

值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足。

以下为Unity3D的生命周期循环图

c#代码示例

Unity3D使用协程常需要用到辅助类Stopwatch,提供一组可用于准确地测量运行时间的方法和属性。

private Stopwatch frameStopwatch;//用来记录上一帧结束到现在所用的时间
private float targetFrameDuration;//自定义的每帧持续时间,防止协程过度消耗线程时间片
private void Awake()
{
    frameStopwatch = new Stopwatch();
}

void Update()
{
    //计算每一帧所用的时间Start()之后Elapsed会一直增加,Stop()之后Elapsed的值就不变
    frameStopwatch.Stop();
    frameStopwatch.Reset();
    frameStopwatch.Start();

  if(ChunkUpdateList.Count > 0)
  {
    StartCoroutine(ProcessChunkQueueLoop());//启动协程处理ChunkUpdateList
  }
}

private IEnumerator ProcessChunkQueueLoop()
{
    while (ChunkUpdateList.Count > 0)
    {
        ProcessChunkUpdateList();//每次处理ChunkUpdateList中的一个数据
        if (frameStopwatch.Elapsed.TotalSeconds >= targetFrameDuration)//这一帧已经运行的时间frameStopwatch.Elapsed.TotalSeconds已经超过自定义每帧的时间targetFrameDuration,则挂起直到这一帧结束再运行
        {
            yield return new WaitForEndOfFrame();
        }
    }
}
 

引用:

  (1)游戏引擎Unity中的单线程与多线程

其他:

  (1)游戏主循环

  (2)3D引擎多线程:渲染与逻辑分离


转载于:https://www.cnblogs.com/DonYao/p/8571981.html
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

协程在Unity中简单操作

协程在Unity中简单操作
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class Lesson14 : MonoBehaviour
{
    Thread t;

    //申明一个变量作为一个公共内存容器
    Queue<Vector3> queue = new Queue<Vector3>();

    Queue<Vector3> queue2 = new Queue<Vector3>();

    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 Unity是否支持多线程?
        //首先要明确一点
        //Unity是支持多线程的
        //只是新开线程无法访问Unity相关对象的内容

        //注意:Unity中的多线程 要记住关闭

        t = new Thread(Test);
        //t.Start();
        #endregion

        #region 知识点二 协同程序是什么?
        //协同程序简称协程
        //它是“假”的多线程,它不是多线程

        //它的主要作用
        //将代码分时执行,不卡主线程
        //简单理解,是把可能会让主线程卡顿的耗时的逻辑分时分步执行

        //主要使用场景
        //异步加载文件
        //异步下载文件
        //场景异步加载
        //批量创建时防止卡顿
        #endregion

        #region 知识点三 协同程序和线程的区别
        //新开一个线程是独立的一个管道,和主线程并行执行
        //新开一个协程是在原线程之上开启,进行逻辑分时分步执行
        #endregion

        #region 知识点四 协程的使用
        //继承MonoBehavior的类 都可以开启 协程函数
        //第一步:申明协程函数
        //  协程函数2个关键点
        //  1-1返回值为IEnumerator类型及其子类
        //  1-2函数中通过 yield return 返回值; 进行返回

        //第二步:开启协程函数
        //协程函数 是不能够 直接这样去执行的!!!!!!!
        //这样执行没有任何效果
        //MyCoroutine(1, "123");
        //常用开启方式
        //IEnumerator ie = MyCoroutine(1, "123");
        //StartCoroutine(ie);
        最快的快捷写法:
        声明变量接收是因为要定向停止协程
        Coroutine c1 = StartCoroutine( MyCoroutine(1, "123") );
        Coroutine c2 = StartCoroutine( MyCoroutine(1, "123"));
        Coroutine c3 = StartCoroutine( MyCoroutine(1, "123"));

        //第三步:关闭协程
        //关闭所有协程
        //StopAllCoroutines();

        //关闭指定协程
        //StopCoroutine(c1);

        #endregion

        #region 知识点五 yield return 不同内容的含义
        //1.下一帧执行
        //yield return 数字;
        //yield return null;
        //在Update和LateUpdate之间执行

        //2.等待指定秒后执行
        //yield return new WaitForSeconds(秒);
        //在Update和LateUpdate之间执行

        //3.等待下一个固定物理帧更新时执行
        //yield return new WaitForFixedUpdate();
        //在FixedUpdate和碰撞检测相关函数之后执行

        //4.等待摄像机和GUI渲染完成后执行,这个主要用来截图用
        //yield return new WaitForEndOfFrame();
        //在LateUpdate之后的渲染相关处理完毕后之后

        //5.一些特殊类型的对象 比如异步加载相关函数返回的对象
        //之后讲解 异步加载资源 异步加载场景 网络加载时再讲解
        //一般在Update和LateUpdate之间执行

        //6.跳出协程
        //yield break;
        #endregion

        #region 知识点六 协程受对象和组件失活销毁的影响
        //协程开启后
        //组件和物体销毁,协程不执行
        //物体失活协程不执行,组件失活协程执行
        #endregion

        #region 总结
        //1.Unity支持多线程,只是新开线程无法访问主线程中Unity相关内容
        //  一般主要用于进行复杂逻辑运算或者网络消息接收等等
        //  注意:Unity中的多线程一定记住关闭
        //2.协同程序不是多线程,它是将线程中逻辑进行分时执行,避免卡顿
        //3.继承MonoBehavior的类都可以使用协程
        //4.开启协程方法、关闭协程方法
        //5.yield return 返回的内容对于我们的意义
        //6.协程只有当组件单独失活时不受影响,其它情况协程会停止
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        if( queue.Count > 0 )
        {
            this.transform.position = queue.Dequeue();
        }
    }

    //关键点一: 协同程序(协程)函数 返回值 必须是 IEnumerator或者继承它的类型 
    IEnumerator MyCoroutine(int i, string str)
    {
        print(i);
        //关键点二: 协程函数当中 必须使用 yield return 进行返回
        yield return null;
        print(str);
        yield return new WaitForSeconds(1f);
        print("2");
        yield return new WaitForFixedUpdate();
        print("3");
        //主要会用来 截图时 会使用
        yield return new WaitForEndOfFrame();
        
        while(true)
        {
            print("5");
            yield return new WaitForSeconds(5f);
        }
    }

    private void Test()
    {
        while(true)
        {
            Thread.Sleep(1000);
            //相当于模拟 复杂算法 算出了一个结果 然后放入公共容器中
            System.Random r = new System.Random();
            queue.Enqueue(new Vector3(r.Next(-10,10), r.Next(-10, 10), r.Next(-10, 10)));
            print("123");
        }
    }

    private void OnDestroy()
    {
        t.Abort();
        t = null;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171

协程的原理

1)延时(等待)一段时间执行代码;2)等某个操作完成之后再执行后面的代码。总结起来就是一句话:控制代码在特定的时机执行。
//协程可以分成两部分
1.协程函数本体() 本质上就是迭代器函数,是C#语法
2.协程调度器(如果是通过StartCorout开启的,那么就不用自己去movenext()了,unity自己实现了一套迭代器的调度系统,理论自己也可以实现一套,不用unity的,下面实现了一个简单的)

//协程本体就是一个能够中间暂停返回的函数
//协程调度器是Unity内部实现的,会在对应的时机帮助我们继续执行协程函数

//Unity只实现了协程调度部分
//协程的本体本质上就是一个 C#的迭代器方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestClass
{
    public int time;
    public TestClass(int time)
    {
        this.time = time;
    }
}

public class Lesson15 : MonoBehaviour
{
    //首先这里去写一个迭代器函数
    IEnumerator Test()
    {
        print("第一次执行");
        yield return 1;
        print("第二次执行");
        yield return 2;
        print("第三次执行");
        yield return "123";
        print("第四次执行");
        yield return new TestClass(10);
    }

    void Start()
    {
        #region 知识点一 协程的本质
        //协程可以分成两部分
        //1.协程函数本体
        //2.协程调度器

        //协程本体就是一个能够中间暂停返回的函数
        //协程调度器是Unity内部实现的,会在对应的时机帮助我们继续执行协程函数

        //Unity只实现了协程调度部分
        //协程的本体本质上就是一个 C#的迭代器方法
        #endregion

        #region 知识点二 协程本体是迭代器方法的体现
        //1.协程函数本体
        //如果我们不通过 开启协程方法执行协程 
        //Unity的协程调度器是不会帮助我们管理协程函数的,
        //需要自己一个个的去移动光标去打印
        IEnumerator ie = Test();

        //但是我们可以自己执行迭代器函数内容
        //ie.MoveNext();//会执行函数中内容遇到 yield return为止的逻辑
        //print(ie.Current);//得到 yield return 返回的内容

        //ie.MoveNext();
        //print(ie.Current);

        //ie.MoveNext();
        //print(ie.Current);

        //ie.MoveNext();
        //TestClass tc = ie.Current as TestClass;
        //print(tc.time);

        //因为MoveNext 返回值是个bool 代表着 是否到了结尾(这个迭代器函数 是否执行完毕)
        //所以就能直接通过循环解决
        while(ie.MoveNext())
        {
            print(ie.Current);
        }

        //2.协程调度器
        //继承MonoBehavior后 开启协程
        //相当于是把一个协程函数(迭代器)放入Unity的协程调度器中帮助我们管理进行执行
        //具体的yield return 后面的规则 也是Unity定义的一些规则

        //总结
        //你可以简化理解迭代器函数
        //C#看到迭代器函数和yield return 语法糖
        //就会把原本是一个的 函数 变成"几部分"
        //我们可以通过迭代器 从上到下遍历这 "几部分"进行执行
        //就达到了将一个函数中的逻辑分时执行的目的

        //而协程调度器就是 利用迭代器函数返回的内容来进行之后的处理
        //比如Unity中的协程调度器
        //根据yield return 返回的内容 决定了下一次在何时继续执行迭代器函数中的"下一部分"

        //理论上来说 我们可以利用迭代器函数的特点 自己实现协程调度器来取代Unity自带的调度器
        #endregion

        #region 总结
        //协程的本质 就是利用 
        //C#的迭代器函数"分步执行"的特点
        //加上
        //协程调度逻辑
        //实现的一套分时执行函数的规则
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105

实现一个简单的协程

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson15_Exercises : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        //Unity自带的协程协调器 开启协程函数(迭代器函数)
        //StartCoroutine(MyTest());

        CoroutineMgr.Instance.MyStartCoroutine(MyTest());
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    IEnumerator MyTest()
    {
        print("1");
        yield return 1;//如果是用自带的 开启 那么数字代表等待1帧继续执行之后的内容
        print("2");
        yield return 2;
        print("3");
        yield return 3;
        print("4");
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class YieldReturnTime
{
    //记录 下次还要执行的 迭代器接口
    public IEnumerator ie;
    //记录 下次执行的时间点
    public float time;
}

public class CoroutineMgr : MonoBehaviour
{
    private static CoroutineMgr instance;
    //单例模式
    public static CoroutineMgr Instance => instance;

    //申明存储 迭代器函数对象的 容器 用于 一会继续执行,
    //外面可能不断地在进行开启协程MyStartCoroutine,用这个存储
    private List<YieldReturnTime> list = new List<YieldReturnTime>();
	
    // Start is called before the first frame update
    void Awake()
    {
        instance = this;
    }

    public void MyStartCoroutine(IEnumerator ie)
    {
        //来进行 分步走 分时间执行的逻辑

        //传入一个 迭代器函数返回的结构 那么应该一来就执行它
        //一来就先执行第一步 执行完了 如果返现 返回的true 证明 后面还有步骤
        if(ie.MoveNext())
        {
            //判断 如果yield return返回的是 数字 是一个int类型 那就证明 是需要等待n秒继续执行
            if(ie.Current is int)
            {
                //按思路 应该把 这个迭代器函数 和它下一次执行的时间点 记录下来
                //然后不停检测 时间 是否到达了 下一次执行的 时间点 然后就继续执行它
                YieldReturnTime y = new YieldReturnTime();
                //记录迭代器接口
                y.ie = ie;
                //记录时间
                y.time = Time.time + (int)ie.Current;
                //把记录的信息 记录到数据容器当中 因为可能有多个协程函数 开启 所以 用一个 list来存储
                list.Add(y);
            }
        }
    }

    // 每帧都会进行查找是否应该继续,注意.ie.MoveNext()是从当前的索引开始的
    void Update()
    {
        //为了避免在循环的时候 从列表里面移除内容 我们可以倒着遍历
        for (int i = list.Count - 1; i >= 0; i--)
        {
            //判断 当前该迭代器函数 是否到了下一次要执行的时间
            //如果到了 就需要执行下一步了
            if( list[i].time <= Time.time  )
            {
                if(list[i].ie.MoveNext()//MoveNext会执行函数中内容遇到 yield return为止的逻辑,
                {
                    //如果是true 那还需要对该迭代器函数 进行处理
                    //如果是 int类型 证明是按秒等待
                    if(list[i].ie.Current is int)
                    {
                        list[i].time = Time.time + (int)list[i].ie.Current;
                    }
                    else
                    {
                        //该list 只是存储 处理时间相关 等待逻辑的 迭代器函数的 
                        //如果是别的类型 就不应该 存在这个list中 应该根据类型把它放入别的容器中
                        list.RemoveAt(i);
                    }
                }
                else
                {
                    //后面已经没有可以等待和执行的了 证明已经执行完毕了逻辑
                    list.RemoveAt(i);
                }
            }
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/90188
推荐阅读
相关标签
  

闽ICP备14008679号