Go Routine使用总结与并发使用同步方法

2024-02-26 03:38

本文主要是介绍Go Routine使用总结与并发使用同步方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

GO对于高并发有着非常良好的支持能力,而GO实现高并发的两个主要工具便是Go Routine和Channel。这里主要对Go Routine进行介绍。

阅读后续之前请先意识到:不要用自己的想法推测编译器的执行顺序,编译器的执行顺序是完全不确定的!

基本概念

一般来说,协程就像轻量级的线程。

线程一般有固定的栈,有一个固定的大小。而goroutines为了避免资源浪费(亦或是资源缺乏),采用动态扩张收缩的策略:初始量为2k,最大可以扩张到1G。

另外,线程/goroutine 切换开销方面,goroutine 远比线程小

线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等。

因为协程在用户态由协程调度器完成,不需要陷入内核,所以goroutine只有三个寄存器的值修改 - PC / SP / DX,代价相对小很多

运行机制

go协程与一般的语言相比有很多有趣的特性,例如如下代码:

func hello() {fmt.Printf("Hello World!")
}func main() {go hello()
}

预期执行结果为输出“Hello World”,但结果为空

原因:函数执行期间启动go routine,函数并不会等待go routine运行完毕才退出,而是会直接退出

修改方式:对代码进行修改,增加main函数等待时间后即可看到正常结果

func hello() {fmt.Printf("Hello World!")
}func main() {go hello()time.Sleep(3)
}

当然,函数本身退出后子协程依然会继续运行直到运行结束或程序不在给他分配资源为止。上述案例无法看到现象是因为主函数会直接退出并使得系统清空全部分配的运行资源。

换为下面程序:

func hello() {go fmt.Printf("Hello World!\n")
}func main() {hello()fmt.Printf("hello function exit\n")time.Sleep(10)
}

会发现结果为:

即函数退出后子协程依然在正常运行

同步方法 

接下来的文字主要是对Go官方文档关于Go Memory Model的说明

参考链接:https://golang.org/ref/memhttps://golang.org/ref/mem

If you must read the rest of this document to understand the behavior of your program, you are being too clever.

Don't be clever.

一段很有意思的话,个人理解是,写出正确的代码是重要的,不要过分纠结于炫技,也不要过分纠结于为何错误的代码是错误的。

Happens Before

首先正如当初学编译器提到的那样,很多时候我们写出来的代码的执行顺序并不是编译器编译的执行顺序。实际上很多时候,编译器会自动对语句的执行顺序进行调整优化,以更好利用寄存器信息。因此即使一开始写为a=1;b=1;,程序也可能先设置b再设置a。

所以,语句的执行顺序很多时候是不确定的,不要先入为主

主要同步机制

GO主要通过channel和锁对程序的执行顺序进行同步

1、Channel同步机制

var c = make(chan int)
var a stringfunc f() {a = "hello, world"c <- 0
}func main() {go f()<-cprint(a)
}

或者把channel的读入读出顺序进行更改

var c = make(chan int)
var a stringfunc f() {a = "hello, world"<-c
}func main() {go f()c <- 0print(a)
}

都可以保证得到预期的"hello, world"输出结果。这是因为channel没有缓存,必须读进去一个并立刻输出才行,否则会阻塞在对应语句处。

如果将channel换成有缓存的channel,例如

var c = make(chan int, 1)
var a stringfunc f() {a = "hello, world"<-c
}func main() {go f()c <- 0print(a)
}

就无法保证得到正确结果,因为channel可以将输入的0缓存后直接退出main函数

2、Lock同步机制

var l sync.Mutex
var a stringfunc f() {a = "hello, world"l.Unlock()
}func main() {l.Lock()go f()l.Lock()print(a)
}

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.

Go语言的设计机制保证了存在多对时,Unlock必定在Lock之后执行

如果在lock之前就执行unlock,将会得到如下结果:

func main() {mu.Unlock()
}fatal error: sync: unlock of unlocked mutex

程序直接报错

其他同步方法

1、Once的使用

var a string
var once sync.Oncefunc setup() {a = "hello, world"
}func doprint() {once.Do(setup)print(a)
}func twoprint() {go doprint()go doprint()
}

Once保证了一个函数只能执行一次

2、WaitGroup

func main() {wg := sync.WaitGroup{}wg.Add(100)for i := 0; i < 100; i++ {go f(i, &wg)}wg.Wait()
}// 一定要通过指针传值,不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) { fmt.Println(i)wg.Done()
}

通过WaitGroup可以准确统计协程数量,以便在全部协程结束后才进行后续操作

必须注意的情况示例

