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

相关文章

postgresql使用UUID函数的方法

《postgresql使用UUID函数的方法》本文给大家介绍postgresql使用UUID函数的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录PostgreSQL有两种生成uuid的方法。可以先通过sql查看是否已安装扩展函数,和可以安装的扩展函数

Java中Arrays类和Collections类常用方法示例详解

《Java中Arrays类和Collections类常用方法示例详解》本文总结了Java中Arrays和Collections类的常用方法,涵盖数组填充、排序、搜索、复制、列表转换等操作,帮助开发者高... 目录Arrays.fill()相关用法Arrays.toString()Arrays.sort()A

如何使用Lombok进行spring 注入

《如何使用Lombok进行spring注入》本文介绍如何用Lombok简化Spring注入,推荐优先使用setter注入,通过注解自动生成getter/setter及构造器,减少冗余代码,提升开发效... Lombok为了开发环境简化代码,好处不用多说。spring 注入方式为2种,构造器注入和setter

MySQL中比较运算符的具体使用

《MySQL中比较运算符的具体使用》本文介绍了SQL中常用的符号类型和非符号类型运算符,符号类型运算符包括等于(=)、安全等于(=)、不等于(/!=)、大小比较(,=,,=)等,感兴趣的可以了解一下... 目录符号类型运算符1. 等于运算符=2. 安全等于运算符<=>3. 不等于运算符<>或!=4. 小于运

使用zip4j实现Java中的ZIP文件加密压缩的操作方法

《使用zip4j实现Java中的ZIP文件加密压缩的操作方法》本文介绍如何通过Maven集成zip4j1.3.2库创建带密码保护的ZIP文件,涵盖依赖配置、代码示例及加密原理,确保数据安全性,感兴趣的... 目录1. zip4j库介绍和版本1.1 zip4j库概述1.2 zip4j的版本演变1.3 zip4

Python 字典 (Dictionary)使用详解

《Python字典(Dictionary)使用详解》字典是python中最重要,最常用的数据结构之一,它提供了高效的键值对存储和查找能力,:本文主要介绍Python字典(Dictionary)... 目录字典1.基本特性2.创建字典3.访问元素4.修改字典5.删除元素6.字典遍历7.字典的高级特性默认字典

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

Nginx安全防护的多种方法

《Nginx安全防护的多种方法》在生产环境中,需要隐藏Nginx的版本号,以避免泄漏Nginx的版本,使攻击者不能针对特定版本进行攻击,下面就来介绍一下Nginx安全防护的方法,感兴趣的可以了解一下... 目录核心安全配置1.编译安装 Nginx2.隐藏版本号3.限制危险请求方法4.请求限制(CC攻击防御)

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

一文详解如何使用Java获取PDF页面信息

《一文详解如何使用Java获取PDF页面信息》了解PDF页面属性是我们在处理文档、内容提取、打印设置或页面重组等任务时不可或缺的一环,下面我们就来看看如何使用Java语言获取这些信息吧... 目录引言一、安装和引入PDF处理库引入依赖二、获取 PDF 页数三、获取页面尺寸(宽高)四、获取页面旋转角度五、判断