【Go】三、Go并发编程

并发编程

我们主流的并发编程思路一般有:多进程、多线程

但这两种方式都需要操作系统介入,进入内核态,是十分大的时间开销

由此而来,一个解决该需求的技术出现了:用户级线程,也叫做 绿程、轻量级线程、协程

python - asyncio、java - netty22111111111111115

由于 go 语言是 web2.0 时代发展起来的语言,go语言没有多线程和多进程的写法,其只有协程的写法 golang - goroutine

func Print() {fmt.Println("打印印")
}func main() {go Print()
}

我们可以使用这种方式来进行并发编程,但这个程序里要注意,我们主程序在确定完异步之后结束,会立即让程序退出,这就导致我们并发的子线程没来得及执行就退出了。

我们可以增加一个Sleep来让主线程让出资源,等待子线程执行完毕再进行操作

func Print() {for {time.Sleep(time.Second)fmt.Println("打印印")}}func main() {go Print()for {time.Sleep(time.Second)fmt.Println("主线程")}
}

另外的,Go 语言协程的一个巨大优势是 可以打开成百上千个协程,协助程序效率的提升

要注意一个问题:

多进程的切换十分浪费时间,且及其浪费系统资源

多线程的切换也很浪费时间,但其解决了浪费系统资源的问题

协程既解决了切换浪费时间的问题,也解决了浪费系统资源的问题

Go语言仅支持协程

Go语言中,协程的调度(gmp机制):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用户新建的协程(Goroutine)会被加入到同一调度器中等待运行,若调度器满,则会将调度器中一半的 G 加入到全局队列中,其他 P 若没有 G 则会从其他 P 中偷取一半的 G ,若所有的都满,则会新建 M 进行处理

P 的数量是固定的

注意 M 和 P 不是永远绑定的,当一个 P 现在绑定的 M 进入了阻塞等情况,P 会自动去寻找空闲的 M 或创建新的 M 来绑定

子 goroutine 如何通知到主 goroutine 其运行状态?也就是我们主协程要知道子协程运行完毕之后再进行进一步操作,也就是 (wait)

func main() {// 定义 sync.Group 类型的变量用于控制goroutine的状态var wg sync.WaitGroupfor i := 0; i < 100; i++ {go func(i int) {wg.Add(1)       // 每次启动一个协程都要开启一个计数器defer wg.Done() // 每次结束之前都要让计数器 -1fmt.Println("这次打印了:" + strconv.Itoa(i))}(i)}wg.Wait()fmt.Println("结束..................................")
}

Go语言中的锁

互斥锁

我们看下面这个程序:

var total int
var wg sync.WaitGroupfunc add() {defer wg.Done()for i := 0; i <= 100000; i++ {total += 1}
}func sub() {defer wg.Done()for i := 0; i <= 100000; i++ {total -= 1}
}func main() {wg.Add(2)go add()go sub()wg.Wait()fmt.Println(total)/*-10000110000168595...*/
}

我们发现这个程序的结果不可预知,这是因为 a 的操作分为三步:

取得 a 的值、执行 a 的计算操作、写入 a 的值,这三步不是原子性的,如果发生了交叉,则一个数准备写入时发生协程切换,这时后面再做再多的操作,也会被这次切换屏蔽掉,最终写入这一个数的结果,由此可知,这种非原子性操作共享数据的模式是不可预知结果的。

那么,我们就需要加锁,像下面这样

