go获取协程(goroutine)号的实例

我就废话不多说了,大家还是直接看代码吧~

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
超时
程序结束

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • 解决go echo后端处理跨域的两种操作方式

    跨域问题一般需要在后台解决会比较好. 1.第一种方式当然是接受所有的跨域方式: func setAccessOriginUrl(c echo.Context) { c.Response().Header().Set("Access-Control-Allow-Origin", "*") } 2.第二种接受指定地址的跨域请求: func setAccessOriginUrl(c echo.Context) { c.Response().Header().Set(&qu

  • graphql---go http请求使用详解

    1. Graphql是什么? GraphQL是Facebook 在2012年开发的,2015年开源,2016年下半年Facebook宣布可以在生产环境使用,而其内部早就已经广泛应用了,用于替代 REST API.facebook的解决方案和简单:用一个"聪明"的节点来进行复杂的查询,将数据按照客户端的要求传回去,后端根据GraphQL机制提供一个具有强大功能的接口,用以满足前端数据的个性化需求,既保证了多样性,又控制了接口数量. GraphQL并不是一门程序语言或者框架,它是描述你的请

  • go语言的工作空间和GOPATH环境变量介绍

    go语言并没有强制一定要使用一定的工作空间和项目结构,对于小型的go程序依靠go run等命令就可以直接编译运行. 然而,保持良好的工作空间和文件结构,对于管理源代码和发布程序都是非常有帮助的. 对于大型的go语言项目,工作空间则是一定要的. 1.go语言的工作空间结构 go语言的工作空间其实就是一个文件目录,目录中必须包含src.pkg.bin三个目录. 其中src目录用于存放go源代码,pkg目录用于package对象,bin目录用于存放可执行对象. 使用go的编译命令工具可以将源代码或pa

  • golang HTTP 服务器 处理 日志/Stream流的操作

    目前,我开发 HTTP 服务, 用的是 beego框架, 方便了很多. 但是, 有时候,还是会遇到一些 特殊的场景. 比如: 过滤日志. 这应该是一种典型的stream,同时数据量也适中, 不会有人,为了这个, 就用一些很重的框架. 可以这样直观的描述这个 逻辑 其他组件 产生 log || \ / 我的组件,业务处理 || \ / 用户, http client 这种情景下, 有几个特殊点: 1. 难以用 string,或者 byte 数组 收集数据 2. 数据Source 端,不断的有数据产

  • 浅谈go 协程的使用陷阱

    golang 语言协程 协程中使用全局变量.局部变量.指针.map.切片等作为参数时需要注意,此变量的值变化问题. 与for 循环,搭配使用更需谨慎. 1.内置函数时直接使用局部变量,未进行参数传递 func main() { for i := 0; i < 100; i++ { go func() { fmt.Println(i) }() } } 运行效果 func main() { for i := 0; i < 100; i++ { go func(i int) { fmt.Printl

  • golang 获取当前执行程序路径的操作

    我就废话不多说了,大家还是直接看代码吧~ 1.获取当前执行程序路径 func execPath() (string, error) { file, err := exec.LookPath(os.Args[0]) if err != nil { return "", err } re, err := filepath.Abs(file) if err != nil { logs.Error("The eacePath failed: %s\n", err.Error

  • Go语言Echo服务器的方法

    本文实例讲述了Go语言Echo服务器的方法.分享给大家供大家参考.具体如下: 复制代码 代码如下: package main import (     "net"     "io" ) func main() {     serv, e := net.Listen("tcp", ":12345")     if e != nil {         panic(e)     }     defer serv.Close()  

  • go获取协程(goroutine)号的实例

    我就废话不多说了,大家还是直接看代码吧~ 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语言并发协程Go

  • python在协程中增加任务实例操作

    1.添加一个任务 task2 = visit_url('http://another.com', 3) asynicio.run(task2) 2.这 2 个程序一共消耗 5s 左右的时间.并没有发挥并发编程的优势 import asyncio import time async def visit_url(url, response_time): """访问 url""" await asyncio.sleep(response_time) r

  • Python 异步协程函数原理及实例详解

    这篇文章主要介绍了Python 异步协程函数原理及实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一. asyncio 1.python3.4开始引入标准库之中,内置对异步io的支持 2.asyncio本身是一个消息循环 3.步骤: (1)创建消息循环 (2)把协程导入 (3)关闭 4.举例: import threading # 引入异步io包 import asyncio # 使用协程 @ asyncio.coroutine def

  • Python获取协程返回值的四种方式详解

    目录 介绍 源码 依次执行结果 介绍 获取协程返回值的四种方式: 1.通过ensure_future获取,本质是future对象中的result方 2.使用loop自带的create_task, 获取返回值 3.使用callback, 一旦await地方的内容运行完,就会运行callback 4.使用partial这个模块向callback函数中传入值 源码 import asyncio from functools import partial async def talk(name): pr

  • Python协程asyncio模块的演变及高级用法

    Python协程及asyncio基础知识 协程(coroutine)也叫微线程,是实现多任务的另一种方式,是比线程更小的执行单元,一般运行在单进程和单线程上.因为它自带CPU的上下文,它可以通过简单的事件循环切换任务,比进程和线程的切换效率更高,这是因为进程和线程的切换由操作系统进行. Python实现协程的主要借助于两个库:asyncio和gevent.由于asyncio已经成为python的标准库了无需pip安装即可使用,这意味着asyncio作为Python原生的协程实现方式会更加流行.本

  • Python协程的用法和例子详解

    从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数.可是,在协程中, yield 通常出现在表达式的右边(例如, datum = yield),可以产出值,也可以不产出 -- 如果 yield 关键字后面没有表达式,那么生成器产出 None. 协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是next(-) 函数. ==yield 关键字甚至还可以不接收或传出数据.不管数据如何流动, yield 都是一种流程控制工具,使用

  • 详解Python 协程的详细用法使用和例子

    从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数.可是,在协程中, yield 通常出现在表达式的右边(例如, datum = yield),可以产出值,也可以不产出 -- 如果 yield 关键字后面没有表达式,那么生成器产出 None. 协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是next(-) 函数. ==yield 关键字甚至还可以不接收或传出数据.不管数据如何流动, yield 都是一种流程控制工具,使用

  • 深入理解python协程

    一.什么是协程 协程拥有自己的寄存器和栈.协程调度切换的时候,将寄存器上下文和栈都保存到其他地方,在切换回来的时候,恢复到先前保存的寄存器上下文和栈,因此:协程能保留上一次调用状态,每次过程重入时,就相当于进入上一次调用的状态. 协程的好处: 1.无需线程上下文切换的开销(还是单线程) 2.无需原子操作(一个线程改一个变量,改一个变量的过程就可以称为原子操作)的锁定和同步的开销 3.方便切换控制流,简化编程模型 4.高并发+高扩展+低成本:一个cpu支持上万的协程都没有问题,适合用于高并发处理

  • python中Task封装协程的知识点总结

    说明 1.Task是Future的子类,Task是对协程的封装,我们把多个Task放在循环调度列表中,等待调度执行. 2.Task对象可以跟踪任务和状态.Future(Task是Futrue的子类)为我们提供了异步编程中最终结果的处理(Task类还具有状态处理功能). 3.把协程封装成Task,加入一个队列等待调用.刚创建Task的时候不执行,遇到await就执行. 实例 import asyncio async def func(): print(1) await asyncio.sleep(

  • 一文搞懂Python中的进程,线程和协程

    目录 1.什么是并发编程 2.进程与多进程 3.线程与多线程 4.协程与多协程 5.总结 1.什么是并发编程 并发编程是实现多任务协同处理,改善系统性能的方式.Python中实现并发编程主要依靠 进程(Process):进程是计算机中的程序关于某数据集合的一次运行实例,是操作系统进行资源分配的最小单位 线程(Thread):线程被包含在进程之中,是操作系统进行程序调度执行的最小单位 协程(Coroutine):协程是用户态执行的轻量级编程模型,由单一线程内部发出控制信号进行调度 直接上一张图看看

随机推荐