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

相关文章

使用Python和OpenCV库实现实时颜色识别系统

《使用Python和OpenCV库实现实时颜色识别系统》:本文主要介绍使用Python和OpenCV库实现的实时颜色识别系统,这个系统能够通过摄像头捕捉视频流,并在视频中指定区域内识别主要颜色(红... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间详解

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

Python常用命令提示符使用方法详解

《Python常用命令提示符使用方法详解》在学习python的过程中,我们需要用到命令提示符(CMD)进行环境的配置,:本文主要介绍Python常用命令提示符使用方法的相关资料,文中通过代码介绍的... 目录一、python环境基础命令【Windows】1、检查Python是否安装2、 查看Python的安

Python并行处理实战之如何使用ProcessPoolExecutor加速计算

《Python并行处理实战之如何使用ProcessPoolExecutor加速计算》Python提供了多种并行处理的方式,其中concurrent.futures模块的ProcessPoolExecu... 目录简介完整代码示例代码解释1. 导入必要的模块2. 定义处理函数3. 主函数4. 生成数字列表5.

Python中help()和dir()函数的使用

《Python中help()和dir()函数的使用》我们经常需要查看某个对象(如模块、类、函数等)的属性和方法,Python提供了两个内置函数help()和dir(),它们可以帮助我们快速了解代... 目录1. 引言2. help() 函数2.1 作用2.2 使用方法2.3 示例(1) 查看内置函数的帮助(

Linux脚本(shell)的使用方式

《Linux脚本(shell)的使用方式》:本文主要介绍Linux脚本(shell)的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述语法详解数学运算表达式Shell变量变量分类环境变量Shell内部变量自定义变量:定义、赋值自定义变量:引用、修改、删

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

Python中使用uv创建环境及原理举例详解

《Python中使用uv创建环境及原理举例详解》uv是Astral团队开发的高性能Python工具,整合包管理、虚拟环境、Python版本控制等功能,:本文主要介绍Python中使用uv创建环境及... 目录一、uv工具简介核心特点:二、安装uv1. 通过pip安装2. 通过脚本安装验证安装:配置镜像源(可

LiteFlow轻量级工作流引擎使用示例详解

《LiteFlow轻量级工作流引擎使用示例详解》:本文主要介绍LiteFlow是一个灵活、简洁且轻量的工作流引擎,适合用于中小型项目和微服务架构中的流程编排,本文给大家介绍LiteFlow轻量级工... 目录1. LiteFlow 主要特点2. 工作流定义方式3. LiteFlow 流程示例4. LiteF

使用Python开发一个现代化屏幕取色器

《使用Python开发一个现代化屏幕取色器》在UI设计、网页开发等场景中,颜色拾取是高频需求,:本文主要介绍如何使用Python开发一个现代化屏幕取色器,有需要的小伙伴可以参考一下... 目录一、项目概述二、核心功能解析2.1 实时颜色追踪2.2 智能颜色显示三、效果展示四、实现步骤详解4.1 环境配置4.