关于goroutine:Go-并发模型Goroutines

前言Goroutines 是 Go 语言次要的并发原语。它看起来十分像线程,然而相比于线程它的创立和治理老本很低。Go 在运行时将 goroutine 无效地调度到实在的线程上,以避免浪费资源,因而您能够轻松地创立大量的 goroutine(例如每个申请一个 goroutine),并且您能够编写简略的,命令式的阻塞代码。因而,Go 的网络代码往往比其它语言中的等效代码更间接,更容易了解(这点从下文中的示例代码能够看出)。 对我来说,goroutine 是将 Go 这门语言与其它语言辨别开来的一个次要特色。这就是为什么大家更喜爱用 Go 来编写须要并发的代码。在上面探讨更多对于 goroutine 之前,咱们先理解一些历史,这样你就能了解为什么你想要它们了。 基于 fork 和线程 高性能服务器须要同时解决来自多个客户端的申请。有很多办法能够设计一个服务端架构来解决这个问题。最容易想到的就是让一个主过程在循环中调用 accept,而后调用 fork 来创立一个解决申请的子过程。这篇 Beej's Guide to Network Programming 指南中提到了这种形式。 在网络编程中,fork 是一个很好的模式,因为你能够专一于网络而不是服务器架构。然而它很难依照这种模式编写出一个高效的服务器,当初应该没有人在实践中应用这种形式了。 fork 同时也存在很多问题,首先第一个是老本: Linux 上的 fork 调用看起来很快,但它会将你所有的内存标记为 copy-on-write。每次写入 copy-on-write 页面都会导致一个小的页面谬误,这是一个很难测量的小提早,过程之间的上下文切换也很低廉。 另一个问题是规模: 很难在大量子过程中协调共享资源(如 CPU、内存、数据库连贯等)的应用。如果流量激增,并且创立了太多过程,那么它们将互相抢夺 CPU。然而如果限度创立的过程数量,那么在 CPU 闲暇时,大量迟缓的客户端可能会阻塞每个人的失常应用,这时应用超时机制会有所帮忙(无论服务器架构如何,超时设置都是很必要的)。 通过应用线程而不是过程,下面这些问题在肯定水平上能失去缓解。创立线程比创立过程更“便宜”,因为它共享内存和大多数其它资源。在共享地址空间中,线程之间的通信也绝对容易,应用信号量和其它构造来治理共享资源,然而,线程依然有很大的老本,如果你为每个连贯创立一个新线程,你会遇到扩大问题。与过程一样,你此时须要限度正在运行的线程的数量,以防止重大的 CPU 争用,并且须要使慢速申请超时。创立一个新线程依然须要工夫,只管能够通过应用线程池在申请之间回收线程来缓解这一问题。 无论你是应用过程还是线程,你依然有一个难以答复的问题: 你应该创立多少个线程?如果您容许有限数量的线程,客户端可能会用完所有的内存和 CPU,而流量会呈现小幅激增。如果你限度服务器的最大线程数,那么一堆迟缓的客户端就会阻塞你的服务器。尽管超时是有帮忙的,但它依然很难无效地应用你的硬件资源。 基于事件驱动 那么既然无奈轻易预测出须要多少线程,当如果尝试将申请与线程解耦时会产生什么呢?如果咱们只有一个线程专门用于利用程序逻辑(或者可能是一个小的、固定数量的线程),而后在后盾应用异步零碎调用解决所有的网络流量,会怎么样?这就是一种 事件驱动 的服务端架构。 事件驱动架构模式是围绕 select 零碎调用设计的。起初像 poll 这样的机制曾经取代了 select,然而 select 是广为人知的,它们在这里都服务于雷同的概念和目标。select 承受一个文件描述符列表(通常是套接字),并返回哪些是筹备好读写的。如果所有文件描述符都没有筹备好,则抉择阻塞,直到至多有一个筹备好。 #include <sys/select.h>#include <poll.h>int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict timeout);int poll(struct pollfd *fds, nfds_t nfds, int timeout);为了实现一个事件驱动的服务器,你须要跟踪一个 socket 和网络上被阻塞的每个申请的一些状态。在服务器上有一个繁多的主事件循环,它调用 select 来解决所有被阻塞的套接字。当 select 返回时,服务器晓得哪些申请能够进行了,因而对于每个申请,它调用利用程序逻辑中的存储状态。当应用程序须要再次应用网络时,它会将套接字连同新状态一起增加回“阻塞”池中。这里的状态能够是应用程序复原它正在做的事件所需的任何货色: 一个要回调的 closure,或者一个 Promise。 ...

July 9, 2023 · 3 min · jiezi

关于goroutine:Golang-协程线程进程-区别以及-GMP-详解

Golang 协程/线程/过程 区别详解转载请注明起源:https://janrs.com/mffp概念过程 每个过程都有本人的独立内存空间,领有本人独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至多有一个过程,一个过程至多有一个线程。过程切换只产生在内核态。 线程 线程领有本人独立的栈和共享的堆,共享堆,不共享栈,是由操作系统调度,是操作系统调度(CPU调度)执行的最小单位。对于过程和线程,都是有内核进行调度,有 CPU 工夫片的概念, 进行抢占式调度。内核由零碎内核进行调度, 零碎为了实现并发,会一直地切换线程执行, 由此会带来线程的上下文切换。 协程 协程线程一样共享堆,不共享栈,协程是由程序员在协程的代码中显示调度。协程(用户态线程)是对内核通明的, 也就是零碎齐全不晓得有协程的存在, 齐全由用户本人的程序进行调度。在栈大小调配不便,且每个协程占用的默认占用内存很小,只有 2kb ,而线程须要 8mb,相较于线程,因为协程是对内核通明的,所以栈空间大小能够按需增大减小。 并发 多线程程序在单核上运行并行 多线程程序在多核上运行 协程与线程次要区别是它将不再被内核调度,而是交给了程序本人而线程是将本人交给内核调度,所以golang中就会有调度器的存在。 详解过程在计算机中,单个 CPU 架构下,每个 CPU 同时只能运行一个工作,也就是同时只能执行一个计算。如果一个过程跑着,就把惟一一个 CPU 给齐全占住,显然是不合理的。而且很大概率上,CPU 被阻塞了,不是因为计算量大,而是因为网络阻塞。如果此时 CPU 始终被阻塞着,其余过程无奈应用,那么计算机资源就是节约了。这就呈现了多过程调用了。多过程就是指计算机系统能够同时执行多个过程,从一个过程到另外一个过程的转换是由操作系统内核治理的,个别是同时运行多个软件。 线程有了多过程,为什么还要线程?起因如下: 过程间的信息难以共享数据,父子过程并未共享内存,须要通过过程间通信(IPC),在过程间进行信息替换,性能开销较大。创立过程(个别是调用 fork 办法)的性能开销较大。在一个过程内,能够设置多个执行单元,这个执行单元都运行在过程的上下文中,共享着同样的代码和全局数据,因为是在全局共享的,就不存在像过程间信息替换的性能损耗,所以性能和效率就更高了。这个运行在过程中的执行单元就是线程。 协程官网的解释:链接:goroutines阐明 Goroutines 是使并发易于应用的一部分。 这个想法曾经存在了一段时间,就是将独立执行的函数(协程)多路复用到一组线程上。 当协程阻塞时,例如通过调用阻塞零碎调用,运行时会主动将同一操作系统线程上的其余协程挪动到不同的可运行线程,这样它们就不会被阻塞。 程序员看不到这些,这就是重点。 咱们称之为 goroutines 的后果可能十分便宜:除了堆栈的内存之外,它们的开销很小,只有几千字节。 为了使堆栈变小,Go 的运行时应用可调整大小的有界堆栈。 一个新创建的 goroutine 被赋予几千字节,这简直总是足够的。 如果不是,运行时会主动减少(和放大)用于存储堆栈的内存,从而容许许多 goroutine 存在于适度的内存中。 每个函数调用的 CPU 开销均匀约为三个便宜指令。 在同一个地址空间中创立数十万个 goroutine 是很理论的。 如果 goroutines 只是线程,系统资源会以更少的数量耗尽。 从官网的解释中能够看到,协程是通过多路复用到一组线程上,所以实质上,协程就是轻量级的线程。然而必须要辨别的一点是,协程是用用户态的,过程跟线程都是内核态,这点十分重要,这也是协程为什么高效的起因。 协程的劣势如下: 节俭 CPU:防止零碎内核级的线程频繁切换,造成的 CPU 资源节约。协程是用户态的线程,用户能够自行管制协程的创立于销毁,极大水平防止了零碎级线程上下文切换造成的资源节约。节约内存:在 64 位的 Linux 中,一个线程须要调配 8MB 栈内存和 64MB 堆内存,零碎内存的制约导致咱们无奈开启更多线程实现高并发。而在协程编程模式下,只须要几千字节(执行Go协程只须要极少的栈内存,大略4~5KB,默认状况下,线程栈的大小为1MB)能够轻松有十几万协程,这是线程无法比拟的。开发效率:应用协程在开发程序之中,能够很不便的将一些耗时的 IO 操作异步化,例如写文件、耗时 IO 申请等。并且它们并不是被操作系统所调度执行,而是程序员手动能够进行调度的。高效率:协程之间的切换产生在用户态,在用户态没有时钟中断,零碎调用等机制,因而效率高。Golang GMP 调度器注: 以下相干常识摘自刘丹冰(AceLd)的博文:[[Golang三关-典藏版] Golang 调度器 GMP 原理与调度全剖析](https://learnku.com/articles/41728 "[Golang三关-典藏版] Golang 调度器 GMP 原理与调度全剖析")简介G 示意:goroutine,即 Go 协程,每个 go 关键字都会创立一个协程。M 示意:thread 内核级线程,所有的 G 都要放在 M 上能力运行。P 示意:processor 处理器,调度 G 到 M 上,其保护了一个队列,存储了所有须要它来调度的G。Goroutine 调度器 P 和 OS 调度器是通过 M 联合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程调配到 CPU 的核上执行, ...