var total int
var wg sync.WaitGroup
var lock sync.Mutex
var lockc = &lockfunc add() {defer wg.Done()for i := 0; i <= 100000; i++ {lock.Lock() // 加锁,直至见到自己这把锁的 Unlock() 方法之前,令这中间的方法都为原子性的total += 1lock.Unlock() // 解锁,配合 Lock() 方法使用}
}func sub() {defer wg.Done()for i := 0; i <= 100000; i++ {lockc.Lock()total -= 1lockc.Unlock()}
}func main() {wg.Add(2)go add()go sub()wg.Wait()fmt.Println(total)fmt.Printf("%p\n", &lock)fmt.Printf("%p\n", &(*lockc))
}

注意,上面这个程序不仅演示了加锁,还演示了,浅拷贝不影响加锁的情况

另外,我们也可以使用 automic 对简单的数值计算进行加锁

var total int32
var wg sync.WaitGroup
var lock sync.Mutex
var lockc = &lockfunc add() {defer wg.Done()for i := 0; i <= 100000; i++ {atomic.AddInt32(&total, 1)}
}func sub() {defer wg.Done()for i := 0; i <= 100000; i++ {atomic.AddInt32(&total, -1)}
}func main() {wg.Add(2)go add()go sub()wg.Wait()fmt.Println(total)
}

读写锁

读写锁就是:允许同时读,不允许同时写,不允许同时读写

func main() {var num intvar rwlock sync.RWMutex // 定义一个读写锁var wg sync.WaitGroup   // 定义等待处理器wg.Add(2)go func() {defer wg.Done()rwlock.Lock() // 写锁defer rwlock.Unlock()num = 12}()// 同步处理器,这里是简便处理time.Sleep(1)go func() {defer wg.Done()rwlock.RLock()defer rwlock.RUnlock()fmt.Println(num)}()wg.Wait()}

一个简单的测试

func main() {var rwlock sync.RWMutex // 定义一个读写锁var wg sync.WaitGroup   // 定义等待处理器wg.Add(6)go func() {time.Sleep(time.Second)defer fmt.Println("释放写锁,可以进行读操作")defer wg.Done()rwlock.Lock() // 写锁defer rwlock.Unlock()fmt.Println("得到写锁,停止读操作")time.Sleep(time.Second * 5)}()for i := 0; i < 5; i++ {go func() {defer wg.Done()for {rwlock.RLock()fmt.Println("得到读锁,进行读操作")time.Sleep(time.Millisecond * 500)rwlock.RUnlock()}}()}wg.Wait()/**得到读锁,进行读操作得到写锁,停止读操作释放写锁,可以进行读操作得到读锁,进行读操作得到读锁,进行读操作得到读锁,进行读操作*/}

通信

Go 语言中对于并发场景下的通信,秉持以下理念:

不要通过共享内存来通信,要通过通信实现共享内存

其他语言都是用一个共享的变量来实现通信,或者消息队列,Go语言就希望实现队列的机制

	var msg chan string //定义一个用于传递 string 的 channel// 创建一个缓冲区大小为 1 的 channel// 只有 有缓冲区的 channel 才可以暂存数据msg = make(chan string, 1)msg <- "data"data := <-msgfmt.Println(data)

只有 goroutine 中才可以使用缓冲区大小为 0 的channel

func main() {var msg chan string //定义一个用于传递 string 的 channel// 创建一个缓冲区大小为 1 的 channel// 只有 有缓冲区的 channel 才可以暂存数据msg = make(chan string, 0)go func(msg chan string) {data := <-msgfmt.Println(data)}(msg)msg <- "data"time.Sleep(time.Second * 3)
}

这时由于 go 语言 channel 中的 happen-before 机制,该机制保证了 就算先 receiver 也会被 goroutine 挂起,等待 sender 完成之后再进行 receiver 的具体执行

go 语言中,channel 的应用场景十分广泛,包括:

  • 信息传递、消息过滤
  • 信号广播
  • 事件订阅与广播
  • 任务分发
  • 结果汇总
  • 并发控制
  • 同步异步

Go 语言的消息接收问题

func main() {var msg chan string //定义一个用于传递 string 的 channel// 创建一个缓冲区大小为 1 的 channel// 只有 有缓冲区的 channel 才可以暂存数据msg = make(chan string, 0)go func(msg chan string) {// 注意这里,每一个接收消息的变量只能接收到一个消息,若有多条消息同时发送,则无法接收data := <-msgfmt.Println(data)math := <-msgfmt.Println(math)}(msg)msg <- "data"msg <- "math"time.Sleep(time.Second * 3)}

如果我们不知道消息会发送来多少,可以使用 for-range 进行监听:

func main() {var msg chan string //定义一个用于传递 string 的 channel// 创建一个缓冲区大小为 1 的 channel// 只有 有缓冲区的 channel 才可以暂存数据msg = make(chan string, 2)go func(msg chan string) {// 若我们不确定有多少消息会过来,我们可以使用 for-range 进行循环验证for data := range msg {fmt.Println(data)}}(msg)msg <- "data"msg <- "math"time.Sleep(time.Second * 3)}
close(msg) // 关闭队列,监听队列的 goroutine 会立刻退出

关闭了的 channel 不能再存储数据,但可以进行数据的取出操作

上面我们所接触的 channel 都是双向的 channel 即这个channel 对应的goroutine 既可以从里面读数据,也可以向里面写数据,这种不符合我们程序,一个程序只做它对应的一个功能,这一程序设计思路

创建单向 channel:

	var ch1 chan int       // 这是一个双向 channelvar ch2 chan<- float64 // 这是一个只能写入 float64 类型数据的单向 channelvar ch3 <-chan int     // 这是一个只能从 存储int型 channel中读取数据的单向channel
	c := make(chan int, 3)     // 创建一个双向 channelvar send chan<- int = c    // 将 c channel 的写入能力赋予给 send,使其成为一个单向发送的 channel (生产者)var receive <-chan int = c // 将c channel 的读取能力赋予给receive,使其成为一个单向接收的 channel (消费者)

经典例子:

/**
经典:
使用两个 goroutine 交替打印:12AB34CD56EF78GH910IJ1112.....YZ2728
*/var number, letter = make(chan bool), make(chan bool)func printNum() {// 这里是等待接收消息,若消息接收不到,则该协程会始终阻塞在这个位置i := 1for {<-numberfmt.Printf("%d%d", i, i+1)i += 2letter <- true}
}func printLetter() {i := 0str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"for {<-letterfmt.Print(str[i : i+2])number <- trueif i <= 23 {i += 2} else {return}}}func main() {go printNum()go printLetter()number <- truetime.Sleep(time.Second * 20)
}

使用 select 对 goroutine 进行监控

// 使用 struct{} 作为传入的信息,由于这个 struct{} 占用内存空间较小,这是一种常见的传递方式func g1(ch chan struct{}) {time.Sleep(time.Second * 3)ch <- struct{}{}
}func g2(ch chan struct{}) {time.Sleep(time.Second * 3)ch <- struct{}{}
}func main() {g1Channel := make(chan struct{})g2Channel := make(chan struct{})go g1(g1Channel)go g2(g2Channel)// 注意这里只要有一个能取到值则 select 结果则结束select {// 若 g1Channel 中能取到值case <-g1Channel:fmt.Println("g1 done")// 若 g1Channel 中能取到值case <-g2Channel:fmt.Println("g2 done")}
}

这里若所有的 goroutine 都就绪了,则 select 执行哪个是随机的,为的是防止某个 goroutine 一直被优先执行导致的另一个 goroutine 饥饿

超时机制:

// 使用 struct{} 作为传入的信息,由于这个 struct{} 占用内存空间较小,这是一种常见的传递方式func g1(ch chan struct{}) {time.Sleep(time.Second * 3)ch <- struct{}{}
}func g2(ch chan struct{}) {time.Sleep(time.Second * 3)ch <- struct{}{}
}func main() {g1Channel := make(chan struct{})g2Channel := make(chan struct{})go g1(g1Channel)go g2(g2Channel)timeChannel := time.NewTimer(5 * time.Second)for {// 注意这里只要有一个能取到值则 select 结果则结束select {// 若 g1Channel 中能取到值case <-g1Channel:fmt.Println("g1 done")// 若 g1Channel 中能取到值case <-g2Channel:fmt.Println("g2 done")case <-timeChannel.C: // timeChannel.C 是获取我们创建的 channel 的方法fmt.Println("time out")return}}
}

context

使用 WithCancel() 引入手动终止进程的功能

func cpuIInfo(ctx context.Context) {defer wg.Done()for {select {case <-ctx.Done(): // 本质还是一个 channelfmt.Println("程序退出执行...........")returndefault:time.Sleep(1 * time.Second)fmt.Println("CPUUUUUUUUUUUUUU")}}
}func main() {/**这里有一个问题,我们可以将以 context.Background() 为参数的 context 是作为最上层的父 context所有以其他 context 为参数的 context 都是他的子 context只要父 context 调用了 cancel() 则其所有的子 context 都会停止*/ctxParent, cancel := context.WithCancel(context.Background())ctxChild, _ := context.WithCancel(ctxParent)wg.Add(1)go cpuIInfo(ctxChild)time.Sleep(5 * time.Second)cancel() // 这个方法会直接向context channel 中传入一个对象,令channel停止wg.Wait()
}

使用 WthTimeout() 来自动引入超时退出机制

var wg sync.WaitGroupfunc cpuIInfo(ctx context.Context) {defer wg.Done()for {select {case <-ctx.Done(): // 本质还是一个 channelfmt.Println("程序退出执行...........")returndefault:time.Sleep(1 * time.Second)fmt.Println("CPUUUUUUUUUUUUUU")}}
}func main() {/**这里有一个问题,我们可以将以 context.Background() 为参数的 context 是作为最上层的父 context所有以其他 context 为参数的 context 都是他的子 context只要父 context 调用了 cancel() 则其所有的子 context 都会停止*/ctxParent, _ := context.WithTimeout(context.Background(), 6*time.Second)ctxChild, _ := context.WithCancel(ctxParent)wg.Add(1)go cpuIInfo(ctxChild)wg.Wait()
}

WithDeadline() 是指定某个时间点,在某个时间点的时候进行执行

WithValue() 则会向 context 中传递一个数据,我们可以在子 goroutine 中调用这个数据

var wg sync.WaitGroupfunc cpuIInfo(ctx context.Context) {defer wg.Done()for {select {case <-ctx.Done(): // 本质还是一个 channelfmt.Println("程序退出执行...........")returndefault:time.Sleep(1 * time.Second)fmt.Printf("%s", ctx.Value("LeaderID"))fmt.Println("CPUUUUUUUUUUUUUU")}}
}func main() {/**这里有一个问题,我们可以将以 context.Background() 为参数的 context 是作为最上层的父 context所有以其他 context 为参数的 context 都是他的子 context只要父 context 调用了 cancel() 则其所有的子 context 都会停止*/ctxParent, _ := context.WithTimeout(context.Background(), 6*time.Second)ctxChild := context.WithValue(ctxParent, "LeaderID", "00001") // 注意 WithValue 方法只有一个返回值wg.Add(1)go cpuIInfo(ctxChild)wg.Wait()
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/677574.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大厂的供应链域数据中台设计

关注我&#xff0c;紧跟本系列专栏文章&#xff0c;咱们下篇再续&#xff01; 作者简介&#xff1a;魔都技术专家兼架构&#xff0c;多家大厂后端一线研发经验&#xff0c;各大技术社区头部专家博主&#xff0c;编程严选网创始人。具有丰富的引领团队经验&#xff0c;深厚业务架…

庆除夕,比特币两日大涨10%

号外&#xff1a;教链内参2024年1月合订本 今日除夕。昨日今日两天&#xff0c;比特币从43k发力上攻&#xff0c;一度涨超10%至47.7k&#xff0c;以独特的方式给全世界的bitcoiners送去了新春的祝福。 一个新鲜的知识&#xff1a;2023年12月22日&#xff0c;第78届联合国大会协…

JVM 执行引擎

概念 执行class文件中的指令&#xff0c;由解释器编译器组成 补充——Java为什么是半编译半解释型语言 因为Java即有编译器也有解释器&#xff0c;可以用其中一种来运行。 程序执行步骤 解释器与编译器区别 编译器 概念 JIT&#xff08;Just In Time Compiler&#xff09;…

深入探索Java IO:从基础到高级操作全览

深入探索Java IO&#xff1a;从基础到高级操作全览 Java IO一、概览二、磁盘操作三、字节操作实现文件复制装饰者模式 四、字符操作编码与解码String 的编码方式Reader 与 Writer实现逐行输出文本文件的内容 五、对象操作序列化Serializabletransient 六、网络操作InetAddressU…

机器学习系列——(十三)多项式回归

引言 在机器学习领域&#xff0c;线性回归是一种常见且简单的模型。然而&#xff0c;在某些情况下&#xff0c;变量之间的关系并不是线性的&#xff0c;这时候我们就需要使用多项式回归来建模非线性关系。多项式回归通过引入高次项来扩展线性回归模型&#xff0c;从而更好地拟…

【前端】Vue实现网站导航 以卡片形式显示(附Demo)

目录 前言1. html版本2. Vue2.1 Demo12.2 Demo2 前言 单独做一个跳转页面推荐阅读&#xff1a;【前端】实现Vue组件页面跳转的多种方式 但是如果网站多了&#xff0c;推荐卡片式导航&#xff0c;具体可看下文&#xff1a;&#xff08;以图片显示显示各个网站&#xff0c;图片…

MySQL-视图(VIEW)

文章目录 1. 什么是视图&#xff1f;2. 视图 VS 数据表3. 视图的优点4. 视图相关语法4.1 创建视图4.2 查看视图4.3 修改视图4.4 删除视图4.5 检查选项 5. 案例6. 注意事项 1. 什么是视图&#xff1f; MySQL 视图&#xff08; View&#xff09;是一种虚拟存在的表&#xff0c;同…

七、滚动条操作——调整图像对比度

对比度调整&#xff1a;是在原来图像基础上进行相应的公式调整&#xff0c;是类似乘法操作&#xff0c;本身像数值越大&#xff0c;对比度增加之后其与低像素点值差距越大&#xff0c;导致对比增强 项目最终效果&#xff1a;通过滚动条trackbar来实现调整图片亮度的功能 我这里…

【Java】苍穹外卖 Day02

苍穹外卖-day02 课程内容 新增员工员工分页查询启用禁用员工账号编辑员工导入分类模块功能代码 **功能实现&#xff1a;**员工管理、菜品分类管理。 员工管理效果&#xff1a; 菜品分类管理效果&#xff1a; 1. 新增员工 1.1 需求分析和设计 1.1.1 产品原型 一般在做需…

6.JavaScript中赋值运算符,自增运算符,比较运算符,逻辑运算符

赋值运算符 就是简单的加减乘除&#xff0c;没啥可说的这里直接上代码比较好 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><…

ios设备解锁 --Apeaksoft iOS Unlocker

Apeaksoft iOS Unlocker是一款针对iOS系统的密码解锁工具。其主要功能包括解锁多种锁屏类型&#xff0c;包括数字密码、Touch ID、Face ID和自定义密码。此外&#xff0c;它还可以帮助用户删除iPhone密码以进入锁屏设备&#xff0c;忘记的Apple ID并将iPhone激活为新的&#xf…

二叉树的锯齿形遍历,力扣

目录 题目&#xff1a; 我们直接看题解吧&#xff1a; 快速理解解题思路小建议&#xff1a; 解题方法&#xff1a; 相似题目对比分析&#xff1a; 解题分析&#xff1a; 解题思路&#xff1a; 补充说明&#xff1a; 思路优化&#xff1a; 代码实现(层序遍历倒序)&#xff1a; 题…

备战蓝桥杯---动态规划(基础2)

本专题主要是介绍几个比较经典的题目&#xff1a; 假设我们令f[i]为前i个的最长不下降子序列&#xff0c;我们会发现难以转移方程很难写&#xff08;因为我们不知道最后一个数&#xff09;。 于是&#xff0c;我们令f[i]为以i结尾的最长不下降子序列&#xff0c;这样子我们就可…

Leetcode 第 112 场双周赛题解

Leetcode 第 112 场双周赛题解 Leetcode 第 112 场双周赛题解题目1&#xff1a;2839. 判断通过操作能否让字符串相等 I思路代码复杂度分析 题目2&#xff1a;2840. 判断通过操作能否让字符串相等 II思路代码复杂度分析 题目3&#xff1a;2841. 几乎唯一子数组的最大和思路代码复…

“深度解析Java虚拟机:运行时数据区域、垃圾收集、内存分配与回收策略、类加载机制“

"深度解析Java虚拟机&#xff1a;运行时数据区域、垃圾收集、内存分配与回收策略、类加载机制" Java 虚拟机一、运行时数据区域程序计数器Java 虚拟机栈本地方法栈堆方法区运行时常量池直接内存 二、垃圾收集判断一个对象是否可被回收1. 引用计数算法2. 可达性分析算…

【前后端的那些事】webrtc入门demo(代码)

文章目录 前端代码apivue界面 后端modelwebsocketconfigresource 龙年到了&#xff0c;先祝福各位龙年快乐&#xff0c;事业有成&#xff01; 最近在搞webrtc&#xff0c;想到【前后端的那些事】好久都没有更新了&#xff0c;所以打算先把最近编写的小demo发出来。 p2p webrt…

for循环的多重跳出

for的多重跳出 1.前言2.标签使用3.使用异常的方式 本文在jdk17中测试通过 1.前言 前段时间面试时&#xff0c;面试官问我多重for循环如何跳出&#xff0c;我懵了&#xff0c;今天特别的研究了一下 本文主要说的不是continue与break&#xff0c;而是少用的另类操作 1.continue:…

数据结构——5.4 树、森林

5.4 树、森林 概念 树的存储结构 双亲表示法 孩子表示法 孩子兄弟表示法&#xff08;二叉树表示法&#xff09;&#xff1a; 二叉树每个结点有三个变量 ① 二叉树结点值&#xff1a;原树结点的值 ② 二叉树左孩子&#xff1a;原树结点的最左孩子 ③ 二叉树右孩子&#xff1a…

计算机网络——04接入网和物理媒体

接入网和物理媒体 接入网络和物理媒体 怎样将端系统和边缘路由器连接&#xff1f; 住宅接入网络单位接入网络&#xff08;学校、公司&#xff09;无线接入网络 住宅接入&#xff1a;modem 将上网数据调制加载到音频信号上&#xff0c;在电话线上传输&#xff0c;在局端将其…

【C语言|数据结构】数据结构顺序表

目录 一、数据结构 1.1概念 1.2总结 1.3为什么需要数据结构&#xff1f; 二、顺序表 1.顺序表的概念及结构 1.1线性表 2.顺序表分类 2.1顺序表和数组的区别 2.2顺序表的分类 2.2.1静态顺序表 2.2.1.1概念 2.2.1.2缺陷 2.2.2动态顺序表 三、动态顺序表的实现 3.1新…