大话 goroutine

2024-06-12 15:38
文章标签 大话 goroutine

本文主要是介绍大话 goroutine,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

goroutine本质上是大号版的异步执行句柄,比之nodejs中的单线程事件循环处理器。之所以在使用goroutine,感觉不到异步,在于golang已经封装了各种异步io操作,运行时一旦发现异步io状态发生改变,则适时进行goroutine切换。让你基本上感觉不到像基于事件编程所带来的直观上的任务执行乱序。

启动 VS 执行

goroutine 这种由运行时控制,构建于线程之上,又比线程粒度小的操作,操作系统一点也不care。通常分配给线程内存会在8M左右,goroutine则只有2kb,一个MB一个KB完全在不同级别,单单性能上就提高很多。结论就是,在任务调度上,goroutine是弱于线程的,但是在资源消耗上,goroutine则是极低的。

异步编程为了追求程序的性能,强行的将线性的程序打乱,程序变得非常的混乱与复杂。对程序状态的管理也变得异常困难。接触网络编程的同学,很容易理解启动与执行完全是两个概念。

	sum()    // 同步执行普通函数,等待它执行完毕
go sum()   // 用go关键字启动一个goroutine立即返回,它会异步执行

换而言之,普通函数是即调即用,用go关键字修饰的函数只是启动立即返回,在当前线程空闲的时候(main函数是一个特殊的goroutine,一旦它没空,意味着你的gorotuine哪怕启动了,也是作不出什么妖来。)作异步执行。更直白点,你看到的goroutine是它启动的样子,它的异步执行由运行时所决定,groutine与goroutine执行顺序,还是不会忘本,回到异步执行流乱序的常态。

创建goroutine

示例使用了select 来阻塞主线程,若使用sleep,则本身是不知道多长时间是恰到好处。
太长浪费资源,太短则有可能所请求的网站还没来得及响应,主线程就退出了,达不到请求目标网站的目的。

package mainimport ("fmt""io/ioutil""log""net/http""time"
)func responseSize(url string) {fmt.Println("Step1: ", url)response, err := http.Get(url)if err != nil {log.Fatal(err)}fmt.Println("Step2: ", url)defer response.Body.Close()fmt.Println("Step3: ", url)body, err := ioutil.ReadAll(response.Body)if err != nil {log.Fatal(err)}fmt.Println("Step4: ", len(body))
}func main() {go responseSize("https://www.stackoverflow.com")go responseSize("https://www.shiyanlou.com")go responseSize("https://www.baidu.com")select {}
}

执行 go run main.go。查看4个阶段响应,字节长度信息,以三个响应速度不一,响应资源不等网站为例。每次print都有io切换(即goroutine在运行时指挥下自动切换)。很明显goroutine 的输出是无序的,与goroutine的启动顺序无关。

Step1:  https://www.baidu.com
Step1:  https://www.stackoverflow.com
Step1:  https://www.shiyanlou.com
Step2:  https://www.baidu.com
Step3:  https://www.baidu.com
Step4:  227
Step2:  https://www.shiyanlou.com
Step3:  https://www.shiyanlou.com
Step4:  196083
Step2:  https://www.stackoverflow.com
Step3:  https://www.stackoverflow.com
Step4:  116044

等待goroutine执行完毕

sync.WaitGroup 在此是同步阻塞主线程,等待一组goroutine执行完毕

Add 方法向 WaitGroup 实例设置默认计数,Done 减少一个,Wait 方法负责阻塞等待其它goroutine执行完毕。

...
func responseSize(url string) {  defer wg.Done()  ...
}var wg sync.WaitGroup      
func  main() {       wg.Add(3)     ...wg.Wait()    fmt.Println("terminate Program")    
}  

从goroutine中取值