June 1, 2023 · 3 min · jiezi

关于goroutine:用golang的channel特性来做简易分拣机的中控部分

先介绍一下我的项目的背景,之前单位有一个做小型快递分拣机的需要,针对小型包裹智能分拣到不通的进口。大抵的物理传送带如下方图所示,原谅我不会画图。此文章的目标,只是给大家展现一下golang channel的用途。 如上图所示,传送带分了几个局部,头部区域,分拣工作区域,硬件设施(传感器和臂手)。 头部区域次要有摄像头和扫码枪,次要是辨认包裹,查问出包裹对应的区域地址。头部区域和分拣工作区域边界,会有一个红外线传感器,来确定包裹进入了分拣工作区域。在传送带的齿轮上会有一个速度传感器,来实时承受信号,计算传送带转动的间隔。分拣工作区域每隔30cm会有臂手(这里咱们会有led灯做模仿,其实就是一个GPIO) 依据以上的简述,咱们用golang代码来简略实现这个逻辑 1. 功能分析 头部区域波及到扫描枪和摄像头的AI辨认,咱们就临时用一个scanPacket函数来模仿代替, // 模仿 每隔两秒钟会有一个包裹func ScanPacket() { ticker := time.NewTicker(2 * time.Second) for { <-ticker.C fmt.Println("scan a packet") } }当辨认到一个包裹后咱们就要确定它要在哪个led灯左近,所以咱们先要把led的配置初始化好 // Light 灯type Light struct { Id int // 灯编号 State string // on/off Distance int64 // 间隔入口红外线传感器的地位(就是距分拣工作区域起始地位的长度) 单位 mm SwitchCh chan struct{} // 用来告诉该灯亮起}var lights = map[int]*Light{ 1: { Id: 1, State: "off", Distance: 300, SwitchCh: make(chan struct{}, 10), }, 2: { Id: 2, State: "off", Distance: 600, SwitchCh: make(chan struct{}, 10), }, 3: { Id: 3, State: "off", Distance: 900, SwitchCh: make(chan struct{}, 10), }, 4: { Id: 4, State: "off", Distance: 1200, SwitchCh: make(chan struct{}, 10), },}// 此处模仿了4个led灯和对应的传送带的地位定义包裹的构造体 ...

December 22, 2022 · 3 min · jiezi

关于goroutine:goroutinewaitgroup下载文件

0.1、索引https://blog.waterflow.link/articles/1663078266267 当咱们下载一个大文件的时候,会因为下载工夫太久而超时或者出错。那么我么咱们能够利用goroutine的个性并发分段的去申请下载资源。 1、Accept-Ranges首先下载链接须要在响应中返回Accept-Ranges,并且它的值不为 “none”,那么该服务器反对范畴申请。比方咱们能够利用HEAD申请来进行检测 ...// head申请获取url的header head, err := http.Head(url) if err != nil { return err } // 判断url是否反对指定范畴申请及哪种类型的分段申请 if head.Header.Get("Accept-Ranges") != "bytes" { return errors.New("not support range download") }...咱们能够应用curl命令看下head头 curl -I https://agritrop.cirad.fr/584726/1/Rapport.pdfHTTP/1.1 200 OKDate: Tue, 13 Sep 2022 13:52:08 GMTServer: HTTPDStrict-Transport-Security: max-age=63072000X-Content-Type-Options: nosniffX-Frame-Options: sameoriginContent-MD5: K4j+rsagurPwGP/5cm8k8Q==Last-Modified: Tue, 04 Jul 2017 08:26:16 GMTExpires: Wed, 13 Sep 2023 13:52:08 GMTContent-Disposition: inline; filename=Rapport.pdfAccept-Ranges: bytes # 容许范畴申请,单位是字节Content-Length: 6659798 # 文件的残缺大小Content-Type: application/pdfX-XSS-Protection: 1; mode=blockX-Permitted-Cross-Domain-Policies: noneCache-Control: public其中,Accept-Ranges: bytes 示意界定范畴的单位是 bytes 。这里 Content-Length也是无效信息,因为它提供了文件的残缺大小。 ...

September 13, 2022 · 3 min · jiezi

关于goroutine:goroutine调度

0.1、索引https://blog.waterflow.link/articles/1662974432717 1、过程一个过程蕴含能够由任何过程调配的公共资源。这些资源包含但不限于内存地址空间、文件句柄、设施和线程。 一个过程会蕴含上面一些属性: Process ID:过程IDProcess State:过程状态Process Priority:过程优先级Program Counter:程序计数器General purpose register:通用寄存器List of open files:关上的文件列表List of open devices:关上的设施列表Protection information:爱护信息List of the child process:子过程列表Pending alarms:待定正告Signals and signal handlers:信号和信号处理程序Accounting information:记账信息2、线程线程是轻量级的过程,一个线程将在过程内的所有线程之间共享过程的资源,如代码、数据、全局变量、文件和内存地址空间。然而栈和寄存器不会共享,每个线程都有本人的栈和寄存器 线程的长处: 进步零碎的吞吐量进步响应能力因为属性更少,上下文切换更快多核CPU的无效利用资源共享(代码、数据、地址空间、文件、全局变量)3、用户级线程用户级线程也称为绿色线程,如:C 中的coroutine、Go 中的 goroutine 和 Ruby 中的 Fiber 该过程保护一个内存地址空间,解决文件,以及正在运行的应用程序的设施和线程。操作系统调度程序决定哪些线程将在任何给定的 CPU 上接管工夫 因而,与耗时和资源密集型的过程创立相比,在一个过程中创立多个用户线程(goroutine)效率更高。 4、goroutine在Go中用户级线程被称作Goroutine,在创立goroutine时须要做到: 易于创立轻量级并发执行可扩大有限堆栈(最大堆栈大小在 64 位上为 1 GB,在 32 位上为 250 MB。)解决阻塞调用高效 (work stealing)其中阻塞调用可能是上面一些起因: 在channel中收发数据网络IO调用阻塞的零碎调用计时器互斥操作(Mutex)为什么go须要调度goroutine? Go 应用称为 goroutine 的用户级线程,它比内核级线程更轻且更便宜。 例如,创立一个初始 goroutine 将占用 2KB 的堆栈大小,而内核级线程将占用 8KB 的堆栈大小。 还有,goroutine 比内核线程有更快的创立、销毁和上下文切换,所以 go 调度器 须要退出来调度 goroutine。OS 不能调度用户级线程,OS 只晓得内核级线程。 Go 调度器 将 goroutine 多路复用到内核级线程,这些线程将在不同的 CPU 内核上运行 ...

September 12, 2022 · 5 min · jiezi

关于goroutine:Go开发工程师迎接上升风口踏入蓝海行业

