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

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象