当前位置: 首页 > 后端技术 > PHP

【精选】Swoole协程和Go协程有什么区别?这篇文章告诉你!

时间:2023-03-29 15:26:34 PHP

1.什么是进程、线程、协程进程?进程是应用程序的启动实例。例如:打开一个软件就是启动一个进程。一个进程拥有代码和打开的文件资源、数据资源以及独立的内存空间。什么是线程?线程属于进程,是程序的执行者。一个进程至少包含一个主线程,也可以有多个子线程。线程有两种调度策略,一种是:分时调度,一种是:抢占式调度。什么是协程?协程是轻量级线程。协程的创建、切换、挂起、销毁都是内存操作,消耗很低。1协程属于线程,协程是在线程中执行的。2协程的调度是用户手动切换的,所以也叫用户空间线程。3协程的调度策略是:协同调度。为什么要使用协程目前,主流语言基本上都会选择多线程作为并发设施。线程相关的概念是抢占式多任务,协程相关的概念是协作式多任务。其实不管是进程还是线程,每次阻塞和切换都需要落入系统调用(systemcall),先让CPU运行操作系统的调度器,然后调度器决定哪个进程(线程))跑步。而且,由于抢占式调度的执行顺序不确定,使用线程时需要非常小心地处理同步问题,而协程则完全没有这个问题(事件驱动和异步程序也有同样的优点)。因为用户写的是协程的调度逻辑,对于我们的CPU来说,协程其实就是一个单线程,所以CPU不需要考虑如何调度和切换上下文,这样就节省了CPU的切换开销,所以协程在某种程度上,它比多线程更好。协程相对于多线程的优势多线程编程比较难,因为调度器可以随时中断线程,必须记住保留锁来保护程序的重要部分,防止多线程在执行过程中被打断。默认情况下,协程将被完全保护以防止中断。我们必须显示要运行的程序其余部分的输出。对于协程来说,不需要一直保持锁,而是多线程之间的同步操作,协程本身就会被同步,因为任何时候,只有一个协程在运行。总结一下几点:不需要系统内核的上下文切换,减少开销;无需原子操作加锁和同步开销,无需担心资源共享;单线程可以实现高并发,即使单核CPU支持上万个协程也不成问题,所以非常适合高并发处理,尤其是在网络爬虫中。2.Swoole协程Swoole的协程客户端必须在协程上下文中使用。//第一种情况:Request回调本身就是一个协程环境$server->on('Request',function($request,$response){//创建一个Mysql协程客户端$mysql=newSwooleCoroutineMySQL();$mysql->connect([]);$mysql->query();});//第二种情况:WorkerStart回调不是协程环境$server->on('WorkerStart',function(){//需要声明一个使用协程客户端之前的协程环境go(function(){//创建Mysql协程客户端$mysql=newSwooleCoroutineMySQL();$mysql->connect([]);$mysql->query();});});Swoole的协程是基于单线程的,无法发挥多核CPU的优势,同时只能调度一个。//启动4个协程$n=4;for($i=0;$i<$n;$i++){go(function()use($i){//模拟IOwaitCo::sleep(1);echomicrotime(true).":hello$i".PHP_EOL;});};echo"hellomainn";//每次结果都一样$phptest.phphellomain1558749158.0913:hello01558749158.0915:hello31558749158.0915:hello21558749158.0915:hello1Swoole协程使用示例及详解//创建Http服务$server=newSwooleHttpServer('127.0.0.1',quecallbackonRest/BASEfunctioncall);/SWOOLE_BA事件调用,the底层会调用C函数coro_create创建一个协程,//同时保存该时间点的CPU寄存器状态和ZendVM堆栈信息。$server->on('Request',function($request,$response){//创建Mysql协程客户端$mysql=newSwooleCoroutineMySQL();//调用mysql->connect时发生IO操作,底层C会调用coro_save函数保存当前协程的状态,//包括ZendVM上下文和协程描述的信息,调用coro_yield放弃程序控制,当前请求会被挂起。//当协程放弃控制后,会继续进入EventLoop处理其他事件,Swoole会继续处理来自其他客户端的Request。$res=$mysql->connect(['host'=>'127.0.0.1','user'=>'root','password'=>'root','database'=>'test']);//IO事件完成后,MySQL连接成功或失败,底层调用C函数coro_resume恢复对应协程,恢复ZendVM上下文,继续向下执行PHP代码。if($res==false){$response->end("MySQL连接失败");返回;}//mysql->query的执行过程和mysql->connect的执行过程一样,也会进行一次协程切换调度$ret=$mysql->query('showtables',2);//所有操作完成后,调用end方法返回结果并销毁这个协程。$response->end('swooleresponseisok,result='.var_export($ret,true));});//启动服务$server->start();3.Go的协程goroutine1goroutine是轻量化线程,Go语言从语言层面支持原生协程。2与线程相比,Go协程的开销非常小。3Go协程的栈开销只有2KB,可以根据程序的需要增减,而且线程必须指定栈的大小,栈的大小是固定的。4个goroutines是通过GPM调度模型实现的。M:表示内核级线程,一个M就是一个线程,goroutine运行在M之上。G:表示一个goroutine,它有自己的栈。P:全称是Processor,处理器。它主要用来执行goroutine,它还维护着一个goroutine队列。Go从运行时、系统调用等多个方面对goroutine的调度进行了封装和处理。当遇到长时间执行或系统调用时,它会主动转移当前协程的CPU,让其他协程调度执行。Go语言的native层支持协程层,不需要声明协程环境。packagemainimport"fmt"funcmain(){//可以直接通过Go关键字启动协程。gofunc(){fmt.Println("HelloGo!")}()}Go协程基于多线程,可以利用多核CPU。可能有多个协程同时执行。packagemainimport("fmt""time")funcmain(){//设置这个参数模拟单线程,和Swoole的协程比较//如果这个参数设置为1,每次输出都是一样的。//runtime.GOMAXPROCS(1)//启动4个协程variint64fori=0;我<4;i++{gofunc(iint64){//模拟IO等待time.Sleep(1*time.Second)fmt.Printf("hello%dn",i)}(i)}fmt.Println("hellomain")//等待其他协程执行完毕,如果没有,//main执行完退出后,其他协程也会陆续退出。time.Sleep(10*time.Second)}//第一次输出的结果$goruntest.gohellomainhello2hello1hello0hello3//第二次输出的结果$goruntest.gohellomainhello2hello0hello3hello1//以此类推,每次输出结果都不一样_"github.com/go-sql-driver/mysql"funcmain(){dsn:=fmt.Sprintf("%v:%v@(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local","root","root","127.0.0.1","3306","fastadmin",)db,错误:=gorm.Open("mysql",dsn)iferr!=nil{fmt.Printf("mysql连接失败,错误:(%v)",err.Error())return}db.DB().SetMaxIdleConns(10)//设置连接池db.DB().SetMaxOpenConns(100)//设置数据库最大连接数db.DB().SetConnMaxLifetime(time.Second*7)http.HandleFunc("/test",func(writerhttp.ResponseWriter,request*http.Request){//http请求在协程中处理//Go源码中src/net/http/server.go:2851行`goc.serve(ctx)`为每个请求启动一个协程varnamestringrow:=db.Table("fa_auth_rule").Where("id=?",1)。Select("name").Row()err=row.Scan(&name)iferr!=nil{fmt.Printf("error:%v",err)return}fmt.Printf("name:%vn",name)})http.ListenAndServe("0.0.0.0:8001",nil)}4.案例分析背景:在我们的积分策略服务系统中,使用了mongodb存储,但是swoole没有提供mongodb协程客户端,所以这个在这种场景下,在连接和操作Mongodb时会发生同步阻塞,无法发生协程切换,导致整个进程被阻塞。在这段时间里,进程将无法再处理新的请求,大大降低了系统的并发度。使用同步的mongodb客户端$server->on('Request',function($request,$response){//swoole没有提供协程客户端,只能使用同步客户端//这种情况下,流程阻塞,无法切换协程$m=newMongoClient();//连接到mongodb$db=$m->test;//选择一个数据库$collection=$db->runoob;//选择集合//更新文档$collection->update(array("title"=>"MongoDB"),array('$set'=>array("title"=>"Swoole")));$cursor=$collection->find();foreach($cursoras$document){echo$document["title"]."n";}}}通过Server->taskCo$server->on('Task',function(swoole_server$serv,$task_id,$worker_id,$data){$m=newMongoClient();//连接到mongodb$db=$m->test;//选择一个数据库$collection=$db->runoob;//选择集合//更新文档$collection->update(array("title"=>"MongoDB"),array('$set'=>array("title"=>"Swoole")));$cursor=$collection->find();foreach($cursoras$document){$data=$documentnt[“标题”];}return$data;});$server->on('Request',function($request,$response)use($server){//通过$server->taskCo()put对mongodb的操作,postittotheasynchronoustask//post到异步任务后,会发生协程切换,可以继续处理其他请求,提供并发能力$tasks[]="helloworld";$result=$server->taskCo($tasks,0.5);$response->end('测试结束,结果:'.var_export($result,true));});以上两种使用方法是Swoole中常用的方法。那么在Go中我们如何处理这个同步问题呢?其实在Go语言中大可不必担心这个问题。正如我们之前所说,Go在语言层面已经支持协程。只要有IO操作发生,网络请求就会切换协程。这也是Go语言天生就支持高并发的原因。packagemainimport("fmt""gopkg.in/mgo.v2""net/http")funcmain(){http.HandleFunc("/test",func(writerhttp.ResponseWriter,request*http.Request){session,err:=mgo.Dial("127.0.0.1:27017")如果err!=nil{fmt.Printf("Error:%vn",err)return}session.SetMode(mgo.Monotonic,true)c:=session.DB("test").C("runoob")fmt.Printf("Connect%vn",c)})http.ListenAndServe("0.0.0.0:8001",nil)}并行:同一时刻,相同一个CPU只能执行同一个任务,同时执行多个任务需要多个CPU。并发性:CPU切换时间任务非常快,你会感觉有很多任务在同时执行。5.协程CPU密集型场景的调度上面我们说的都是基于IO密集型场景的调度。那么如果是CPU密集型场景,应该怎么处理呢?在Swoolev4.3.2中,已经支持协程CPU密集型场景的调度。为支持CPU密集型调度,需要在编译时加入编译选项--enable-scheduler-tick,启用tick调度器。其次,我们还需要手动声明declare(tick=N)语法函数来实现协程调度。$max_msec,]);$s=microtime(1);echo"startn";$flag=1;go(function()use(&$flag,$max_msec){echo"coro1开始循环$max_msecmsecn";$i=0;while($flag){$i++;}echo"coro1canexitn";});$t=microtime(1);$u=$t-$s;echo"shedule使用时间".round($u*1000,5)."msn";go(function()use(&$flag){echo"coro2setflag=false";$flag=false;});echo"endn";//输出startcoro1开始循环10msecschedule使用时间10.2849mscoro2setflag=falseendcoro1即可exitGo执行CPU密集型操作时,可能导致协程无法抢占CPU而永远挂起。这时需要显示调用代码runtime.Gosched()挂起当前协程,让出CPU给其他协程。packagemainimport("fmt""time")funcmain(){//如果设置了单线程,第一个协程不能放弃时间片//第二个协程一直拿不到时间片,阻塞等待.//runtime.GOMAXPROCS(1)flag:=truegofunc(){fmt.Printf("coroutineonestartn")i:=0forflag{i++//如果加上这行代码,协程可以让thetimeSlice//这是一个特例,因为fmt.Printf是一个内联函数//fmt.Printf("i:%dn",i)}fmt.Printf("coroutineoneexitn")}()gofunc(){fmt.Printf("协程二开始n")flag=falsefmt.Printf("协程二退出n")}()time.Sleep(5*time.Second)fmt.Printf("endn")}//输出结果协程一启动协程二启动协程二退出协程一退出结束注:time.sleep()模拟IO操作,对于i++模拟CPU密集型操作。总结协程是开销很小的轻量级线程。Swoole的协程客户端需要在协程上下文中使用。在Swoolev4.3.2之后,已经支持协程CPU密集型场景调度。Go语言级别已经完全支持协程。