首页 文章资讯内容详情

Golang 并发简介

2026-06-01 4 花语

本文内容纲要:

-并发概要 -协程介绍 -golang并发 -实现方式 -简单示例 -并发时的缓冲 -并发时的超时

并发概要

随着多核CPU的普及,为了更快的处理任务,出现了各种并发编程的模型,主要有以下几种:

模型名称 优点 缺点 多进程 简单,隔离性好,进程间几乎无影响 开销最大 多线程 目前使用最多的方式,开销比多进程小 高并发模式下,效率会有影响 异步 相比多线程而言,可以减少线程的数量 编码要求高,需要对流程分割合理 协程 用户态线程,不需要操作系统来调度,所以轻量,开销极小 需要语言支持

协程介绍

协程是个抽象的概念,可以映射到到操作系统层面的进程,线程等概念.

由于协程是用户态的线程,不用操作系统来调度,所以不受操作系统的限制,可以轻松的创建百万个,因此也被称为"轻量级线程".

在golang中,协程不是由库实现的,而是受语言级别支持的,因此,在golang中,使用协程非常方便.

下面通过例子演示在golang中,如何使用协程来完成并发操作.

golang并发

实现方式

golang中,通过go关键字可以非常简单的启动一个协程,几乎没有什么学习成本.

当然并发编程中固有的业务上的困难依然存在(比如并发时的同步,超时等),但是golang在语言级别给我们提供了优雅简洁的解决这些问题的途径.

理解了golang中协程的使用,会给我们写并发程序时带来极大的便利.

首先以一个简单的例子开始golang的并发编程. packagemain import( "fmt" "time" ) funcmain(){ fori:=0;i<10;i++{ gosum(i,i+10) } time.Sleep(time.Second*5) } funcsum(start,endint)int{ varsumint=0 fori:=start;i<end;i++{ sum+=i } fmt.Printf("Sumfrom%dto%dis%d\n",start,end,sum) returnsum }

执行结果如下:(同时启动10个协程做累加运算,10个协程的执行顺序可能会不一样)

$gorunmain.go Sumfrom0to10is45 Sumfrom6to16is105 Sumfrom7to17is115 Sumfrom2to12is65 Sumfrom8to18is125 Sumfrom1to11is55 Sumfrom9to19is135 Sumfrom3to13is75 Sumfrom4to14is85 Sumfrom5to15is95

通过go关键字启动协程之后,主进程并不会等待协程的执行,而是继续执行直至结束.

本例中,如果没有time.Sleep(time.Second*5)等待5秒的话,那么主进程不会等待那10个协程的运行结果,直接就结束了.

主进程结束也会导致那10个协程的执行中断,所以,如果去掉time.Sleep这行代码,可能屏幕上什么显示也没有.

简单示例

实际使用协程时,我们一般会等待所有协程执行完成(或者超时)后,才会结束主进程,但是不会用time.Sleep这种方式,

因为主进程并不知道协程什么时候会结束,没法设置等待时间.

这时,就看出golang中的channel机制所带来的好处了.下面用channel来改造上面的time.Sleep

packagemain import"fmt" funcmain(){ varch=make(chanstring) fori:=0;i<10;i++{ gosum(i,i+10,ch) } fori:=0;i<10;i++{ fmt.Print(<-ch) } } funcsum(start,endint,chchanstring){ varsumint=0 fori:=start;i<end;i++{ sum+=i } ch<-fmt.Sprintf("Sumfrom%dto%dis%d\n",start,end,sum) }

程序执行结果和上面一样,因为是并发的缘故,可能输出的sum顺序可能会不一样.

$gorunmain.go Sumfrom9to19is135 Sumfrom0to10is45 Sumfrom5to15is95 Sumfrom6to16is105 Sumfrom7to17is115 Sumfrom2to12is65 Sumfrom8to18is125 Sumfrom3to13is75 Sumfrom1to11is55 Sumfrom4to14is85

golang的chan可以是任意类型的,上面的例子中定义的是string型.

从上面的程序可以看出,往chan中写入数据之后,协程会阻塞在那里,直到在某个地方将chan中的值读取出来,协程才会继续运行下去.

上面的例子中,我们启动了10个协程,每个协程都往chan中写入了一个字符串,然后在main函数中,依次读取chan中的字符串,并在屏幕上打印出来.

通过golang中的chan,不仅实现了主进程和协程之间的通信,而且不用像time.Sleep那样不可控(因为你不知道要Sleep多长时间).

并发时的缓冲

上面的例子中,所有协程使用的是同一个chan,chan的容量默认只有1,当某个协程向chan中写入数据时,其他协程再次向chan中写入数据时,其实是阻塞的.

等到chan中的数据被读出之后,才会再次让某个其他协程写入,因为每个协程都执行的非常快,所以看不出来.

改造下上面的例子,加入些Sleep代码,延长每个协程的执行时间,我们就可以看出问题,代码如下:

packagemain import( "fmt" "time" ) funcmain(){ varch=make(chanstring) fori:=0;i<5;i++{ gosum(i,i+10,ch) } fori:=0;i<10;i++{ time.Sleep(time.Second*1) fmt.Print(<-ch) } } funcsum(start,endint,chchanstring)int{ ch<-fmt.Sprintf("Sumfrom%dto%disstartingat%s\n",start,end,time.Now().String()) varsumint=0 fori:=start;i<end;i++{ sum+=i } time.Sleep(time.Second*10) ch<-fmt.Sprintf("Sumfrom%dto%dis%dat%s\n",start,end,sum,time.Now().String()) returnsum }

