go获取协程(goroutine)号的实例
我就废话不多说了,大家还是直接看代码吧~
funcGetGID()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)
returnn
}
补充:Go语言并发协程Goroutine和通道channel
Go语言并发协程Goroutine
1.1Go语言竞争状态
有并发,就有资源竞争,如果两个或者多个goroutine在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态,这就是并发中的资源竞争。
并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。
以下代码就会出现竞争状态:
import(
"fmt"
"runtime"
"sync"
)
var(
countint32
wgsync.WaitGroup
)
funcmain(){
wg.Add(2)
goincCount()
goincCount()
wg.Wait()
fmt.Println(count)
}
funcincCount(){
deferwg.Done()
fori:=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
通过gobuild-race生成一个可以执行文件,然后再运行这个可执行文件,就可以检测资源竞争信息,看到打印出的检测信息。如下
================== WARNING:DATARACE Readat0x000000619cbcbygoroutine8: main.incCount() D:/code/src/main.go:25+0x80//goroutine8在代码25行读取共享资源value:=count Previouswriteat0x000000619cbcbygoroutine7: main.incCount() D:/code/src/main.go:28+0x9f//goroutine7在代码28行修改共享资源count=value Goroutine8(running)createdat: main.main() D:/code/src/main.go:17+0x7e Goroutine7(finished)createdat: main.main() D:/code/src/main.go:16+0x66//两个goroutine都是从main函数的16、17行通过go关键字启动的。 ================== 4 Found1datarace(s)
1.2锁住共享资源
Go语言提供了传统的同步goroutine的机制,就是对共享资源加锁。atomic和sync包里的一些函数就可以对共享的资源进行加锁操作。
1.2.1原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针
import(
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var(
counterint64
wgsync.WaitGroup
)
funcmain(){
wg.Add(2)
goincCounter(1)
goincCounter(2)
wg.Wait()//等待goroutine结束
fmt.Println(counter)
}
funcincCounter(idint){
deferwg.Done()
forcount:=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(
shutdownint64
wgsync.WaitGroup
)
funcmain(){
wg.Add(2)
godoWork("A")
godoWork("B")
time.Sleep(1*time.Second)
fmt.Println("ShutdownNow")
atomic.StoreInt64(&shutdown,1)
wg.Wait()
}
funcdoWork(namestring){
deferwg.Done()
for{
fmt.Printf("Doing%sWork\n",name)
time.Sleep(250*time.Millisecond)
ifatomic.LoadInt64(&shutdown)==1{
fmt.Printf("Shutting%sDown\n",name)
break
}
}
}
--output--
DoingAWork
DoingBWork
DoingBWork
DoingAWork
DoingAWork
DoingBWork
DoingBWork
DoingAWork//前8行顺序每次运行时都不一样
ShutdownNow
ShuttingADown
ShuttingBDown//A和B都shutdown后,由wg.Done()把计数器置0
上面代码中main函数使用StoreInt64函数来安全地修改shutdown变量的值。如果哪个doWorkgoroutine试图在main函数调用StoreInt64的同时调用LoadInt64函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。
1.2.2锁
见上篇文章,上面的例子为保持同步,取消竞争,可照以下操作:
funcincCounter(idint){
deferwg.Done()
forcount:=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"
)
funcmain(){
//构建一个通道,这里有没有缓冲都可,因为是收了就发,无需阻塞等待
ch:=make(chanint)
//开启一个并发匿名函数
gofunc(){
//从3循环到0
fori:=3;i>=0;i--{
//发送3到0之间的数值
ch<-i
//每次发送完时等待
time.Sleep(time.Second)
}
}()
//遍历接收通道数据
fordata:=rangech{
//打印通道数据
fmt.Println(data)
//当遇到数据0时,退出接收循环
ifdata==0{
break
}
}
}
--output--
1.3.1单向通道
ch:=make(chanint) //声明一个只能写入数据的通道类型,并赋值为ch varchSendOnlychan<-int=ch 或 ch:=make(chan<-int) //声明一个只能读取数据的通道类型,并赋值为ch varchRecvOnly<-chanint=ch 或 ch:=make(<-chanint)
1.3.2优雅的关闭通道
1.3.3无缓冲的通道
如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。(阻塞指的是由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足才解除阻塞)这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
在网球比赛中,两位选手会把球在两个人之间来回传递。选手总是处在以下两种状态之一,要么在等待接球,要么将球打向对方。可以使用两个goroutine来模拟网球比赛,并使用无缓冲的通道来模拟球的来回
//这个示例程序展示如何用无缓冲的通道来模拟
//2个goroutine间的网球比赛
packagemain
import(
"fmt"
"math/rand"
"sync"
"time"
)
//wg用来等待程序结束
varwgsync.WaitGroup
funcinit(){
rand.Seed(time.Now().UnixNano())
}
//main是所有Go程序的入口
funcmain(){
//创建一个无缓冲的通道
court:=make(chanint)
//计数加2,表示要等待两个goroutine
wg.Add(2)
//启动两个选手
goplayer("Nadal",court)
goplayer("Djokovic",court)
//发球
court<-1
//等待游戏结束
wg.Wait()
}
//player模拟一个选手在打网球
funcplayer(namestring,courtchanint){
//在函数退出时调用Done来通知main函数工作已经完成
deferwg.Done()
for{
//等待球被击打过来
ball,ok:=<-court
if!ok{
//如果通道被关闭,我们就赢了
fmt.Printf("Player%sWon\n",name)
return
}
//选随机数,然后用这个数来判断我们是否丢球
n:=rand.Intn(100)
ifn%13==0{
fmt.Printf("Player%sMissed\n",name)
//关闭通道,表示我们输了
close(court)
return
}
//显示击球数,并将击球数加1
fmt.Printf("Player%sHit%d\n",name,ball)
ball++
//将球打向对手,为啥这里是把ball发送到另一个go协程?
//因为court无缓冲,此时另一个go协程正好在等待接收court内的值,所以此时转向另一个go协程代码
court<-ball
}
}
1.3.4有缓冲的通道
有缓冲的通道是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收,发送和接受的阻塞条件为只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。
为什么要给通道限制缓冲区大小?
通道(channel)是在两个goroutine间通信的桥梁。使用goroutine的代码必然有一方提供数据,一方消费数据。当提供数据一方的数据供给速度大于消费方的数据处理速度时,如果通道不限制长度,那么内存将不断膨胀直到应用崩溃。因此,限制通道的长度有利于约束数据提供方的供给速度,供给数据量必须在消费方处理量+通道长度的范围内,才能正常地处理数据。
1.3.5channel超时机制
select机制不是专门为超时而设计的,却能很方便的解决超时问题,因为select的特点是只要其中有一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况。
基本语句为:
每个case语句里必须是一个IO操作,
select{
case<-chan1:
//如果chan1成功读到数据,则进行该case处理语句
casechan2<-1:
//如果成功向chan2写入数据,则进行该case处理语句
default:
//如果上面都没有成功,则进入default处理流程
}
例子,注意之所以输出5个num,是因为select里的time.After在这里的意思是ch通道无值可以接收的时候的3s后才print超时,即最多ch通道最多阻塞等待3s
funcmain(){
ch:=make(chanint)
quit:=make(chanbool)
//新开一个协程
gofunc(){
for{
select{
casenum:=<-ch:
fmt.Println("num=",num)
case<-time.After(3*time.Second):
fmt.Println("超时")
quit<-true
}
}
}()//别忘了()
fori:=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
超时
程序结束
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。