Go-并发编程基础(goroutine、channel、select等)

简介: Go-并发编程基础(goroutine、channel、select等)

概念

  • 并发:指宏观上在一段时间内同时运行多个程序,微观交替运行。
  • 并行:指同一时刻能运行多个指令。
  • 进程:一段程序的执行过程,是系统进行资源分配的基本单位,一个进程至少有一个线程
  • 线程:操作系统能够进行运算调度的最小单位,它被包含在进程之中。

协程 goroutine

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源,
  • 协程从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。
  • Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了

当一个程序启动时,其主函数即在一个单独的goroutine中运行,称之为main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。当主函数返回时,所有的goroutine都会直接打断,程序退出。

操作已经就绪,对应的goroutine就会重新分配到逻辑处理器上来完成操作。调度器对可以创建的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建10000个线程。这个限制值可以通过调用runtime/debug包的SetMaxThreads方法来更改。如果程序试图使用更多的线程,就会崩溃。

goroutine调度-MPG模式

M(Main Thread):操作系统的主线程(是物理线程),又称内核线程。

P(Processor):处理器,管理协程,例如协程执行需要的上下文等

G(Goroutine):go协程

举个例子:

2020062310470442.png

分成两个部分来看

原来的情况是M主线程正在执行G0协程,另外有三个协程在队列等待如果G0协程阻塞,比如读取文件或者数据库等。

这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写。等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。

这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。

进行调度的调度器为Seched,它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。

package main
import (
  "fmt"
  "runtime"
  "time"
)
func routinetest(name string){
  for i:=0;i<3;i++{
    fmt.Println("routinetest",i,":hello,",name)
    time.Sleep(100*time.Millisecond)
  }
}
func main() {
  num :=runtime.NumCPU()
  fmt.Println(num)
  runtime.GOMAXPROCS(num)
  //runtime.GOMAXPROCS(1)
  //--------使用go开启协程----------
  go routinetest("lady")
  go routinetest("killer")
  time.Sleep(time.Second)
}

可以通过runtime.GOMAXPROCS设置cpu数,这里设置成了8个。

2020062310470442.png

通道Channel

相对于sync的低水平同步,使用channel可以实现高水平同步,channel是先进先出的。

数据结构

Channel 在运行时的内部表示是 runtime.hchan,该结构体中包含了用于保护成员变量的互斥锁,从某种程度上说,Channel 是一个用于同步和通信的有锁队列,使用互斥锁解决程序中可能存在的线程竞争问题是很常见的,我们能很容易地实现有锁队列。

type hchan struct {
  qcount   uint
  dataqsiz uint
  buf      unsafe.Pointer
  elemsize uint16
  closed   uint32
  elemtype *_type
  sendx    uint
  recvx    uint
  recvq    waitq
  sendq    waitq
  lock mutex
}

runtime.hchan 结构体中的五个字段 qcountdataqsizbufsendxrecv 构建底层的循环队列:

  • qcount — Channel 中的元素个数;
  • dataqsiz — Channel 中的循环队列的长度;
  • buf — Channel 的缓冲区数据指针;
  • sendx — Channel 的发送操作处理到的位置;
  • recvx — Channel 的接收操作处理到的位置;

除此之外,elemsize 和 elemtype 分别表示当前 Channel 能够收发的元素类型和大小;sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构:

type waitq struct {
  first *sudog
  last  *sudog
}

runtime.sudog 表示一个在等待列表中的 Goroutine,该结构中存储了两个分别指向前后 runtime.sudog 的指针以构成链表。

声明&初始化

初始化需要使用make(t Type, size ...IntegerType) Type,size为缓存大小

var b chan int

var c = make(chan int)

var d = make(chan int,10)

b为nil,c为无缓存channel,d为有缓存channel

发送与接收

发送使用channel<-data,接收使用[var,ok]:=<-channel,当左侧没有变量接收时会直接丢弃掉数据,ok可以标识channel是否有数据,无数据是,接收变量获取到的是对应类型的零值。

对于nil的channel,发送和接收都会阻塞,所以不make的channel没有用,实际编程中channel应该都初始化

对于无缓存的channel,发送后会阻塞,直至接收

对于有缓存的channel,满了后发送会被阻塞,接收无影响

遍历和关闭

close

关闭后无法写入,只能读取,例如

close(d)

普通for循环

  for j := 0;j<len(c); j++{
    fmt.Println(<-c)
  }

若取的时候,没有其他goroutine写入的话,会读出一半。例如,刚开始len(c)是10个,当j为5时,len(c)也是5了,就跳出循环了。

  for j := 0;len(c)!=0; j++{
    fmt.Println(<-c)
  }

上面这种方法可以

for range

关闭后可以正常遍历,遍历也是从channel中接收值,大小会变化,例如

  for data := range d{
    fmt.Println(data)
  }

若不关闭,会一直接收数据,即使当前channel没有数据了,无goroutine写入时会block,若是在main routine中,会导致deadlock错误。

                                      channel状态总结

image.png

单方向的channel

只发送chan<-int

只接收

var in =  make(chan <- int)
var out = make(<-chan int,3)

channel中的channel

package main
import "fmt"
type Request struct{
  num int
  result chan int
}
func result(r Request)  {
  r.result <- r.num + 1
}
func main() {
  r := Request{1,make(chan int)}
  go result(r)
  fmt.Println(<-r.result)
}

常见错误

panic: close of nil channel


关闭nil的channel


fatal error: all goroutines are asleep - deadlock!


main routine被永久阻塞,例如,接收一个空的channel,一直没有goroutine向里面放数据


panic: send on closed channel


向关闭的channel中发送数据

time与select

