go获取协程(goroutine)号的实例
作者:逆月林 发布时间:2024-05-29 22:07:21
我就废话不多说了,大家还是直接看代码吧~
func GetGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
补充:Go语言并发协程Goroutine和通道channel
Go语言并发协程Goroutine
1.1 Go语言竞争状态
有并发,就有资源竞争,如果两个或者多个 goroutine 在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态,这就是并发中的资源竞争。
并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。
以下代码就会出现竞争状态:
import (
"fmt"
"runtime"
"sync"
)
var (
count int32
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
value := count
runtime.Gosched()
value++
count = value
}
}
count 变量没有任何同步保护,所以两个 goroutine 都会对其进行读写,会导致对已经计算好的结果被覆盖,以至于产生错误结果。
代码中的 runtime.Gosched() 是让当前 goroutine 暂停的意思,退回执行队列runq,让其他等待的 goroutine 运行,目的是为了使资源竞争的结果更明显,下次运行暂停的goroutine时从断点处开始。
分析程序运行过程:
g1 读取到 count 的值为 0;
然后 g1 暂停了,切换到 g2 运行,g2 读取到 count 的值也为 0;
g2 暂停,切换到 g1暂停的位置继续运行,g1 对 count+1,count 的值变为 1;
g1 暂停,切换到 g2,g2 刚刚已经获取到值 0,对其 +1,最后赋值给 count,其结果还是 1;
可以看出 g1 对 count+1 的结果被 g2 给覆盖了,两个 goroutine 都 +1 而结果还是 1。
通过上面的分析可以看出,之所以出现上面的问题,是因为两个 goroutine 相互覆盖结果。
所以我们对于同一个资源的读写必须是原子化的,也就是说,同一时间只能允许有一个 goroutine 对共享资源进行读写操作。 此例子的共享资源就是count
通过go build -race生成一个可以执行文件,然后再运行这个可执行文件,就可以检测资源竞争信息,看到打印出的检测信息。如下
==================
WARNING: DATA RACE
Read at 0x000000619cbc by goroutine 8:
main.incCount()
D:/code/src/main.go:25 +0x80// goroutine 8 在代码 25 行读取共享资源value := count
Previous write at 0x000000619cbc by goroutine 7:
main.incCount()
D:/code/src/main.go:28 +0x9f// goroutine 7 在代码 28行修改共享资源count=value
Goroutine 8 (running) created at:
main.main()
D:/code/src/main.go:17 +0x7e
Goroutine 7 (finished) created at:
main.main()
D:/code/src/main.go:16 +0x66//两个 goroutine 都是从 main 函数的 16、17 行通过 go 关键字启动的。
==================
4
Found 1 data race(s)
1.2 锁住共享资源
Go语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。atomic 和 sync 包里的一些函数就可以对共享的资源进行加锁操作。
1.2.1 原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait() //等待goroutine结束
fmt.Println(counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1) //安全的对counter加1
runtime.Gosched()
}
}
上述代码中使用了 atmoic 包的 AddInt64 函数,这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 gorountie 运行并完成这个加法操作。
另外两个有用的原子函数是 LoadInt64 和 StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。下面的代码就使用了 LoadInt64 和 StoreInt64 函数来创建一个同步标志,这个标志可以向程序里多个 goroutine 通知某个特殊状态。
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var (
shutdown int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go doWork("A")
go doWork("B")
time.Sleep(1 * time.Second)
fmt.Println("Shutdown Now")
atomic.StoreInt64(&shutdown, 1)
wg.Wait()
}
func doWork(name string) {
defer wg.Done()
for {
fmt.Printf("Doing %s Work\n", name)
time.Sleep(250 * time.Millisecond)
if atomic.LoadInt64(&shutdown) == 1 {
fmt.Printf("Shutting %s Down\n", name)
break
}
}
}
--output--
Doing A Work
Doing B Work
Doing B Work
Doing A Work
Doing A Work
Doing B Work
Doing B Work
Doing A Work//前8行顺序每次运行时都不一样
Shutdown Now
Shutting A Down
Shutting B Down//A和B都shut down后,由wg.Done()把计数器置0
上面代码中 main 函数使用 StoreInt64 函数来安全地修改 shutdown 变量的值。如果哪个 doWork goroutine 试图在 main 函数调用 StoreInt64 的同时调用 LoadInt64 函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。
1.2.2 锁
见上篇文章,上面的例子为保持同步,取消竞争,可照以下操作:
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
//同一时刻只允许一个goroutine进入这个临界区
mutex.Lock()
{
value := counter
runtime.Gosched()//退出当前goroutine,调度器会再次分配这个 goroutine 继续运行。
value++
counter = value
}
mutex.Unlock() //释放锁,允许其他正在等待的goroutine进入临界区
}
}
1.3 通道chan
统统将通道两端的goroutine理解为生产者-消费者模式。
通道的数据接收一共有以下 4 种写法。
阻塞接收数据
阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
2) 非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch
data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
ok:表示是否接收到数据。
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行
3) 循环接收数据
import (
"fmt"
"time"
)
func main() {
// 构建一个通道,这里有没有缓冲都可,因为是收了就发,无需阻塞等待
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
// 从3循环到0
for i := 3; i >= 0; i-- {
// 发送3到0之间的数值
ch <- i
// 每次发送完时等待
time.Sleep(time.Second)
}
}()
// 遍历接收通道数据
for data := range ch {
// 打印通道数据
fmt.Println(data)
// 当遇到数据0时, 退出接收循环
if data == 0 {
break
}
}
}
--output--
1.3.1 单向通道
ch := make(chan int)
// 声明一个只能写入数据的通道类型, 并赋值为ch
var chSendOnly chan<- int = ch
或
ch := make(chan<- int)
//声明一个只能读取数据的通道类型, 并赋值为ch
var chRecvOnly <-chan int = ch
或
ch := make(<-chan int)
1.3.2 优雅的关闭通道
1.3.3 无缓冲的通道
如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。(阻塞指的是由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足才解除阻塞)这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
在网球比赛中,两位选手会把球在两个人之间来回传递。选手总是处在以下两种状态之一,要么在等待接球,要么将球打向对方。可以使用两个 goroutine 来模拟网球比赛,并使用无缓冲的通道来模拟球的来回
// 这个示例程序展示如何用无缓冲的通道来模拟
// 2 个goroutine 间的网球比赛
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// wg 用来等待程序结束
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
// main 是所有Go 程序的入口
func main() {
// 创建一个无缓冲的通道
court := make(chan int)
// 计数加 2,表示要等待两个goroutine
wg.Add(2)
// 启动两个选手
go player("Nadal", court)
go player("Djokovic", court)
// 发球
court <- 1
// 等待游戏结束
wg.Wait()
}
// player 模拟一个选手在打网球
func player(name string, court chan int) {
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
for {
// 等待球被击打过来
ball, ok := <-court
if !ok {
// 如果通道被关闭,我们就赢了
fmt.Printf("Player %s Won\n", name)
return
}
// 选随机数,然后用这个数来判断我们是否丢球
n := rand.Intn(100)
if n%13 == 0 {
fmt.Printf("Player %s Missed\n", name)
// 关闭通道,表示我们输了
close(court)
return
}
// 显示击球数,并将击球数加1
fmt.Printf("Player %s Hit %d\n", name, ball)
ball++
// 将球打向对手,为啥这里是把ball发送到另一个go协程?
//因为court无缓冲,此时另一个go协程正好在等待接收court内的值,所以此时转向另一个go协程代码
court <- ball
}
}
1.3.4 有缓冲的通道
有缓冲的通道是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收,发送和接受的阻塞条件为只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
为什么要给通道限制缓冲区大小?
通道(channel)是在两个 goroutine 间通信的桥梁。使用 goroutine 的代码必然有一方提供数据,一方消费数据。当提供数据一方的数据供给速度大于消费方的数据处理速度时,如果通道不限制长度,那么内存将不断膨胀直到应用崩溃。因此,限制通道的长度有利于约束数据提供方的供给速度,供给数据量必须在消费方处理量+通道长度的范围内,才能正常地处理数据。
1.3.5 channel超时机制
select 机制不是专门为超时而设计的,却能很方便的解决超时问题,因为 select 的特点是只要其中有一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。
基本语句为:
每个 case 语句里必须是一个 IO 操作,
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
例子,注意之所以输出5个num,是因为select里的time.After在这里的意思是ch通道无值可以接收的时候的3s后才print超时,即最多ch通道最多阻塞等待3s
func main() {
ch := make(chan int)
quit := make(chan bool)
//新开一个协程
go func() {
for {
select {
case num := <-ch:
fmt.Println("num = ", num)
case <-time.After(3 * time.Second):
fmt.Println("超时")
quit <- true
}
}
}() //别忘了()
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)//主协程进入休眠状态,等待上面的go协程运行并进入阻塞等待状态,就这样来回运行,并通过chan通信
}
<-quit
fmt.Println("程序结束")
}
--output--
num = 0
num = 1
num = 2
num = 3
num = 4
超时
程序结束
以上为个人经验,希望能给大家一个参考,也希望大家多多支持asp之家。如有错误或未考虑完全的地方,望不吝赐教。
来源:https://blog.csdn.net/niyuelin1990/article/details/76689428
猜你喜欢
- 题目描述1275. 找出井字棋的获胜者 - 力扣(LeetCode)A 和 B 在一个 3 x&nb
- 今天小编利用美丽的汤来为大家演示一下如何实现京东商品信息的精准匹配~~HTML文件其实就是由一组尖括号构成的标签组织起来的,每一对尖括号形式
- 我希望大家敲一遍<!DOCTYPE html><html><head><meta charset=
- 在LintCode上练习遇到这个问题,查阅资料找到多种方法,总结如下。输入输出123321第一种:整数方法取余取整实现class Solut
- 现在小编已经学习语言程序良久,但是在了解以后,如果让小编再去学习语言要入手入口,一定是先从掌握函数开始了解,原因很简单,任何一个代码串都是有
- 解析来自各种来源和格式的时间序列信息pd.to_datetime( arg,#int, float, str, d
- 什么是Flyway?转载:https://blog.waterstrong.me/flyway-in-practice/Flyway is
- php循环输出26个大小写英文字母for($i=65;$i<91;$i++){ echo strtolower(chr($
- 井字棋,英文名叫Tic-Tac-Toe,是一种在3*3格子上进行的连珠游戏,和五子棋类似,由于棋盘一般不画边框,格线排成井字故得名。游戏需要
- 单例模式单例是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。单例拥有与全局变量相同的优缺点。 尽管
- 数字列表和其他列表类似,但是有一些函数可以使数字列表的操作更高效。我们创建一个包含10个数字的列表,看看能做哪些工作吧。# Print ou
- 模板是一个文本,用于分离文档的表现形式和内容。 模板定义了占位符以及各种用于规范文档该如何显示的各部分基本逻辑(模板标签)。 模板通常用于产
- 前言:以往看到我博客的小伙伴可能都知道,我的前言一般都是吐槽和讲废话环节,哈哈哈哈。今天难得休息,最近可真是太忙了,博主已经连续一年都在99
- keras中正则化(regularization)keras内置3种正则化方法keras.regularizers.l1(lambda)ke
- MySQL超长字符截断又名"SQL-Column-Truncation",是安全研究者Stefan Esser在2008
- 实例如下所示:import numpy as npa1 = np.array([1,2,3,4],dtype=np.complex128)p
- mysql常用导出数据命令:1.mysql导出整个数据库 mysqldump -hhostname -uusername -pp
- 1.安装插件npm install jquery --save //jquery插件npm install bo
- 最近一直在研究 Javascript 相关的技术。在《Javascript 高级程序设计》有篇章节着重阐述了优
- 本文以连接错误ECONNREFUSED为例,看看nodejs对错误处理的过程。 假设我们有以下代码1. const net =