goroutine 间通常使用channel,类似于linux中的pipe管道,来实现通信。

 package mainimport ("fmt""io/ioutil""log""net/http""sync")var wg sync.WaitGroupfunc responseSize(url string, nums chan<- int) {defer wg.Done()response, err := http.Get(url)if err != nil {log.Fatal(err)}defer response.Body.Close()body, err := ioutil.ReadAll(response.Body)if err != nil {log.Fatal(err)}nums <- len(body)      // 将值写入通道}func main() {nums := make(chan int)   // 声明通道wg.Add(3)go responseSize("https://www.stackoverflow.com", nums)fmt.Println(<-nums)    //  发送数据wg.Wait()    close(nums)   // 关闭通道

控制goroutine执行

使用通道可以控制goroutine的执行与暂停,通道方便goroutine间通信

package mainimport ("fmt""sync""time"
)var i intfunc work() {time.Sleep(250 * time.Millisecond)i++fmt.Println(i)
}func routine(command <-chan string, wg *sync.WaitGroup) {defer wg.Done()var status = "Play"for {select {case cmd := <-command:fmt.Println(cmd)switch cmd {case "Stop":returncase "Pause":status = "Pause"default:status = "Play"}default:if status == "Play" {work()}}}
}func main() {var wg sync.WaitGroupwg.Add(1)command := make(chan string)go routine(command, &wg)time.Sleep(1 * time.Second)command <- "Pause"time.Sleep(1 * time.Second)command <- "Play"time.Sleep(1 * time.Second)command <- "Stop"wg.Wait()
}

运行结果

1
2
3
4
Pause
Play
5
6
7
8
9
Stop

原子函数修复条件竞争

争用条件是由于对共享资源的不同步访问而引起。原子函数提供了用于同步访问整数和指针的底层锁定机制。同步包下的atomic中的函数通过锁定对共享资源的访问来提供支持同步goroutine的支持。

package mainimport ("fmt""runtime""sync""sync/atomic"
)var (counter int32wg      sync.WaitGroup
)func main() {wg.Add(3)go increment("python")go increment("java")go increment("Golang")wg.Wait()fmt.Println("Counter:", counter)
}func increment(name string) {defer wg.Done()for range name {atomic.AddInt32(&counter, 1)runtime.Gosched()}
}

使用原子函数对值时,它会强制一次有且仅一个goroutine能够完成值修改操作。当别goroutine试图调用任何原子函数时,会自动同步所对应引用的变量。

mutex定义临界区

mutex通常被用来定义临界区,确保该区域的代码一次只能被一个goroutine访问执行。

package mainimport ("fmt""sync"
)var (counter int32wg      sync.WaitGroupmutex   sync.Mutex
)func main() {wg.Add(3)go increment("Python")go increment("Go Programming Language")go increment("Golang")wg.Wait()fmt.Println("Counter:", counter)
}func increment(lang string) {defer wg.Done()for i := 0; i < 3; i++ {mutex.Lock(){fmt.Println(lang)counter++}mutex.Unlock()}
}

去掉锁,执行下面语句,分析数据竞争

go run -race main.go

goroutine并发

解决数据争用,需要确保一次请求时仅有一个goroutine在使用临界资源。用消息传递或用锁机制。
每个gorotuine在自己的领域内都是一个独立的个体,goroutine间的地位是平等的,没有谁比谁更高贵。

消息传递,你不用向我请求,我有了消息会通知你。goroutine的启动类似于到医院挂号,院方会在合适的时候给你看病(让goroutine运行)。golang的通道则典型用了这种消息传递机制。

用锁,简单粗暴。其道理好比,将goroutine间原先对临界资源的争用,转移到对锁的抢夺上。有锁就有权使用。存在的问题在于,如果抢锁的人多,系统发锁/收锁需要将通知到即将使用临界资源的所有人。成本太高,即便改进产生的变种读写锁,条件锁等,终归太重了。

二者的区别,在于临界资源的使用方。用锁是使用者主动出手,干预共享资源。用消息传递,一切听指挥,你要用事先向上级报备,上级视情况通知你去用。典型的走过去,与送过来的关系。有点控制反转的味道。

goroutine异步,乱序执行其意义在于最大程度压榨CPU性能。带来的问题,如同nodejs回调地狱般可恶,在调式编程不易理解。为了协同各个goroutine有效工作,需要一些同步手段。如其它语言并发编程中一般,go也搞出常规的原子函数,读写锁,WaitGroup,条件锁等同步,但这已经满足不了需求(太麻烦),golang在语言级别整出了通道channel。

Context

细细思之,goroutine本质上相当于半独立的行省,各行其是,会天下大乱。goroutine与groutine之间也存在从属关系(父子共存),竞争关系(谁行谁上),互斥关系(有你没我) 等。为了协调一组goroutine共同做大事,golang1.7之后将实验性质的 context 包归并了主库,其主要功能在于产生一组context上下文树,实现了一堆有关系的goroutine协同工作,比如A协程生了B,B又产生的C程。这种子子孙孙无穷尽也的趋势,你就无法使用waitgroup(因其一开始对要启动的goroutine数目不确定)。借鉴了linux父子进程,goroutine小号版的进程形式,context包提供了这种树形关系,比如父goroutine挂了之前,会先通知其子挂,同理其子也会通知到时孙辈…

不仅如此,context 还提供了超时,截止日期,让其从属goroutine在符合待定条件下,自行消亡,而不致于一直占着茅坑不拉屎,最后出现因瞬时请求量巨大,服务端积存了大量未死亡(默认超时时长比较大)的goroutine,而耗尽了资源,一不小心将你本来弱小的服务搞死的局面。

context当然也提供了一个value接口,用于存储值。类似于vue的数据流下发,避免频繁在多个goroutine间拷贝数据。其在web请求处理方面有积极意义,好比用过微信的token同学知道,其存在token作用域(token请求存在范围限制0,用这个接口你可以存储一些带作用域的请求标识,这样在web网络通信时,就可基于请求转发到不同组的goroutine进行一系列处理。

这篇关于大话 goroutine的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1054625

相关文章

适配器模式(大话设计模式)C/C++版本

适配器模式 C++ #include <iostream>using namespace std;// 球员class Player{protected:string name;public:Player(string name) : name(name) {}virtual void Attack() = 0;virtual void Defense() = 0;virtual

备忘录模式(大话设计模式)C/C++版本

备忘录模式 C++ #include <iostream>#include <string>using namespace std;// Memento类,备忘录,此处为角色状态存储箱class RoleStateMemento{private:int m_vit; // 生命力int m_atk; // 攻击力int m_def; // 防御力public:RoleState

关于goroutine的一些小理解

前言 今天看一个大神的代码,看到了runtime.Gosched(),瞬间把我看懵了。。 后面梳理了下,其实这个竟然之前学习goroutine的时候没学到,所以顺便记录下,以便日后留恋。 知识点 理解这个goroutine必须要知道以下三大块东西。 进程、线程、协程并发、并行Golang中的M、P、G 我会用非常通熟易懂的方式来进行说明。(我也怕很烦的解释,不利于记忆啊!) 进程、

大话C语言:第25篇 动态库

1 动态库概述         C语言动态库(也称为共享库)是在程序运行时被加载到内存中的库文件,它包含了可由多个程序共享的代码和数据。动态库在编译时不会被直接链接到目标程序中,而是在程序运行时动态加载。这种特性使得动态库具有一些优势,如节省磁盘空间、便于更新和维护、以及支持多个程序同时共享库代码。         动态库通常以.so(在Linux和类Unix系统中)或.dll(在Window

《大话数据结构》最小生成树——Kruskal算法

/*2014-6-24思想:n个节点的图中,只需要找到权值最小且不与现有边集合构成环的(n-1)条边,必成最小生成树。方案:将边的权值进行筛选,每次找到权值最小的边,补充道边集合中即可。难点:如何确保这些边不构成环——对每个边,让其起始节点是祖先,通过洄游寻根,如果祖先相同说明两个节点是“近亲”,会构成闭环:A-B-C-A三角形中:1. A-B边中确定B的祖先和父亲都是A;2. B-C边中,确定C

Linkin大话Java和internet概念

整理电脑,无意中翻到不知道哪里来的文章,觉得里面写的很好,仔细看过一遍后,整理了下贴了出来,其中的好多概念我觉得讲的很透彻。 既然Java不过另一种类型的程序设计语言,为什么还有这么多的人认为它是计算机程序设计的一个里程碑? Java除了可解决传统的程序设计问题以外,还能解决World Wide Web(万维网)上的编程问题。java比较高级的应用有2块:一块是我现在正在玩的j2

linkin大话设计模式--常用模式总结

linkin大话设计模式--常用模式总结 一,常用设计模式定义 Abstract Factory(抽象工厂模式):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 Adapter(适配器模式):将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 Bridge(桥接模式):将抽象部分与它的实现部分分离,

linkin大话设计模式--适配器模式

linkin大话设计模式--适配器模式 大家知道,在java中只允许单继承,但是在实际问题中往往都需要多继承,java引入了接口这一概念。(一个类可以实现多个接口) 由于接口中都是抽象方法,那么我们在实现它的时候就必须全部覆写这些方法。假如我有一个类,这个类只想覆写一部份方法怎么办?   在接口与这个类中间可以加一个抽象类: 抽象类去覆写接口中的全部方法,而那个类去继

观察者模式(大话设计模式)C/C++版本

观察者模式 扩展:观察者模式——委托 C++ 参考:https://www.cnblogs.com/Galesaur-wcy/p/15905936.html #include <iostream>#include <list>#include <memory>#include <string>using namespace std;// Observer类 抽象观察者,为所有的

抽象工厂模式(大话设计模式)C/C++版本

抽象工厂模式 C++ 参考:https://www.cnblogs.com/Galesaur-wcy/p/15927110.html #include <iostream>using namespace std;// 抽象产品Department ,定义具体产品的公共接口class Department{public:virtual ~Department() = default