select是针对并发特有的控制结构。和switch很像,但每个case不是表达式而是通信,当有多个case可以时,将伪随机选择一个,所以不能依赖select来做顺序通信。

超时

func After(d Duration) <-chan Time

到达一定时间后可以从channel接收数据

package main
import (
  "fmt"
  "math/rand"
  "time"
)
func main() {
  timeout := time.After(2*time.Second)
  c := make(chan int)
  go func() {
    for {
      c<-0
      time.Sleep(time.Duration(rand.Intn(500))*time.Millisecond)
    }
  }()
  for {
    select {
    case <-c:
      fmt.Println("I'm working...")
    case <-timeout:
      fmt.Println("time out")
      return
    }
  }
}

2020062310470442.png

时间间隔

func Tick(d Duration) <-chan Time

package main
import (
  "fmt"
  "math/rand"
  "time"
)
func main() {
  timeout := time.After(3*time.Second)
  timetrick := time.Tick(time.Second)
  c := make(chan int)
  go func() {
    for {
      c<-0
      time.Sleep(time.Duration(rand.Intn(500))*time.Millisecond)
    }
  }()
  for {
    select {
    case <-c:
      fmt.Println("I'm working...")
    case <-timetrick:
      fmt.Println("1 second pass")
    case <-timeout:
      fmt.Println("3 second")
      return
    }
  }
}

2020062310470442.png

并发的后序内容查看:

Go-并发模式1(Basic Examples)

Go-并发模式2(Patterns)

更多Go相关内容:Go-Golang学习总结笔记

有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系

相关文章
|
人工智能 监控 Kubernetes
Go语言在select语句中实现优先级
Go语言中的`select`语句用于监控多个Channel的发送或接收操作,选择就绪的分支执行。它支持多种使用场景,如空`select`永久阻塞、单`case`阻塞读写、多`case`随机选择、配合`default`实现非阻塞操作等。通过嵌套`select`还可实现执行优先级,适用于如Kubernetes中任务调度等实际场景。
192 0
|
存储 Go API
如何在 Go 中获取 goroutine 的 ID?
在 Go 语言中,Goroutine 是轻量级线程,但官方未提供直接获取 Goroutine ID 的 API,以避免开发者依赖其进行复杂操作。然而,在调试和日志跟踪时,获取 Goroutine ID 可能很有帮助。本文介绍了通过 `runtime.Stack` 解析调用栈信息提取 Goroutine ID 的方法,尽管性能较低,适合调试场景。同时推荐使用第三方包 `github.com/petermattis/goid`,它通过 C 和汇编实现更高效地获取 Goroutine ID。需要注意的是,Goroutine ID 不应用于业务逻辑,而应关注 Goroutine 行为与通道通信。
373 11
如何在 Go 中获取 goroutine 的 ID?
理解 Go 语言中的 select 用法
本文深入解析了Go语言中`select`的用法,它类似于`switch case`,但仅用于通道(channel)的操作。文章通过多个示例说明了`select`的基本用法、避免死锁的方法、随机性特点以及如何实现超时机制。同时总结了`select`与`switch`的区别:`select`专用于通道操作,case执行是随机的,需注意死锁问题,且不支持`fallthrough`和函数表达式。
458 1
理解 Go 语言中的 select 用法
|
监控 Go 开发者
深入解析 go 语言中的 select 语句
在 Go 语言中,`select` 是专为并发编程设计的控制结构,用于在多个 channel 操作间进行非阻塞选择。它类似于 `switch`,但所有 case 分支必须是 channel 的 I/O 操作。本文通过通俗易懂的语言和丰富代码示例,详细讲解 `select` 的各种用法,包括基本语法、空 `select`、多分支选择、`default` 分支、特点总结及最佳实践,适合初学者和有经验的开发者学习参考。掌握 `select`,能有效提升 Go 并发编程的灵活性与效率。
398 6
|
Go 开发者
Go语言实战案例:使用select监听多个channel
本文为《Go语言100个实战案例 · 网络与并发篇》第5篇,详解Go并发核心工具`select`的使用。通过实际案例讲解如何监听多个Channel、实现多任务处理、超时控制和非阻塞通信,帮助开发者掌握Go并发编程中的多路异步事件处理技巧。
|
数据采集 编解码 监控
Go语言实战案例:使用channel实现生产者消费者模型
本文是「Go语言100个实战案例 · 网络与并发篇」第4篇,通过实战案例详解使用 Channel 实现生产者-消费者模型,涵盖并发控制、任务调度及Go语言并发哲学,助你掌握优雅的并发编程技巧。
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
Go 开发者
Go 并发编程基础:无缓冲与有缓冲通道
本章深入探讨Go语言中通道(Channel)的两种类型:无缓冲通道与有缓冲通道。无缓冲通道要求发送和接收必须同步配对,适用于精确同步和信号通知;有缓冲通道通过内部队列实现异步通信,适合高吞吐量和生产者-消费者模型。文章通过示例对比两者的行为差异,并分析死锁风险及使用原则,帮助开发者根据场景选择合适的通道类型以实现高效并发编程。
|
Go 调度 开发者
Go 并发编程基础:select 多路复用
Go 语言中的 `select` 是一种强大的并发控制结构,用于同时监听多个通道操作。它支持随机选择就绪的通道、阻塞等待以及通过 `default` 实现非阻塞通信。结合 `time.After()`,可轻松实现超时机制,适用于网络请求、任务调度等场景。本文详细介绍了 `select` 的基本用法、特性及实战技巧,如合并多通道输入、处理通道关闭等,帮助开发者高效管理协程与通道交互,避免常见陷阱。

热门文章

最新文章