download:Go开发工程师:迎接回升风口,踏入蓝海行业import org.json.JSONArray;import android.app.Activity;import android.app.AlertDialog;import android.content.ActivityNotFoundException;import android.content.DialogInterface;import android.content.Intent;import android.net.Uri;import android.os.Bundle;import com.phonegap.api.PhonegapActivity;import com.phonegap.api.Plugin;import com.phonegap.api.PluginResult;public class PluginTest extends Plugin { 在HTML文件中调用方法在html文件中引入phonegap和插件的js文件,调用方法 复制代码 <html> <head><meta charset="utf-8"><title>JAVA传参</title><script src="phonegap.js"></script> phonegap包--><script src="js/jquery.js"></script><script src="simplePlugin.js"></script>自定义的插件文件--><script> $(document).ready(function(e) { $("#btn_test").click(function(){ window.plugins.simplePlugin.hello( function(result) { alert("返回的第一个参数:"+result.str1+"返回的第二个参数"+result.str2); }, function(error) { }, "第一个参数", "第二个参数" ); });});</script></head><body><button type="button" id="btn_test">Click Me!</button></body></html>复制代码

November 13, 2021 · 1 min · jiezi

Go-并发的一些总结

GO并发goroutinegoroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。 Go routine并不会运行得比线程更快更快,它只是增加了更多的并发性。当一个goroutine被阻塞(比如等待IO),golang的scheduler会调度其它可以执行的goroutine运行。与线程相比,它有以下几个优点: 内存消耗更少: Goroutine所需要的内存通常只有2kb,而线程则需要1Mb(500倍)。创建与销毁的开销更小 由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。切换开销更小 这是goroutine于线程的主要区别,也是golang能够实现高并发的主要原因。线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占。在线程切换的过程中需要保存/恢复所有的寄存器信息,比如16个通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等。goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,就是一个普通的函数。 go hello(a, b, c)通过关键字go启动goroutine。 package mainimport ( "fmt" "runtime")func say(s string) { for i := 0; i < 5; i++ { runtime.Gosched() fmt.Println(s) }}func main() { go say("world") //开一个新的Goroutines执行 say("hello") //当前Goroutines执行}// 以上程序执行后将输出:// hello// world// hello// world// hello// world// hello// world// hello可以看到go关键字很方便的就实现了并发编程。上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。 runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。默认情况下,在Go 1.5将标识并发系统线程个数的runtime.GOMAXPROCS的初始值由1改为了运行环境的CPU核数。 但在Go 1.5以前调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。 正如前面提到的,goroutine的调度方式是协同式的。在协同式调度中,没有时间片的概念。为了并行执行goroutine,调度器会在以下几个时间点对其进行切换: Channel接受或者发送会造成阻塞的消息当一个新的goroutine被创建时可以造成阻塞的系统调用,如文件和网络操作channels不同goroutine之间通讯 全局变量的互斥锁使用管道channel来解决channle本质就是一个数据结构-队列 数据是先进先出【FIFO : first in first out】 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的 channel有类型的,一个string的channel只能存放string类型数据。Go 语言中使用了CSP模型来进行线程通信,准确说,是轻量级线程goroutine之间的通信。CSP模型和Actor模型类似,也是由独立的,并发执行的实体所构成,实体之间也是通过发送消息进行通信的。Actor模型和CSP模型区别Actor之间直接通讯,而CSP是通过Channel通讯,在耦合度上两者是有区别的,后者更加松耦合。主要的区别在于:CSP模型中消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。在Actor模型中,由于Actor可以根据自己的状态选择处理哪个传入消息,自主性可控性更好些。在Go语言中为了不堵塞进程,程序员必须检查不同的传入消息,以便预见确保正确的顺序。CSP好处是Channel不需要缓冲消息,而Actor理论上需要一个无限大小的邮箱作为消息缓冲。CSP模型的消息发送方只能在接收方准备好接收消息时才能发送消息。相反,Actor模型中的消息传递是异步的,即消息的发送和接收无需在同一时间进行,发送方可以在接收方准备好接收消息前将消息发送出去。 ...

June 26, 2019 · 2 min · jiezi

简单理解 Goroutine 是如何工作的

新公司使用 Golang,Golang 的魔力之一就是可以开启成千上万的 goroutine 来处理并发,于是上网看一些简单的关于 Goroutine 的介绍 https://blog.nindalf.com/post…后期再深入了解 Go Runtime 是如何管理和调度 goroutineGo 语言介绍如果你第一次接触 GO 编程,或者你对“并发不是并行这句话”没有任何概念,你可以先去看一下 Rob Pike 的演讲excellent talk on the subject,这30分钟的演讲你值得拥有当人们听到 concurrency(并发) 这个词往往会联想到 parallelism(并行),他们是有关联但完全不一样的概念。在编程的世界中,concurrency 是独立执行的过程的组合,而 parallelism 则是计算任务的同时执行。concurrency 是在一段时间内处理多个任务,parallelism 是同时做多个任务。Go 可以让我们去编写并发的程序,Go提供了 goroutine 和 goroutine 之间相互通信的能力。本文会更多地关注 goroutineGoroutine 和 OS ThreadGo 使用 goroutine 处理并发,而 Java 则使用 thread(线程)。为了比较 goroutine 和 thread 的区别,我们关注以下三个方面——内存使用,开启和销毁,切换时间内存使用创建 goroutine 不需要太多的内存,2KB的栈内存足矣,随后会伴随堆内存的分配和释放而增长。线程的开启则需要1MB内存 (goroutine的500倍),并伴随着一片警戒内存页(guard page)的分配,作为线程栈内存之前的警戒区域。所以,服务器在接收请求的时候可以为每个请求分配一个 goroutine 来处理,并不会有内存问题。但是一请求一线程 (例如Java bio 编程) 模式很有可能会导致 OutOfMemoryError,这不仅仅出现在 Java (bio) 编程中,那些使用OS线程处理并发的语言大多都需要面临这类问题。Goroutine 创建和销毁线程的创建和销毁有很大的开销,因为我们必须向 OS 请求线程资源,并且在线程完成后归还,维护线程池是一个很好的应对方法。相反,Go Runtime 花费很小的开销就能创建和销毁 goroutine,Go语言也没有提供对 goroutine 的人工管理接口 (意思就是 Go 已经安排好了,你不用瞎操心去管理 goroutine)Goroutine 切换开销当线程阻塞时,另一个线程会被调度。线程的调度是抢占式的,在切换过程中,调度器必须保存和恢复所有寄存器状态,包括:16个通用寄存器,程序计数器 PC,栈指针 SP,段寄存器,16个 XMM 寄存器,16个 AVX 寄存器,FP 协处理器状态等等。系统频繁地进行线程切换将会带来巨大的开销。Goroutine 的调度是协作式的(所以被称为 go 协程?)。在进行协和切换时,只有3个寄存器需要被存储和恢复,他们是程序计数器PC,栈指针和通用寄存器DX,开销很小。Goroutine 的数量通常是巨大的,但这不会影响 goroutine 的切换时间。调度器只会关注运行状态的 goroutine,而忽略阻塞态的 goroutine。Go 跟现代调度器一样都是 O(1) 单位时间复杂度,意味着增加goroutine 数量不会增加切换时间Goroutine 如何执行之前提及过,Go Runtime(运行时) 管理了 goroutine 的创建,调度和销毁。Go Runtime 事先分配了一些线程,所有的 goroutine 都在这些线程上多路复用。在某个时刻,每个线程只会装载执行一个 goroutine,如果那个 goroutine 被阻塞了,他会被换出,并由另一个 goroutine 获得线程执行因为 goroutine 的调度是协作式的,一个执行无限循环任务的 goroutine 会“饿死”其他在相同线程上的goroutine(其他 goroutine 无法抢占获得线程)。这个问题在 Go 1.2 中有所缓解,通过在 goroutine 进入一个方法时偶尔去调用调度器(执行调度),所以一个包含方法执行的无限循环是可被抢占的。Goroutine 阻塞Goroutine 的阻塞不会导致线程的阻塞。即使成千上万的 goroutine 被创建,即使他们大多数都被阻塞了,但只要 Go Runtime 调度其他可用的 goroutine,就不会造成系统资源浪费用简单的话说,goroutine 是一种更轻量级的OS线程的抽象。Go 开发者不需要管理线程,同样OS也感知不到 goroutine 的存在。在 OS 的视角下,Go 程序的就像一个事件驱动的 C 程序。线程和CPU虽然你不能直接控制 Go Runtime 创建线程的数量,你仍可控制程序使用的CPU核心数,通过runtime.GOMAXPROCS(n)来设置 GOMAXPROCS。增加 CPU 核心数也话并不能显著提高程序的性能,但你可以使用工具来找到程序运行最理想的CPU核心数总结像其他语言一样,你应该尽可能避免让多个 goroutine 同时访问共享资源。goroutine 之间不要使用共享内存进行通信,最好的做法是使用 channel 在他们之间传输数据。最后我强烈建议你阅读 C.A.R.Hoare 的文章 Communicating Sequential Processes。在文章中,他预言单核心 CPU 会最终到达性能瓶颈,芯片制造者将会堆积核心数量。他所表达的观点对 GO 语言的设计有着深远的影响。 ...

March 27, 2019 · 1 min · jiezi

Go调度器系列(2)宏观看调度器

上一篇文章《Go语言高阶:调度器系列(1)起源》,学goroutine调度器之前的一些背景知识,这篇文章则是为了对调度器有个宏观的认识,从宏观的3个角度,去看待和理解调度器是什么样子的,但仍然不涉及具体的调度原理。三个角度分别是:调度器的宏观组成调度器的生命周期GMP的可视化感受在开始前,先回忆下调度器相关的3个缩写:G: goroutine,每个G都代表1个goroutineM: 工作线程,是Go语言定义出来在用户层面描述系统线程的对象 ,每个M代表一个系统线程P: 处理器,它包含了运行Go代码的资源。3者的简要关系是P拥有G,M必须和一个P关联才能运行P拥有的G。调度器的功能《Go语言高阶:调度器系列(1)起源》中介绍了协程和线程的关系,协程需要运行在线程之上,线程由CPU进行调度。在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上。Go的调度器也是经过了多个版本的开发才是现在这个样子的,1.0版本发布了最初的、最简单的调度器,是G-M模型,存在4类问题1.1版本重新设计,修改为G-P-M模型,奠定当前调度器基本模样1.2版本加入了抢占式调度,防止协程不让出CPU导致其他G饿死在$GOROOT/src/runtime/proc.go的开头注释中包含了对Scheduler的重要注释,介绍Scheduler的设计曾拒绝过3种方案以及原因,本文不再介绍了,希望你不要忽略为数不多的官方介绍。Scheduler的宏观组成Tony Bai在《也谈goroutine调度器》中的这幅图,展示了goroutine调度器和系统调度器的关系,而不是把二者割裂开来,并且从宏观的角度展示了调度器的重要组成。自顶向下是调度器的4个部分:全局队列(Global Queue):存放等待运行的G。P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G’时,G’优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS个。M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。调度器的生命周期接下来我们从另外一个宏观角度——生命周期,认识调度器。所有的Go程序运行都会经过一个完整的调度器生命周期:从创建到结束。即使下面这段简单的代码:package mainimport “fmt”// main.mainfunc main() { fmt.Println(“Hello scheduler”)}也会经历如上图所示的过程:runtime创建最初的线程m0和goroutine g0,并把2者关联。调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。示例代码中的main函数是main.main,runtime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。G拥有栈,M根据G中的栈信息和调度信息设置运行环境M运行GG退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。调度器的生命周期几乎占满了一个Go程序的一生,runtime.main的goroutine执行之前都是为调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束。GMP的可视化感受上面的两个宏观角度,都是根据文档、代码整理出来,最后我们从可视化角度感受下调度器,有2种方式。方式1:go tool tracetrace记录了运行时的信息,能提供可视化的Web页面。简单测试代码:main函数创建trace,trace会运行在单独的goroutine中,然后main打印"Hello trace"退出。func main() { // 创建trace文件 f, err := os.Create(“trace.out”) if err != nil { panic(err) } defer f.Close() // 启动trace goroutine err = trace.Start(f) if err != nil { panic(err) } defer trace.Stop() // main fmt.Println(“Hello trace”)}运行程序和运行trace:➜ trace git:(master) ✗ go run trace1.goHello trace➜ trace git:(master) ✗ lstrace.out trace1.go➜ trace git:(master) ✗➜ trace git:(master) ✗ go tool trace trace.out2019/03/24 20:48:22 Parsing trace…2019/03/24 20:48:22 Splitting trace…2019/03/24 20:48:22 Opening browser. Trace viewer is listening on http://127.0.0.1:55984效果:从上至下分别是goroutine(G)、堆、线程(M)、Proc(P)的信息,从左到右是时间线。用鼠标点击颜色块,最下面会列出详细的信息。我们可以发现:runtime.main的goroutine是g1,这个编号应该永远都不变的,runtime.main是在g0之后创建的第一个goroutine。g1中调用了main.main,创建了trace goroutine g18。g1运行在P2上,g18运行在P0上。P1上实际上也有goroutine运行,可以看到短暂的竖线。go tool trace的资料并不多,如果感兴趣可阅读:https://making.pusher.com/go-… ,中文翻译是:https://mp.weixin.qq.com/s/nf… 。方式2:Debug trace示例代码:// main.mainfunc main() { for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println(“Hello scheduler”) }}编译和运行,运行过程会打印trace:➜ one_routine2 git:(master) ✗ go build .➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000 ./one_routine2结果:SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]SCHED 1001ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 2002ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 3004ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 4005ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello schedulerSCHED 5013ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]Hello scheduler看到这密密麻麻的文字就有点担心,不要愁!因为每行字段都是一样的,各字段含义如下:SCHED:调试信息输出标志字符串,代表本行是goroutine调度器的输出;0ms:即从程序启动到输出这行日志的时间;gomaxprocs: P的数量,本例有8个P;idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;threads: os threads/M的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;spinningthreads: 处于自旋状态的os thread数量;idlethread: 处于idle状态的os thread的数量;runqueue=0: Scheduler全局队列中G的数量;[0 0 0 0 0 0 0 0]: 分别为8个P的local queue中的G的数量。看第一行,含义是:刚启动时创建了8个P,其中5个空闲的P,共创建5个M,其中1个M处于自旋,没有M处于空闲,8个P的本地队列都没有G。再看个复杂版本的,加上scheddetail=1可以打印更详细的trace信息。命令:➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2结果:截图可能更代码匹配不起来,最初代码是for死循环,后面为了减少打印加了限制循环5次每次分别打印了每个P、M、G的信息,P的数量等于gomaxprocs,M的数量等于threads,主要看圈黄的地方:第1处:P1和M2进行了绑定。第2处:M2和P1进行了绑定,但M2上没有运行的G。第3处:代码中使用fmt进行打印,会进行系统调用,P1系统调用的次数很多,说明我们的用例函数基本在P1上运行。第4处和第5处:M0上运行了G1,G1的状态为3(系统调用),G进行系统调用时,M会和P解绑,但M会记住之前的P,所以M0仍然记绑定了P1,而P1称未绑定M。总结时刻这篇文章,从3个宏观的角度介绍了调度器,也许你依然不知道调度器的原理,心里感觉模模糊糊,没关系,一步一步走,通过这篇文章希望你了解了:Go调度器和OS调度器的关系Go调度器的生命周期/总体流程P的数量等于GOMAXPROCSM需要通过绑定的P获取G,然后执行G,不断重复这个过程示例代码本文所有示例代码都在Github,可通过阅读原文访问:golang_step_by_step/tree/master/scheduler参考资料Go程序的“一生”也谈goroutine调度器Debug trace, 当前调度器设计人Dmitry Vyukov的文章Go tool trace中文翻译Dave关于GODEBUG的介绍最近的感受是:自己懂是一个层次,能写出来需要抬升一个层次,给他人讲懂又需要抬升一个层次。希望朋友们有所收获。如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/ ...

March 27, 2019 · 2 min · jiezi

Go语言高阶:调度器系列(1)起源

如果把语言比喻为武侠小说中的武功,如果只是会用,也就是达到四五层,如果用的熟练也就六七层,如果能见招拆招也得八九层,如果你出神入化,立于不败之地十层。如果你想真正掌握一门语言的,怎么也得八层以上,需要你深入了解这门语言方方面面的细节。希望以后对Go语言的掌握能有八九层,怎么能不懂调度器!?Google、百度、微信搜索了许多Go语言调度的文章,这些文章上来就讲调度器是什么样的,它由哪些组成,它的运作原理,搞的我只能从这些零散的文章中形成调度器的“概貌”,这是我想要的结果,但这还不够。学习不仅要知其然,还要知其所以然。学习之前,先学知识点的历史,再学知识,这样你就明白,为什么它是当下这个样子。所以,我打算写一个goroutine调度器的系列文章,从历史背景讲起,循序渐进,希望大家能对goroutine调度器有一个全面的认识。这篇文章介绍调度器相关的历史背景,请慢慢翻阅。远古时代上面这个大家伙是ENIAC,它诞生在宾夕法尼亚大学,是世界第一台真正的通用计算机,和现代的计算机相比,它是相当的“笨重”,它的计算能力,跟现代人手普及的智能手机相比,简直是一个天上一个地下,ENIAC在地下,智能手机在天上。它上面没有操作系统,更别提进程、线程和协程了。进程时代后来,现代化的计算机有了操作系统,每个程序都是一个进程,但是操作系统在一段时间只能运行一个进程,直到这个进程运行完,才能运行下一个进程,这个时期可以成为单进程时代——串行时代。和ENIAC相比,单进程是有了几万倍的提度,但依然是太慢了,比如进程要读数据阻塞了,CPU就在哪浪费着,伟大的程序员们就想了,不能浪费啊,怎么才能充分的利用CPU呢?后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了。线程时代多进程真实个好东西,有了对进程的调度能力之后,伟大的程序员又发现,进程拥有太多资源,在创建、切换和销毁的时候,都会占用很长的时间,CPU虽然利用起来了,但CPU有很大的一部分都被用来进行进程调度了,怎么才能提高CPU的利用率呢?大家希望能有一种轻量级的进程,调度不怎么花时间,这样CPU就有更多的时间用在执行任务上。后来,操作系统支持了线程,线程在进程里面,线程运行所需要资源比进程少多了,跟进程比起来,切换简直是“不算事”。一个进程可以有多个线程,CPU在执行调度的时候切换的是线程,如果下一个线程也是当前进程的,就只有线程切换,“很快”就能完成,如果下一个线程不是当前的进程,就需要切换进程,这就得费点时间了。这个时代,CPU的调度切换的是进程和线程。多线程看起来很美好,但实际多线程编程却像一坨屎,一是由于线程的设计本身有点复杂,而是由于需要考虑很多底层细节,比如锁和冲突检测。协程多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存(每个线程的内存占用级别为MB),线程多了之后调度也会消耗大量的CPU。伟大的程序员们有开始想了,如何才能充分利用CPU、内存等资源的情况下,实现更高的并发?既然线程的资源占用、调度在高并发的情况下,依然是比较大的,是否有一种东西,更加轻量?你可能知道:线程分为内核态线程和用户态线程,用户态线程需要绑定内核态线程,CPU并不能感知用户态线程的存在,它只知道它在运行1个线程,这个线程实际是内核态线程。用户态线程实际有个名字叫协程(co-routine),为了容易区分,我们使用协程指用户态线程,使用线程指内核态线程。协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。协程和线程有3种映射关系:N:1,N个协程绑定1个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1个进程的所有协程都绑定在1个线程上,一是某个程序用不了硬件的多核加速能力,二是一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。1:1,1个协程绑定1个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点,但有一个缺点是协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。M:N,M个协程绑定1个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂。协程是个好东西,不少语言支持了协程,比如:Lua、Erlang、Java(C++即将支持),就算语言不支持,也有库支持协程,比如C语言的coroutine(风云大牛作品)、Kotlin的kotlinx.coroutines、Python的gevent。goroutineGo语言的诞生就是为了支持高并发,有2个支持高并发的模型:CSP和Actor。鉴于Occam和Erlang都选用了CSP(来自Go FAQ),并且效果不错,Go也选了CSP,但与前两者不同的是,Go把channel作为头等公民。就像前面说的多线程编程太不友好了,Go为了提供更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。Go中,协程被称为goroutine(Rob Pike说goroutine不是协程,因为他们并不完全相同),它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。Go语言的老调度器终于来到了Go语言的调度器环节。调度器的任务是在用户态完成goroutine的调度,而调度器的实现好坏,对并发实际有很大的影响,并且Go的调度器就是M:N类型的,实现起来也是最复杂。现在的Go语言调度器是2012年重新设计的(设计方案),在这之前的调度器称为老调度器,老调度器的实现不太好,存在性能问题,所以用了4年左右就被替换掉了,老调度器大概是下面这个样子:最下面是操作系统,中间是runtime,runtime在Go中很重要,许多程序运行时的工作都由runtime完成,调度器就是runtime的一部分,虚线圈出来的为调度器,它有两个重要组成:M,代表线程,它要运行goroutine。Global G Queue,是全局goroutine队列,所有的goroutine都保存在这个队列中,goroutine用G进行代表。M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。老调度器有4个缺点:创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M’。M中的mcache是用来存放小对象的,mcache和栈都和M关联造成了大量的内存开销和差的局部性。系统调用导致频繁的线程阻塞和取消阻塞操作增加了系统开销。Go语言的新调度器面对以上老调度的问题,Go设计了新的调度器,设计文稿:https://golang.org/s/go11sched新调度器引入了:P:Processor,它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。work stealing:当M绑定的P没有可运行的G时,它可以从其他运行的M’那里偷取G。现在,调度器中3个重要的缩写你都接触到了,所有文章都用这几个缩写,请牢记:G: goroutineM: 工作线程P: 处理器,它包含了运行Go代码的资源,M必须和一个P关联才能运行G。这篇文章的目的不是介绍调度器的实现,而是调度器的一些理念,帮助你后面更好理解调度器的实现,所以我们回归到调度器设计思想上。调度器的有两大思想:复用线程:协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用。在调度器中复用线程还有2个体现:1)work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。2)hand off,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。利用并行:GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。调度器的两小策略:抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。上面提到并行了,关于并发和并行再说一下:Go创始人Rob Pike一直在强调go是并发,不是并行,因为Go做的是在一段时间内完成几十万、甚至几百万的工作,而不是同一时间同时在做大量的工作。并发可以利用并行提高效率,调度器是有并行设计的。并行依赖多核技术,每个核上在某个时间只能执行一个线程,当我们的CPU有8个核时,我们能同时执行8个线程,这就是并行。结束语这篇文章的主要目的是为后面介绍Go语言调度器做铺垫,由远及近的方式简要介绍了多进程、多线程、协程、并发和并行有关的“史料”,希望你了解为什么Go采用了goroutine,又为何调度器如此重要。如果你等不急了,想了解Go调度器相关的原理,看下这些文章:设计方案:https://golang.org/s/go11sched代码中关于调度器的描述:https://golang.org/src/runtim…引用最多的调度器文章:https://morsmachine.dk/go-sch…kavya的PPT,目前看到的讲调度最好的PPT:https://speakerdeck.com/kavya…work stealing论文:http://supertech.csail.mit.ed…分析调度器的论文(就问你6不6,还有论文研究):http://www.cs.columbia.edu/~a…声明:关于老调度器的资料已经完全搜不到,根据新版调度器设计方案的描述,想象着写了老调度器这一章,可能存在错误。参考资料https://en.wikipedia.org/wiki…https://en.wikipedia.org/wiki...https://en.wikipedia.org/wiki...https://golang.org/doc/faq#go...https://golang.org/s/go11schedhttps://golang.org/src/runtim…如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/03/10/golang-scheduler-1-history

March 10, 2019 · 1 min · jiezi

Go优雅重启Web server示例-讲解版

本文参考 GRACEFULLY RESTARTING A GOLANG WEB SERVER进行归纳和说明。你也可以从这里拿到添加备注的代码版本。我做了下分割,方便你能看懂。问题因为 golang 是编译型的,所以当我们修改一个用 go 写的服务的配置后,需要重启该服务,有的甚至还需要重新编译,再发布。如果在重启的过程中有大量的请求涌入,能做的无非是分流,或者堵塞请求。不论哪一种,都不优雅~,所以slax0r以及他的团队,就试图探寻一种更加平滑的,便捷的重启方式。原文章中除了排版比较帅外,文字内容和说明还是比较少的,所以我希望自己补充一些说明。原理上述问题的根源在于,我们无法同时让两个服务,监听同一个端口。解决方案就是复制当前的 listen 文件,然后在新老进程之间通过 socket 直接传输参数和环境变量。新的开启,老的关掉,就这么简单。防看不懂须知Unix domain socket一切皆文件先玩一下运行程序,过程中打开一个新的 console,输入 kill -1 [进程号],你就能看到优雅重启的进程了。代码思路func main() { 主函数,初始化配置 调用serve()}func serve() { 核心运行函数 getListener() // 1. 获取监听 listener start() // 2. 用获取到的 listener 开启 server 服务 waitForSignal() // 3. 监听外部信号,用来控制程序 fork 还是 shutdown}func getListener() { 获取正在监听的端口对象 (第一次运行新建)}func start() { 运行 http server}func waitForSignal() { for { 等待外部信号 1. fork子进程 2. 关闭进程 }}上面是代码思路的说明,基本上我们就围绕这个大纲填充完善代码。定义结构体我们抽象出两个结构体,描述程序中公用的数据结构var cfg srvCfgtype listener struct { // Listener address Addr string json:"addr" // Listener file descriptor FD int json:"fd" // Listener file name Filename string json:"filename"}type srvCfg struct { sockFile string addr string ln net.Listener shutDownTimeout time.Duration childTimeout time.Duration}listener 是我们的监听者,他包含了监听地址,文件描述符,文件名。文件描述符其实就是进程所需要打开的文件的一个索引,非负整数。我们平时创建一个进程时候,linux都会默认打开三个文件,标准输入stdin,标准输出stdout,标准错误stderr,这三个文件各自占用了 0,1,2 三个文件描述符。所以之后你进程还要打开文件的话,就得从 3 开始了。这个listener,就是我们进程之间所要传输的数据了。srvCfg 是我们的全局环境配置,包含 socket file 路径,服务监听地址,监听者对象,父进程超时时间,子进程超时时间。因为是全局用的配置数据,我们先 var 一下。入口看看我们的 main 长什么样子func main() { serve(srvCfg{ sockFile: “/tmp/api.sock”, addr: “:8000”, shutDownTimeout: 5time.Second, childTimeout: 5*time.Second, }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(Hello, world!)) }))}func serve(config srvCfg, handler http.Handler) { cfg = &config var err error // get tcp listener cfg.ln, err = getListener() if err != nil { panic(err) } // return an http Server srv := start(handler) // create a wait routine err = waitForSignals(srv) if err != nil { panic(err) }}很简单,我们把配置都准备好了,然后还注册了一个 handler–输出 Hello, world!serve 函数的内容就和我们之前的思路一样,只不过多了些错误判断。接下去,我们一个一个看里面的函数…获取 listener也就是我们的 getListener() 函数func getListener() (net.Listener, error) { // 第一次执行不会 importListener ln, err := importListener() if err == nil { fmt.Printf(“imported listener file descriptor for addr: %s\n”, cfg.addr) return ln, nil } // 第一次执行会 createListener ln, err = createListener() if err != nil { return nil, err } return ln, err}func importListener() (net.Listener, error) { …}func createListener() (net.Listener, error) { fmt.Println(“首次创建 listener”, cfg.addr) ln, err := net.Listen(“tcp”, cfg.addr) if err != nil { return nil, err } return ln, err}因为第一次不会执行 importListener, 所以我们暂时不需要知道 importListener 里是怎么实现的。只肖明白 createListener 返回了一个监听对象。而后就是我们的 start 函数func start(handler http.Handler) *http.Server { srv := &http.Server{ Addr: cfg.addr, Handler: handler, } // start to serve go srv.Serve(cfg.ln) fmt.Println(“server 启动完成,配置信息为:",cfg.ln) return srv}很明显,start 传入一个 handler,然后协程运行一个 http server。监听信号监听信号应该是我们这篇里面重头戏的入口,我们首先来看下代码:func waitForSignals(srv *http.Server) error { sig := make(chan os.Signal, 1024) signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) for { select { case s := <-sig: switch s { case syscall.SIGHUP: err := handleHangup() // 关闭 if err == nil { // no error occured - child spawned and started return shutdown(srv) } case syscall.SIGTERM, syscall.SIGINT: return shutdown(srv) } } }}首先建立了一个通道,这个通道用来接收系统发送到程序的命令,比如kill -9 myprog,这个 9 就是传到通道里的。我们用 Notify 来限制会产生响应的信号,这里有:SIGTERMSIGINTSIGHUP关于信号如果实在搞不清这三个信号的区别,只要明白我们通过区分信号,留给了进程自己判断处理的余地。然后我们开启了一个循环监听,显而易见地,监听的就是系统信号。当信号为 syscall.SIGHUP ,我们就要重启进程了。而当信号为 syscall.SIGTERM, syscall.SIGINT 时,我们直接关闭进程。于是乎,我们就要看看,handleHangup 里面到底做了什么。父子间的对话进程之间的优雅重启,我们可以看做是一次愉快的父子对话,爸爸给儿子开通了一个热线,爸爸通过热线把现在正在监听的端口信息告诉儿子,儿子在接受到必要的信息后,子承父业,开启新的空进程,告知爸爸,爸爸正式退休。func handleHangup() error { c := make(chan string) defer close(c) errChn := make(chan error) defer close(errChn) // 开启一个热线通道 go socketListener(c, errChn) for { select { case cmd := <-c: switch cmd { case “socket_opened”: p, err := fork() if err != nil { fmt.Printf(“unable to fork: %v\n”, err) continue } fmt.Printf(“forked (PID: %d), waiting for spinup”, p.Pid) case “listener_sent”: fmt.Println(“listener sent - shutting down”) return nil } case err := <-errChn: return err } } return nil}socketListener 开启了一个新的 unix socket 通道,同时监听通道的情况,并做相应的处理。处理的情况说白了就只有两种:通道开了,说明我可以造儿子了(fork),儿子来接爸爸的信息爸爸把监听对象文件都传给儿子了,爸爸完成使命handleHangup 里面的东西有点多,不要慌,我们一个一个来看。先来看 socketListener:func socketListener(chn chan<- string, errChn chan<- error) { // 创建 socket 服务端 fmt.Println(“创建新的socket通道”) ln, err := net.Listen(“unix”, cfg.sockFile) if err != nil { errChn <- err return } defer ln.Close() // signal that we created a socket fmt.Println(“通道已经打开,可以 fork 了”) chn <- “socket_opened” // accept // 阻塞等待子进程连接进来 c, err := acceptConn(ln) if err != nil { errChn <- err return } // read from the socket buf := make([]byte, 512) nr, err := c.Read(buf) if err != nil { errChn <- err return } data := buf[0:nr] fmt.Println(“获得消息子进程消息”, string(data)) switch string(data) { case “get_listener”: fmt.Println(“子进程请求 listener 信息,开始传送给他吧~”) err := sendListener(c) // 发送文件描述到新的子进程,用来 import Listener if err != nil { errChn <- err return } // 传送完毕 fmt.Println(“listener 信息传送完毕”) chn <- “listener_sent” }}sockectListener创建了一个 unix socket 通道,创建完毕后先发送了 socket_opened 这个信息。这时候 handleHangup 里的 case “socket_opened” 就会有反应了。同时,socketListener 一直在 accept 阻塞等待新程序的信号,从而发送原 listener 的文件信息。直到发送完毕,才会再告知 handlerHangup listener_sent。下面是 acceptConn 的代码,并没有复杂的逻辑,就是等待子程序请求、处理超时和错误。func acceptConn(l net.Listener) (c net.Conn, err error) { chn := make(chan error) go func() { defer close(chn) fmt.Printf(“accept 新连接%+v\n”, l) c, err = l.Accept() if err != nil { chn <- err } }() select { case err = <-chn: if err != nil { fmt.Printf(“error occurred when accepting socket connection: %v\n”, err) } case <-time.After(cfg.childTimeout): fmt.Println(“timeout occurred waiting for connection from child”) } return}还记的我们之前定义的 listener 结构体吗?这时候就要派上用场了:func sendListener(c net.Conn) error { fmt.Printf(“发送老的 listener 文件 %+v\n”, cfg.ln) lnFile, err := getListenerFile(cfg.ln) if err != nil { return err } defer lnFile.Close() l := listener{ Addr: cfg.addr, FD: 3, // 文件描述符,进程初始化描述符为0 stdin 1 stdout 2 stderr,所以我们从3开始 Filename: lnFile.Name(), } lnEnv, err := json.Marshal(l) if err != nil { return err } fmt.Printf(“将 %+v\n 写入连接\n”, string(lnEnv)) _, err = c.Write(lnEnv) if err != nil { return err } return nil}func getListenerFile(ln net.Listener) (*os.File, error) { switch t := ln.(type) { case *net.TCPListener: return t.File() case *net.UnixListener: return t.File() } return nil, fmt.Errorf(“unsupported listener: %T”, ln)}sendListener 先将我们正在使用的tcp监听文件(一切皆文件)做了一份拷贝,并把必要的信息塞进了listener 结构体中,序列化后用 unix socket 传输给新的子进程。说了这么多都是爸爸进程的代码,中间我们跳过了创建子进程,那下面我们来看看 fork,也是一个重头戏:func fork() (*os.Process, error) { // 拿到原监听文件描述符并打包到元数据中 lnFile, err := getListenerFile(cfg.ln) fmt.Printf(“拿到监听文件 %+v\n,开始创建新进程\n”, lnFile.Name()) if err != nil { return nil, err } defer lnFile.Close() // 创建子进程时必须要塞的几个文件 files := []*os.File{ os.Stdin, os.Stdout, os.Stderr, lnFile, } // 拿到新进程的程序名,因为我们是重启,所以就是当前运行的程序名字 execName, err := os.Executable() if err != nil { return nil, err } execDir := filepath.Dir(execName) // 生孩子了 p, err := os.StartProcess(execName, []string{execName}, &os.ProcAttr{ Dir: execDir, Files: files, Sys: &syscall.SysProcAttr{}, }) fmt.Println(“创建子进程成功”) if err != nil { return nil, err } // 这里返回 nil 后就会直接 shutdown 爸爸进程 return p, nil}当执行 StartProcess 的那一刻,你会意识到,子进程的执行会回到最初的地方,也就是 main 开始。这时候,我们 获取 listener中的 importListener 方法就会被激活:func importListener() (net.Listener, error) { // 向已经准备好的 unix socket 建立连接,这个是爸爸进程在之前就建立好的 c, err := net.Dial(“unix”, cfg.sockFile) if err != nil { fmt.Println(“no unix socket now”) return nil, err } defer c.Close() fmt.Println(“准备导入原 listener 文件…”) var lnEnv string wg := sync.WaitGroup{} wg.Add(1) go func(r io.Reader) { defer wg.Done() // 读取 conn 中的内容 buf := make([]byte, 1024) n, err := r.Read(buf[:]) if err != nil { return } lnEnv = string(buf[0:n]) }(c) // 写入 get_listener fmt.Println(“告诉爸爸我要 ‘get-listener’ 了”) _, err = c.Write([]byte(“get_listener”)) if err != nil { return nil, err } wg.Wait() // 等待爸爸传给我们参数 if lnEnv == "” { return nil, fmt.Errorf(“Listener info not received from socket”) } var l listener err = json.Unmarshal([]byte(lnEnv), &l) if err != nil { return nil, err } if l.Addr != cfg.addr { return nil, fmt.Errorf(“unable to find listener for %v”, cfg.addr) } // the file has already been passed to this process, extract the file // descriptor and name from the metadata to rebuild/find the *os.File for // the listener. // 我们已经拿到了监听文件的信息,我们准备自己创建一份新的文件并使用 lnFile := os.NewFile(uintptr(l.FD), l.Filename) fmt.Println(“新文件名:”, l.Filename) if lnFile == nil { return nil, fmt.Errorf(“unable to create listener file: %v”, l.Filename) } defer lnFile.Close() // create a listerer with the *os.File ln, err := net.FileListener(lnFile) if err != nil { return nil, err } return ln, nil}这里的 importListener 执行时间,就是在父进程创建完新的 unix socket 通道后。至此,子进程开始了新的一轮监听,服务…结束代码量虽然不大,但是传递了一个很好的优雅重启思路,有些地方还是要实践一下才能理解(对于我这种新手而言)。其实网上还有很多其他优雅重启的方式,大家可以 Google 一下。希望我上面简单的讲解能够帮到你,如果有错误的话请及时指出,我会更正的。你也可以从这里拿到添加备注的代码版本。我做了下分割,方便你能看懂。 ...

January 23, 2019 · 6 min · jiezi

Go并发调用的超时处理

之前有聊过 golang 的协程,我发觉似乎还很理论,特别是在并发安全上,所以特结合网上的一些例子,来试验下go routine中 的 channel, select, context 的妙用。场景-微服务调用我们用 gin(一个web框架) 作为处理请求的工具,没有安装过的话,需求是这样的:一个请求 X 会去并行调用 A, B, C 三个方法,并把三个方法返回的结果加起来作为 X 请求的 Response。但是我们这个 Response 是有时间要求的(不能超过3秒的响应时间),可能 A, B, C 中任意一个或两个,处理逻辑十分复杂,或者数据量超大,导致处理时间超出预期,那么我们就马上切断,并返回已经拿到的任意个返回结果之和。我们先来定义主函数:func main() { r := gin.New() r.GET("/calculate", calHandler) http.ListenAndServe(":8008", r)}非常简单,普通的请求接受和 handler 定义。其中 calHandler 是我们用来处理请求的函数。分别定义三个假的微服务,其中第三个将会是我们超时的哪位func microService1() int { time.Sleep(1time.Second) return 1}func microService2() int { time.Sleep(2time.Second) return 2}func microService3() int { time.Sleep(10*time.Second) return 3}接下来,我们看看 calHandler 里到底是什么func calHandler(c *gin.Context) { …}要点1–并发调用直接用 go 就好了嘛所以一开始我们可能就这么写:go microService1()go microService2()go microService3()很简单有没有,但是等等,说好的返回值我怎么接呢?为了能够并行地接受处理结果,我们很容易想到用 channel 去接。所以我们把调用服务改成这样:var resChan = make(chan int, 3) // 因为有3个结果,所以我们创建一个可以容纳3个值的 int channel。go func() { resChan <- microService1()}()go func() { resChan <- microService2()}()go func() { resChan <- microService3()}()有东西接,那也要有方法去算,所以我们加一个一直循环拿 resChan 中结果并计算的方法:var resContainer, sum intfor { resContainer = <-resChan sum += resContainer}这样一来我们就有一个 sum 来计算每次从 resChan 中拿出的结果了。要点2–超时信号还没结束,说好的超时处理呢?为了实现超时处理,我们需要引入一个东西,就是 context,什么是 context ?我们这里只使用 context 的一个特性,超时通知(其实这个特性完全可以用 channel 来替代)。可以看在定义 calHandler 的时候我们已经将 c gin.Context 作为参数传了进来,那我们就不用自己在声明了。gin.Context 简单理解为贯穿整个 gin 声明周期的上下文容器,有点像是分身,亦或是量子纠缠的感觉。有了这个 gin.Context, 我们就能在一个地方对 context 做出操作,而其他正在使用 context 的函数或方法,也会感受到 context 做出的变化。ctx, _ := context.WithTimeout(c, 3time.Second) //定义一个超时的 context只要时间到了,我们就能用 ctx.Done() 获取到一个超时的 channel(通知),然后其他用到这个 ctx 的地方也会停掉,并释放 ctx。一般来说,ctx.Done() 是结合 select 使用的。所以我们又需要一个循环来监听 ctx.Done()for { select { case <- ctx.Done(): // 返回结果}现在我们有两个 for 了,是不是能够合并下?for { select { case resContainer = <-resChan: sum += resContainer fmt.Println(“add”, resContainer) case <- ctx.Done(): fmt.Println(“result:”, sum) return }}诶嘿,看上去不错。不过我们怎么在正常完成微服务调用的时候输出结果呢?看来我们还需要一个 flagvar count intfor { select { case resContainer = <-resChan: sum += resContainer count ++ fmt.Println(“add”, resContainer) if count > 2 { fmt.Println(“result:”, sum) return } case <- ctx.Done(): fmt.Println(“timeout result:”, sum) return }}我们加入一个计数器,因为我们只是调用3次微服务,所以当 count 大于2的时候,我们就应该结束并输出结果了。要点3–并发中的等待这是一种偷懒的方法,因为我们知道了调用微服务的次数,如果我们并不知道,或者之后还要添加呢?手动每次改 count 的判断阈值会不会太沙雕了?这时候我们就要加入 sync 包了。我们将会使用的 sync 的一个特性是 WaitGroup。它的作用是等待一组协程运行完毕后,执行接下去的步骤。我们来改下之前微服务调用的代码块:var success = make(chan int, 1) // 成功的通道标识wg := sync.WaitGroup{} // 创建一个 waitGroup 组wg.Add(3) // 我们往组里加3个标识,因为我们要运行3个任务go func() { resChan <- microService1() wg.Done() // 完成一个,Done()一个}()go func() { resChan <- microService2() wg.Done()}()go func() { resChan <- microService3() wg.Done()}()wg.Wait() // 直到我们前面三个标识都被 Done 了,否则程序一直会阻塞在这里success <- 1 // 我们发送一个成功信号到通道中既然我们有了 success 这个信号,那么再把它加入到监控 for 循环中,并做些修改,删除原来 count 判断的部分。go func() { for { select { case resContainer = <-resChan: sum += resContainer fmt.Println(“add”, resContainer) case <- success: fmt.Println(“result:”, sum) return case <- ctx.Done(): fmt.Println(“result:”, sum) return } }}()三个 case,分工明确,一个用来拿服务输出的结果并计算,一个用来做最终的完成输出,一个是超时输出。同时我们将这个循环监听,也作为协程运行。至此,所有的主要代码都完成了。下面是完全版package mainimport ( “context” “fmt” “net/http” “sync” “time” “github.com/gin-gonic/gin”)// 一个请求会触发调用三个服务,每个服务输出一个 int,// 请求要求结果为三个服务输出 int 之和// 请求返回时间不超过3秒,大于3秒只输出已经获得的 int 之和func calHandler(c gin.Context) { var resContainer, sum int var success, resChan = make(chan int), make(chan int, 3) ctx, _ := context.WithTimeout(c, 3time.Second) go func() { for { select { case resContainer = <-resChan: sum += resContainer fmt.Println(“add”, resContainer) case <- success: fmt.Println(“result:”, sum) return case <- ctx.Done(): fmt.Println(“result:”, sum) return } } }() wg := sync.WaitGroup{} wg.Add(3) go func() { resChan <- microService1() wg.Done() }() go func() { resChan <- microService2() wg.Done() }() go func() { resChan <- microService3() wg.Done() }() wg.Wait() success <- 1 return}func main() { r := gin.New() r.GET("/calculate", calHandler) http.ListenAndServe(":8008", r)}func microService1() int { time.Sleep(1time.Second) return 1}func microService2() int { time.Sleep(2time.Second) return 2}func microService3() int { time.Sleep(10*time.Second) return 3}上面的程序只是简单描述了一个调用其他微服务超时的处理场景。实际过程中还需要加很多很多调料,才能保证接口的对外完整性。大家,讲究看下吧~啊哈哈哈哈 ...

January 13, 2019 · 3 min · jiezi

Golang并发模型:轻松入门协程池

goroutine是非常轻量的,不会暂用太多资源,基本上有多少任务,我们可以开多少goroutine去处理。但有时候,我们还是想控制一下。比如,我们有A、B两类工作,不想把太多资源花费在B类务上,而是花在A类任务上。对于A,我们可以来1个开一个goroutine去处理,对于B,我们可以使用一个协程池,协程池里有5个线程去处理B类任务,这样B消耗的资源就不会太多。控制使用资源并不是协程池目的,使用协程池是为了更好并发、程序鲁棒性、容错性等。废话少说,快速入门协程池才是这篇文章的目的。协程池指的是预先分配固定数量的goroutine处理相同的任务,和线程池是类似的,不同点是协程池中处理任务的是协程,线程池中处理任务的是线程。最简单的协程池模型上面这个图展示了最简单的协程池的样子。先把协程池作为一个整体看,它使用2个通道,左边的jobCh是任务通道,任务会从这个通道中流进来,右边的retCh是结果通道,协程池处理任务后得到的结果会写入这个通道。至于协程池中,有多少协程处理任务,这是外部不关心的。看一下协程池内部,图中画了5个goroutine,实际goroutine的数量是依具体情况而定的。协程池内每个协程都从jobCh读任务、处理任务,然后将结果写入到retCh。示例模型看懂了,看个小例子吧。workerPool()会创建1个简单的协程池,协程的数量可以通入参数n执行,并且还指定了jobCh和retCh两个参数。worker()是协程池中的协程,入参分布是它的ID、job通道和结果通道。使用for-range从jobCh读取任务,直到jobCh关闭,然后一个最简单的任务:生成1个字符串,证明自己处理了某个任务,并把字符串作为结果写入retCh。main()启动genJob获取存放任务的通道jobCh,然后创建retCh,它的缓存空间是200,并使用workerPool启动一个有5个协程的协程池。1s之后,关闭retCh,然后开始从retCh中读取协程池处理结果,并打印。genJob启动一个协程,并生产n个任务,写入到jobCh。示例运行结果如下,一共产生了10个任务,显示大部分工作都被worker 2这个协程抢走了,如果我们设置的任务成千上万,协程池长时间处理任务,每个协程处理的工作数量就会均衡很多。➜ go run simple_goroutine_pool.goworker 2 processed job: 4worker 2 processed job: 5worker 2 processed job: 6worker 2 processed job: 7worker 2 processed job: 8worker 2 processed job: 9worker 0 processed job: 1worker 3 processed job: 2worker 4 processed job: 3worker 1 processed job: 0回顾最简单的协程池模型就这么简单,再回头看下协程池及周边由哪些组成:协程池内的一定数量的协程。任务队列,即jobCh,存在协程池不能立即处理任务的情况,所以需要队列把任务先暂存。结果队列,即retCh,同上,协程池处理任务的结果,也存在不能被下游立刻提取的情况,要暂时保存。协程池最简要(核心)的逻辑是所有协程从任务读取任务,处理后把结果存放到结果队列。Go并发系列文章Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门selectGolang并发模型:select进阶Golang并发模型:轻松入门协程池如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。如果喜欢本文,随意转载,但请保留此原文链接。博客文章列表,点此可查看

December 20, 2018 · 1 min · jiezi

Golang并发模型:并发协程的优雅退出

goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。这篇文章介绍,如何合理的退出goroutine,减少软件bug。goroutine在退出方面,不像线程和进程,不能通过某种手段强制关闭它们,只能等待goroutine主动退出。但也无需为退出、关闭goroutine而烦恼,下面就介绍3种优雅退出goroutine的方法,只要采用这种最佳实践去设计,基本上就可以确保goroutine退出上不会有问题,尽情享用。1:使用for-range退出for-range是使用频率很高的结构,常用它来遍历数据,range能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束,接着退出for循环。它在并发中的使用场景是:当协程只从1个channel读取数据,然后进行处理,处理后协程退出。下面这个示例程序,当in通道被关闭时,协程可自动退出。go func(in <-chan int) { // Using for-range to exit goroutine // range has the ability to detect the close/end of a channel for x := range in { fmt.Printf(“Process %d\n”, x) }}(inCh)2:使用,ok退出for-select也是使用频率很高的结构,select提供了多路复用的能力,所以for-select可以让函数具有持续多路处理多个channel的能力。但select没有感知channel的关闭,这引出了2个问题:继续在关闭的通道上读,会读到通道传输数据类型的零值,如果是指针类型,读到nil,继续处理还会产生nil。继续在关闭的通道上写,将会panic。问题2可以这样解决,通道只由发送方关闭,接收方不可关闭,即某个写通道只由使用该select的协程关闭,select中就不存在继续在关闭的通道上写数据的问题。问题1可以使用,ok来检测通道的关闭,使用情况有2种。第一种:如果某个通道关闭后,需要退出协程,直接return即可。示例代码中,该协程需要从in通道读数据,还需要定时打印已经处理的数量,有2件事要做,所有不能使用for-range,需要使用for-select,当in关闭时,ok=false,我们直接返回。go func() { // in for-select using ok to exit goroutine for { select { case x, ok := <-in: if !ok { return } fmt.Printf(“Process %d\n”, x) processedCnt++ case <-t.C: fmt.Printf(“Working, processedCnt = %d\n”, processedCnt) } }}()第二种:如果某个通道关闭了,不再处理该通道,而是继续处理其他case,退出是等待所有的可读通道关闭。我们需要使用select的一个特征:select不会在nil的通道上进行等待。这种情况,把只读通道设置为nil即可解决。go func() { // in for-select using ok to exit goroutine for { select { case x, ok := <-in1: if !ok { in1 = nil } // Process case y, ok := <-in2: if !ok { in2 = nil } // Process case <-t.C: fmt.Printf(“Working, processedCnt = %d\n”, processedCnt) } // If both in channel are closed, goroutine exit if in1 == nil && in2 == nil { return } }}()3:使用退出通道退出使用,ok来退出使用for-select协程,解决是当读入数据的通道关闭时,没数据读时程序的正常结束。想想下面这2种场景,,ok还能适用吗?接收的协程要退出了,如果它直接退出,不告知发送协程,发送协程将阻塞。启动了一个工作协程处理数据,如何通知它退出?使用一个专门的通道,发送退出的信号,可以解决这类问题。以第2个场景为例,协程入参包含一个停止通道stopCh,当stopCh被关闭,case <-stopCh会执行,直接返回即可。当我启动了100个worker时,只要main()执行关闭stopCh,每一个worker都会都到信号,进而关闭。如果main()向stopCh发送100个数据,这种就低效了。func worker(stopCh <-chan struct{}) { go func() { defer fmt.Println(“worker exit”) // Using stop channel explicit exit for { select { case <-stopCh: fmt.Println(“Recv stop signal”) return case <-t.C: fmt.Println(“Working .”) } } }() return}最佳实践回顾发送协程主动关闭通道,接收协程不关闭通道。技巧:把接收方的通道入参声明为只读,如果接收协程关闭只读协程,编译时就会报错。协程处理1个通道,并且是读时,协程优先使用for-range,因为range可以关闭通道的关闭自动退出协程。,ok可以处理多个读通道关闭,需要关闭当前使用for-select的协程。显式关闭通道stopCh可以处理主动通知协程退出的场景。完整示例代码本文所有代码都在仓库,可查看完整示例代码:https://github.com/Shitaibin/…并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 4, 2018 · 1 min · jiezi