Go语言程序设计及实例 课件 第4章 Go语言并发编程_第1页
Go语言程序设计及实例 课件 第4章 Go语言并发编程_第2页
Go语言程序设计及实例 课件 第4章 Go语言并发编程_第3页
Go语言程序设计及实例 课件 第4章 Go语言并发编程_第4页
Go语言程序设计及实例 课件 第4章 Go语言并发编程_第5页
已阅读5页,还剩35页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第4章Go语言并发编程并发编程概述01基本概念1.并行与并发2.进程、线程与协程基本概念1.并行与并发计算机程序的执行在底层有并行与并发两种基本方式。(1)并行(Parallel)并行是指在同一时刻有多条指令在多个处理器(核心)上执行。现在的计算机通常都是多核CPU,当一个CPU核心在执行一个进程时,另一个核心可以执行另一个进程,两个进程互不抢占CPU资源,同时运行,这种方式称为“并行”。(2)并发(Concurrent)并发是指同一时刻CPU只能执行一条指令,而多个进程的指令被快速地轮换执行。这样从宏观上看多个进程也是同时在运行,但从微观看,它们并不是“同时”的,只是把时间分成若干片段,交替使用而已。基本概念2.进程、线程与协程按照程序被执行单元(粒度)的大小,可分为进程、线程和协程三个级别。(1)进程(Process)进程是程序在某个数据集上的一次运行活动,是系统进行资源分配和调度的基本单位,也是操作系统结构的基础。每个进程都有自己独立的内存空间,隔离性好、健壮性高,但是进程是“重量级”的,现代操作系统虽然能够对进程进行调度,可是当进程数大于CPU核心数的时候,并发调度就要进行进程间的切换,这种切换需要保存上下文、恢复堆栈等,操作开销比较大,频繁地切换也很耗时;此外,单进程的程序最多使用一个CPU工作,是无法有效利用机器资源的,由于CPU运行时不可避免要与外部设备通信,这导致单个进程经常会被阻塞,包括I/O等待、缺页中断、等待网络等,尤其对于I/O密集型的程序,外部I/O速度慢和阻塞导致CPU的大部分时间都被白白浪费了。基本概念(2)线程(Thread)线程有时也被称为“轻量级”进程,它是进程中的一个实体,是被系统独立调度和分派的基本单位。线程拥有自己独立的栈但不拥有系统资源,而是与同属一个进程的其他线程共享该进程所拥有的系统资源。一个标准的线程由线程ID、当前指令指针、寄存器集合和堆栈组成。线程一般也由操作系统调度,在同一个任务的进程中操作系统会启动多个内核线程进行处理,线程切换代价小,并发性能较之单进程程序要好得多,但缺点是线程之间共享内存空间极易造成数据访问混乱,一个线程误操作内存就可能危及整个进程,程序健壮性不高。(3)协程(goroutine)多核时代的程序对并发处理的要求越来越高,但是不能无限制地增加系统线程数,因线程数过多会导致操作系统的调度开销变大,每个线程被分配的运行时间片减少,单个线程速度降低使得仅靠增加线程数已不能进一步提升性能。为了不让线程数无限地膨胀,于是就有了协程的概念。协程是一种用户态的轻量级线程,它本质上其实是一种特殊的函数,但这种函数可以像线程那样在某个地方被“挂起”,并且可以重新在挂起处继续往下运行,每个协程拥有自己的寄存器上下文和栈。基本概念正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程,简单来说,在一个线程内可以有多个这样的特殊函数在运行,进程、线程和协程之间的从属关系如图。02Go语言的并发模型1.CSP简介2.MPG调度模型Go语言的并发模型1.CSP简介CSP(CommunicatingSequentialProcesses,通信顺序进程)是由计算机科学家托尼·霍尔(TonyHoare)于1977年提出的一种并发系统消息通信模型。其最基本的思想是:将并发系统抽象为“通道”和“并发实体”两部分,通道用来传递消息,并发实体用于执行。该模型具有如下特点:(1)并发实体是独立的,它们之间没有共享的内存空间。(2)通道与并发实体之间也相互独立,没有从属关系。(3)并发实体之间的数据交换通过通道(而非共享内存)实现,无论在通道中放数据还是从通道中取数据,都会导致并发实体的阻塞,直到通道中的数据被取出或者通道中被放入新的数据,并发实体以这种方式实现同步。(4)消息的发送和接收有严格的时序限制。在这个模型中,发送和接收消息的并发实体并不“知道”对方具体是谁,它们之间是完全解耦的,此外,通道与并发实体也实现了解耦,可以独立地创建通道和存取数据,并在不同的并发实体中传递使用。Go语言的并发模型2.MPG调度模型(1)M(机器)一个M对应于操作系统的一个内核线程,是操作系统层面调度和执行的实体。M处于整个系统的最底层,不停地被唤醒或创建,然后执行。M是内核空间的线程在用户空间Go进程中的映射,仅负责执行。(2)P(处理器)P可以理解为一种执行用户代码的逻辑(虚拟)处理器,它包含了M执行G所必需的资源,但并不是运行实体,而是一个用于管理的数据结构,负责调度G以控制代码的并行度。一个运行中的M只能绑定一个P,P持有G的队列,M通过P来间接地实现对G的调度。(3)G(协程)G是Go语言运行时对协程的抽象描述,其中存放了并发执行体的元信息,包括入口函数、堆栈、上下文等,G的创建、休眠、恢复、停止等都受到Go运行时的管理。Go语言的并发模型实际运行程序过程中,M和P的组合一起为G提供有效的运行环境,多个可执行的G顺序地排成一个队列挂在某个P上面等待调度执行,如图。Go语言的并发模型可见,Go语言的并发模型设计是非常巧妙的,它有以下几个主要的优点。(1)使用比线程粒度更小的协程(G)作为并发调度的基本单元,所有的协程都直接由语言本身的运行时在用户空间中调度,不像传统语言使用线程那样依赖于内核空间的操作系统,这就避免了用户态到内核态频繁切换的成本。(2)将操作系统内核的线程映射为用户空间一个个虚拟的机器(M),就可以根据程序并发性能的需要动态地创建数量合适的M,有效解决了以往线程模式下由于线程数过多造成单个线程运行时间片不足或数量过少并发度又不够的两难问题。(3)协程相比线程占用更小的栈空间,因而允许用户创建成千上万的协程实例,这一点对于实现并发规模很大、吞吐量要求很高的互联网应用尤为重要。第4章Go语言并发编程协程与通道01协程及其特性1.有名函数创建协程2.匿名函数创建协程3.协程的特性4.协程运行与退出协程及其特性1.有名函数创建协程有名函数创建协程的代码形式为:func函数名([形参列表]){ //定义一个函数

函数体}