执行结果如下:

$gorunmain.go Sumfrom4to14isstartingat2015-10-1313:59:56.025633342+0800CST Sumfrom3to13isstartingat2015-10-1313:59:56.025608644+0800CST Sumfrom0to10isstartingat2015-10-1313:59:56.025508327+0800CST Sumfrom2to12isstartingat2015-10-1313:59:56.025574486+0800CST Sumfrom1to11isstartingat2015-10-1313:59:56.025593711+0800CST Sumfrom4to14is85at2015-10-1314:00:07.030611465+0800CST Sumfrom3to13is75at2015-10-1314:00:08.031926629+0800CST Sumfrom0to10is45at2015-10-1314:00:09.036724803+0800CST Sumfrom2to12is65at2015-10-1314:00:10.038125044+0800CST Sumfrom1to11is55at2015-10-1314:00:11.040366206+0800CST

为了演示chan的阻塞情况,上面的代码中特意加了一些time.Sleep函数.

每个执行Sum函数的协程都会运行10秒 main函数中每隔1秒读一次chan中的数据

从打印结果我们可以看出,所有协程几乎是同一时间开始的,说明了协程确实是并发的.

其中,最快的协程(Sumfrom4to14…)执行了11秒左右,为什么是11秒左右呢?

说明它阻塞在了Sum函数中的第一行上,等了1秒之后,main函数开始读出chan中数据后才继续运行.

它自身运行需要10秒,加上等待的1秒,正好11秒左右.

最慢的协程执行了15秒左右,这个也很好理解,总共启动了5个协程,main函数每隔1秒读出一次chan,最慢的协程等待了5秒,

再加上自身执行了10秒,所以一共15秒左右.

到这里,我们很自然会想到能否增加chan的容量,从而使得每个协程尽快执行,完成自己的操作,而不用等待,消除由于main函数的处理所带来的瓶颈呢?

答案是当然可以,而且在golang中实现还很简单,只要在创建chan时,指定chan的容量就行. packagemain import( "fmt" "time" ) funcmain(){ varch=make(chanstring,10) fori:=0;i<5;i++{ gosum(i,i+10,ch) } fori:=0;i<10;i++{ time.Sleep(time.Second*1) fmt.Print(<-ch) } } funcsum(start,endint,chchanstring)int{ ch<-fmt.Sprintf("Sumfrom%dto%disstartingat%s\n",start,end,time.Now().String()) varsumint=0 fori:=start;i<end;i++{ sum+=i } time.Sleep(time.Second*10) ch<-fmt.Sprintf("Sumfrom%dto%dis%dat%s\n",start,end,sum,time.Now().String()) returnsum }

执行结果如下:

$gorunmain.go Sumfrom0to10isstartingat2015-10-1314:22:14.64534265+0800CST Sumfrom2to12isstartingat2015-10-1314:22:14.645382961+0800CST Sumfrom3to13isstartingat2015-10-1314:22:14.645408947+0800CST Sumfrom4to14isstartingat2015-10-1314:22:14.645417257+0800CST Sumfrom1to11isstartingat2015-10-1314:22:14.645427028+0800CST Sumfrom1to11is55at2015-10-1314:22:24.6461138+0800CST Sumfrom3to13is75at2015-10-1314:22:24.646330223+0800CST Sumfrom2to12is65at2015-10-1314:22:24.646325521+0800CST Sumfrom4to14is85at2015-10-1314:22:24.646343061+0800CST Sumfrom0to10is45at2015-10-1314:22:24.64634674+0800CST

从执行结果可以看出,所有协程几乎都是10秒完成的.所以在使用协程时,记住可以通过使用缓存来进一步提高并发性.

并发时的超时

并发编程,由于不能确保每个协程都能及时响应,有时候协程长时间没有响应,主进程不可能一直等待,这时候就需要超时机制.

在golang中,实现超时机制也很简单. packagemain import( "fmt" "time" ) funcmain(){ varch=make(chanstring,1) vartimeout=make(chanbool,1) gosum(1,10,ch) gofunc(){ time.Sleep(time.Second*5)//5秒超时 timeout<-true }() select{ casesum:=<-ch: fmt.Print(sum) case<-timeout: fmt.Println("Sorry,TIMEOUT!") } } funcsum(start,endint,chchanstring)int{ varsumint=0 fori:=start;i<end;i++{ sum+=i } time.Sleep(time.Second*10) ch<-fmt.Sprintf("Sumfrom%dto%dis%d\n",start,end,sum) returnsum }

通过一个匿名函数来控制超时,然后同时启动计算sum的协程和timeout协程,在select中看谁先结束,

如果timeout结束后,计算sum的协程还没有结束的话,就会进入超时处理.

上例中,timeout只有5秒,sum协程会执行10秒,所以执行结果如下:

$gorunmain.go Sorry,TIMEOUT!

修改time.Sleep(time.Second*5)为time.Sleep(time.Second*15)的话,就会看到sum协程的执行结果

本文内容总结:并发概要,协程介绍,golang并发,实现方式,简单示例,并发时的缓冲,并发时的超时,

原文链接:https://www.cnblogs.com/wang_yb/p/4874668.html