var a string
var done boolfunc setup() {a = "hello, world"done = true
}func doprint() {if !done {once.Do(setup)}print(a)
}func twoprint() {go doprint()go doprint()
}

不要想当然按照自己的想法来理解编译器的习惯,很多时候执行顺序是不确定的。即使a在done前面,读到done的结果也不代表能读到a的结果。

Go并发编程范式

上面主要讲解了Go的一些编程范式

下面将介绍GO常见的一些并发写法

Wg的通常写法:启动协程时需要把参数一起进行传入 

这样能得到正确结果,例如42315

如果启动协程时不采用go func(var){}(value)的形式,如下所示:

那么无法得到正确结果(例如45555)。

原因为:go func运行时读取的i的值已经进行了修改

这是一种很糟糕的设计,逻辑上是没有问题的,但是因为不断轮训导致CPU负载过高,单核CPU可能会迅速冲击到100%

解决方法:以毫秒为单位在for循环最后加一个时间等待函数

* 特别值得注意的一种语法:sync.NewCond

代码示例:

代码范式:

进一步提升了performance,避免出现了长时间不更新,每次轮询都是在空等待的情况出现几率

只有当cond进行了broadcast后才会唤起cond.Wait条件,进行后续处理

补充说明:这里值得注意的一个点是,cond里的锁是mu

初始声明:cond = sync.NewCond(&mu)

所以实际上cond里操作的锁就是mu,也因此才能实现后面的操作,打通两边的环节

具体说明可以参见下面的博客:

Golang sync.Cond详细理解_skh2015java的博客-CSDN博客_golang sync.cond

另外这里有需要担心的地方

理想情况是,先是进入wait for循环进入,然后wait和broadcast交互处理,节省CPU空转时间

但是有个问题,上面 broadcast的10个for循环连着跑完怎么办?

答:没影响,这样的话下面的for循环无法进入,直接加锁放锁而没有进入 wait的for循环,依然可以实现我们的目的

关于channel错误代码示例:

这个程序会直接堵住

程序串行执行,到第二步的时候直接堵死,无法走到第三行

channel在执行过程中需要有另一个go routine对channel进行处理,否则会堵住

用于让程序定量执行

例如:

写代码时经常一个不小心就陷入死锁,要注意规避死锁的情况出现,如下所示:

是否需要加锁、需要持有锁的时间等问题都需要进行斟酌

defer与defer func(){}的区别

核心在于对闭包的理解

func main() {var a = 1var b = 2defer fmt.Println(a + b)a = 2fmt.Println("main")
}


输出:

main
3
稍微修改一下,再看看:

func main() {var a = 1var b = 2defer func() {fmt.Println(a + b)}()a = 2fmt.Println("main")
}


输出:

main
4
结论:闭包获取变量相当于引用传递,而非值传递。

稍微再修改一下,再看看:

func main() {var a = 1var b = 2defer func(a int, b int) {fmt.Println(a + b)}(a, b)a = 2fmt.Println("main")
}


输出:

main
3
结论:传参是值复制。

还可以理解为:defer 调用的函数,参数的值在 defer 定义时就确定了,看下代码

defer fmt.Println(a + b),在这时,参数的值已经确定了。

而 defer 函数内部所使用的变量的值需要在这个函数运行时才确定,看下代码

defer func() { fmt.Println(a + b) }(),a 和 b 的值在函数运行时,才能确定。

参考资料

golang中协程和线程的区别 - 简书

使用 defer 函数 要注意的几个点_gr32442187do的博客-CSDN博客

这篇关于Go Routine使用总结与并发使用同步方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

C++中使用vector存储并遍历数据的基本步骤

《C++中使用vector存储并遍历数据的基本步骤》C++标准模板库(STL)提供了多种容器类型,包括顺序容器、关联容器、无序关联容器和容器适配器,每种容器都有其特定的用途和特性,:本文主要介绍C... 目录(1)容器及简要描述‌php顺序容器‌‌关联容器‌‌无序关联容器‌(基于哈希表):‌容器适配器‌:(

Python判断for循环最后一次的6种方法

《Python判断for循环最后一次的6种方法》在Python中,通常我们不会直接判断for循环是否正在执行最后一次迭代,因为Python的for循环是基于可迭代对象的,它不知道也不关心迭代的内部状态... 目录1.使用enuhttp://www.chinasem.cnmerate()和len()来判断for

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

使用Python合并 Excel单元格指定行列或单元格范围

《使用Python合并Excel单元格指定行列或单元格范围》合并Excel单元格是Excel数据处理和表格设计中的一项常用操作,本文将介绍如何通过Python合并Excel中的指定行列或单... 目录python Excel库安装Python合并Excel 中的指定行Python合并Excel 中的指定列P

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可