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

相关文章

Conda与Python venv虚拟环境的区别与使用方法详解

《Conda与Pythonvenv虚拟环境的区别与使用方法详解》随着Python社区的成长,虚拟环境的概念和技术也在不断发展,:本文主要介绍Conda与Pythonvenv虚拟环境的区别与使用... 目录前言一、Conda 与 python venv 的核心区别1. Conda 的特点2. Python v

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

C#中Guid类使用小结

《C#中Guid类使用小结》本文主要介绍了C#中Guid类用于生成和操作128位的唯一标识符,用于数据库主键及分布式系统,支持通过NewGuid、Parse等方法生成,感兴趣的可以了解一下... 目录前言一、什么是 Guid二、生成 Guid1. 使用 Guid.NewGuid() 方法2. 从字符串创建

Python使用python-can实现合并BLF文件

《Python使用python-can实现合并BLF文件》python-can库是Python生态中专注于CAN总线通信与数据处理的强大工具,本文将使用python-can为BLF文件合并提供高效灵活... 目录一、python-can 库:CAN 数据处理的利器二、BLF 文件合并核心代码解析1. 基础合

Python使用OpenCV实现获取视频时长的小工具

《Python使用OpenCV实现获取视频时长的小工具》在处理视频数据时,获取视频的时长是一项常见且基础的需求,本文将详细介绍如何使用Python和OpenCV获取视频时长,并对每一行代码进行深入解析... 目录一、代码实现二、代码解析1. 导入 OpenCV 库2. 定义获取视频时长的函数3. 打开视频文

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

SQL Server配置管理器无法打开的四种解决方法

《SQLServer配置管理器无法打开的四种解决方法》本文总结了SQLServer配置管理器无法打开的四种解决方法,文中通过图文示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录方法一:桌面图标进入方法二:运行窗口进入检查版本号对照表php方法三:查找文件路径方法四:检查 S

MyBatis-Plus 中 nested() 与 and() 方法详解(最佳实践场景)

《MyBatis-Plus中nested()与and()方法详解(最佳实践场景)》在MyBatis-Plus的条件构造器中,nested()和and()都是用于构建复杂查询条件的关键方法,但... 目录MyBATis-Plus 中nested()与and()方法详解一、核心区别对比二、方法详解1.and()

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注