funcmain(){ ... go函数名([实参列表]) //创建一个协程 ...}【实例4.1】定义一个有名函数,功能为输出指定范围内的素数,创建一个协程启动函数运行。程序代码如下(gofunc01.go):运行结果如图。协程及其特性2.匿名函数创建协程匿名函数创建协程的代码形式为:funcmain(){ ... gofunc([形参列表]){ //匿名函数

函数体 }([实参列表]) //启动协程 ...}说明:这种方式直接在go关键字后面写出匿名函数体,传入实参列表(如果有的话)就可以启动协程来执行函数体的代码。协程及其特性【实例4.2】写一个匿名函数,功能同样为输出指定范围内的素数,启动协程运行它。程序代码如下(gofunc02.go):packagemainimport( "fmt" "time")funcisPrime(nint)bool{ //判断素数的函数 ...//代码同上例,略。}

funcmain(){ vars,e=1,100 gofunc(start,endint){ fmt.Println(start,"~",end,"之间的素数有:") fori:=start;i<=end;i++{ ifisPrime(i){ fmt.Print(i,"") } } }(s,e) //传入实参启动协程 time.Sleep(1*time.Second)}协程及其特性3.协程的特性协程具有如下一些重要的特性。(1)独立于进程一个协程一旦被创建,它就脱离所在的进程(即主程序)独自运行,不再受进程的控制,如果应用程序(进程)先于协程结束,它将无法获得协程的运行结果。例如,在上面两实例的入口函数中,将启动协程之后延时等待的语句注释掉,如下://【实例4.1】入口函数funcmain(){ goprintPrime(1,100) //创建并启动协程 //time.Sleep(1*time.Second) //注释掉}//【实例4.2】入口函数funcmain(){ vars,e=1,100 gofunc(start,endint){ ...//匿名函数体,代码略。 }(s,e) //传入实参启动协程 //time.Sleep(1*time.Second) //注释掉}协程及其特性(2)独立于启动它的函数协程除了由主程序启动,也可以在用户自定义的任何函数中启动,并且所开启的协程也独立于启动它的函数,如果函数在启动协程后不等待而先行返回,同样也得不到协程的运行结果。【实例4.3】通过函数启动协程,输出指定范围内的素数。程序代码如下(gofunc03.go):协程及其特性(3)独立于启动它的协程运行中的协程又可以创建并开启新的协程,新建的协程同样也会脱离开它的启动者。【实例4.4】通过协程启动新协程,由新协程来输出指定范围内的素数。程序代码如下(gofunc04.go):packagemainimport( ...//导入库,代码略)funcisPrime(nint)bool{ //判断素数的函数 ...//代码同上例,略。}funcprintPrime(start,endint){ //输出素数的函数 ...//代码同前,略。}funcstartGor(){ //启动新协程的函数 goprintPrime(1,100)}

funcmain(){ gostartGor() //用startGor函数再创建一个协程 time.Sleep(1*time.Second)}协程及其特性(4)各协程之间相互独立Go系统中的所有(无论是由主程序、函数还是由其他协程启动的)协程,地位都是平等的,它们之间相互独立。【实例4.5】用多个协程分别输出不同范围内的素数。程序代码如下(gofunc05.go):运行结果如图。可以看到,每次运行输出的素数顺序都不一样,且来自同一个协程的输出内容也并不总是排在一起,比如第3次运行的素数23是第2个协程输出的,但它后面跟的却是来自第1个协程的素数2,这不仅表明协程间是独立运行的,而且也反映了各协程并发执行轮占CPU所导致的输出不连续现象。协程及其特性4.协程运行与退出Go程序在初始启动时就会为入口(main)函数创建一个协程,这个协程会一直运行到整个应用程序(进程)结束时才退出,而在程序运行期间由代码中go关键字创建并开启的其他协程,则会在执行完自己函数的任务代码后自动退出。Go语言运行时的runtime包提供了一个NumGoroutine函数供用户获取当前正在运行的协程数,而协程自身也可在任何时候通过调用其中的Goexit函数主动退出。【实例4.6】寻找第一个大于10000的素数。程序代码如下(gofunc06.go):运行结果如图。02通道与协程间通信1.通道的创建和使用2.通道控制与遍历3.带缓冲的通道4.多通道处理通道与协程间通信1.通道的创建和使用(1)通道的创建在Go语言中,通道也是一种特殊的类型,以chan关键字声明,用make函数创建一个通道,语句格式如下:通道名:=make(chan类型)其中,“通道名”是一个实例句柄,可在程序代码中引用它来对通道进行访问和操作;通道中存放的元素可以是各种类型的数据,在创建时通过“类型”明确指定。(2)通道的读写向通道中写入元素(发送数据)使用“<-”操作符,格式如下:通道名<-元素值这里的“元素值”可以是变量、常量、表达式,也可以是函数返回值,但值的数据类型必须与创建通道时所指定的类型相一致。通道与协程间通信从通道中读取元素(接收数据)同样也用“<-”操作符,通常有下面两种写法:①用变量接收变量名:=<-通道名将从通道读取的元素赋给一个接收变量,然后在程序中使用,如果通道中没有数据,该语句将被阻塞,直到有其他协程向通道中写入了数据为止。例如:funcmain(){ ch:=make(chanint) //创建一个整型元素的通道 gofunc(){ varnint fmt.Print("输入一个自然数:") fmt.Scanln(&n) ch<-n //将用户输入的自然数写入通道 }() num:=<-ch //用一个变量接收通道中的元素 fmt.Println("输入的数是",num)}通道与协程间通信这段程序通过一个匿名函数的协程向通道中写入用户输入的数,主程序协程用一个变量从通道接收数据,程序运行后,提示用户输入,当用户尚未输入时,通道中没有数据,程序会一直阻塞在语句“num:=<-ch”上等待,直到读取到数后才输出显示,过程如图。②直接读取<-通道名随时接收通道中的数据用于计算或输出,此方式也会发生阻塞,还可用于控制协程间的并发同步(忽略掉数据内容)。通道与协程间通信(3)通道的使用在并发编程中若使用通道,通常将通道的发送(写)和接收(读)操作分别放在两个不同的协程中进行。将通道作为协程函数的参数之一,主程序创建通道实例,并以其作为实参传入开启的协程,在协程代码中对通道进行写操作,然后在另一个协程内或回到主程序中对通道进行读操作。【实例4.7】用协程寻找第一个大于100的素数,并通过通道传递给主程序,输出结果。程序代码如下(channel01.go):运行结果如图。通道与协程间通信2.通道控制与遍历当协程之间需要传递的数据不止一项时,可以往通道中写入多个元素,然后遍历读取,用for-range语句,形式如下:for数据项:=range通道名{ ...//对数据项进行处理的代码}但要特别注意:在这种情况下需要对通道加以控制,通常由写通道的协程在发送完全部数据项后关闭通道,只有被关闭的通道才能正常接受for-range语句的遍历操作,否则,即使数据已全部读完,但由于通道的阻塞特性,读取协程并不能“确知”对方是否还有数据要发,于是一直阻塞在通道上无限期等待,会导致死锁。通道与协程间通信【实例4.8】用协程将1~10范围内的素数通过通道传递给主程序输出。程序代码如下(channel02.go):运行结果如图。从接收数据前后显示的系统协程数可知,虽然通道已经写完并关闭,但只要还未被读取,写通道的协程就阻塞在上面不会退出。如果将关闭通道的“close(ch)”语句注释掉,再次运行虽然也能读到通道中的数据,但程序会发生死锁,输出如图。通道与协程间通信3.带缓冲的通道如果既想在协程之间传递数据进行交互,同时又不想协程被阻塞,可以采用带缓冲的通道,只要在创建通道时加上一个缓冲区容量参数,如下:通道名:=make(chan类型,容量)【实例4.9】用带缓冲的通道将1~10范围内的素数传递给主程序输出。程序代码如下(channel03.go):运行结果如图。通道与协程间通信4.多通道处理当系统中有多个通道在同时工作时,Go语言可使用select语句随机地任选一个通道的数据进行处理,代码形式如下:select{case通道1读/写: [<语句1>]case通道2读/写: [<语句2>]...case通道n读/写: [<语句n>][default:<语句n+1>][case是否超时: ...//超时处理 return]}通道与协程间通信【实例4.10】多通道处理演示。开两个协程,它们都有各自的通道,一个协程是等待用户将数输入再输出至通道;另一个则自动地每隔一定时间按顺序向通道输出一个数,下面演示这两个协程通道的并发处理。程序代码如下(channel04.go):启动程序后,一个协程就从0开始逐个顺序地输出数,过程中用户可随时从键盘键入任意数,通过另一协程打断它,在屏幕上显示自己输的数,但原来的协程并不会中止,而是从被打断处接着继续往下执行,在自动输出了10个数后该协程的运行放缓了,由原本每隔5秒延长至15秒输出一个数,此时用户若不尽快输入新数,一旦到了超时时间(10秒),程序就会退出。运行演示如图。第4章Go语言并发编程并发控制01读写控制读写控制当不同的协程同时读写一个数据时,如果不加控制,可能产生不正确的结果,例如,一个销售系统中有某商品进货的总量quantity、销售量sales和库存量stocks三项数据,有一个buy协程模拟购物行为,每次执行将购物数量i累加到销售量sales上,然后再在库存量stocks中减去购物数量i,如果系统中只有这一个协程,那么任何时候肯定都满足:总量=销售量+库存量但是,在多协程并发环境中却并非如此,来看下面的程序。运行结果如图。读写控制为了避免此类错误,sync包提供了读写锁RWMutex来控制并发协程对数据的访问,它其实是Go内部定义的一个结构体(struct),其上有两对(四个)方法,如下:(1)控制写操作func(*RWMutex)Lock() //加写锁func(*RWMutex)Unlock() //解写锁写锁只能有一个,如果在添加之前已经有其他的写锁,则Lock方法会阻塞,直到该写锁解除。(2)控制读操作func(*RWMutex)Rlock() //加读锁func(*RWMutex)RUnlock() //解读锁读锁可以加多个,但如果已有写锁则无法加读锁,在只有读锁或没有锁时才可以加读锁。写锁的权限高于读锁,有写锁时优先进行写锁定,加了写锁的代码段执行期间会阻止其他任何协程进来,对数据的写权限被持有写锁的协程独占,直到写操作完成。读写控制【实例4.11】用读写锁控制商品销售数据的并发读写。对上面所举例的销售数据读写程序应用读写锁,程序代码改为如下(rwmutex.go):再运行程序,结果如图。02同步等待同步等待本章之前的所有实例,为了确保主程序最后退出,都在main函数中使用“time.Sleep(...)”延时语句等待其他协程的结果,但这并不是一个好方法,对于运行时长无法估计的协程,很难预先设定一个合适的等待时间,例如,在计算机整型所表示的数值范围内寻找一个最大的素数,程序写成如下:packagemainimport( ...//导入库,代码略)funcisPrime(nint)bool{ //判断素数的函数 ...//代码同前,略。}

funcmain(){ gofunc(){ //启动寻找素数的协程 fori:=92233720

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论