go并发编程
- 一、 并发介绍
- 1,进程和线程
- 2,并发和并行
- 3,协程和线程
- 4,goroutine
- 二、 Goroutine
- 1,使用goroutine
- 1)启动单个goroutine
- 2)启动多个goroutine
- 2,goroutine与线程
- 3,goroutine调度
- 三、runtime包
- 1,runtime.Gosched()
- 2,runtime.Goexit() 退出当时协程
- 3, runtime.GOMAXPROCS
- 4,将任务分配到不同的CPU逻辑核心上实现并行
- 5,Go语言中的操作系统线程和goroutine的关系:
- 四、channel
- 1,CSP模型
- 2,channel 类型
- 3,创建channel
- 4,channel 操作
- 1)发送
- 2)接收
- 3)关闭
- 5,无缓冲通道
- 6,有缓冲通道
- 7,从通道中遍历获取值
- 8,单向通道
- 8,通道总结
一、 并发介绍
1,进程和线程
- A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
- B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
- C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行
2,并发和并行
- A. 多线程程序在一个核的cpu上运行,就是并发。
- B. 多线程程序在多个核的cpu上运行,就是并行。
并发:
并行:
3,协程和线程
- 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
- 线程:一个线程上可以跑多个协程,协程是轻量级的线程。
4,goroutine
- goroutine 只是由官方实现的超级"线程池"。
- 每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因
- goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
二、 Goroutine
Go语言引入了goroutine机制,简化了并发编程。程序员只需定义任务函数,通过开启goroutine实现并发执行,而无需自己管理线程池、任务调度和上下文切换。
Go运行时负责智能分配任务到CPU,将复杂性隐藏在底层。
这使得Go成为现代化编程语言,使并发编程更加简单和高效。
1,使用goroutine
- 在函数,或匿名函数 前面添加 go 关键词。
- 一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
1)启动单个goroutine
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
go hello()
fmt.Println("main goroutine done!")
}
//但打印没有 ,Hello Goroutine! ,因为main 生命周期结束,goroutine 还没启动
//让main 等等 goroutine 粗暴方法:
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
2)启动多个goroutine
- 使用了sync.WaitGroup来实现goroutine的同步
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
打印出来,并不是顺序,因为这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。
2,goroutine与线程
- 可增长栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
3,goroutine调度
Go语言的运行时(runtime)引入了GPM模型来实现并发调度,与传统操作系统调度不同。
-
G(goroutine): G是goroutine的缩写,代表一个任务单元。它存储了该任务的信息,以及与所在P(处理器)的关联。
-
P(处理器): P管理一组goroutine队列,包含当前goroutine的运行上下文。P负责调度自己的队列,比如暂停耗时长的任务、切换到其他任务。当P队列为空,它会从全局队列取任务,甚至从其他P队列抢占任务。
-
M(机器): M是Go运行时对操作系统内核线程的虚拟。通常是一一对应的关系,每个M执行一个goroutine。当一个G长时间阻塞在一个M上,会创建新的M,将其他G挂载在新M上。旧M释放后,用于回收资源。
-
GOMAXPROCS: 用于设定P的个数,控制并发度,但不会过多地增加P和M,以避免频繁切换的开销。
Go语言与其他语言不同之处在于,它在运行时实现了自己的调度器,使用m:n调度技术。这意味着goroutine的调度发生在用户态,避免了内核态与用户态的频繁切换,包括内存分配与释放都在用户态维护,性能开销较小。此外,Go语言充分利用多核硬件资源,将多个goroutine均匀分配在物理线程上,加上goroutine的轻量特性,保证了高效的并发调度性能。
三、runtime包
1,runtime.Gosched()
一种协作式多任务切换的方式,让正在运行的 goroutine 暂时停下来,让其他等待执行的 goroutine 有机会运行。
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
}("world")
// 主程
for i := 0; i < 2; i++ {
//切换 再次分配任务
runtime.Gosched()
fmt.Println("hello")
}
}
2,runtime.Goexit() 退出当时协程
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
}
}
3, runtime.GOMAXPROCS
-
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
-
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
-
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
4,将任务分配到不同的CPU逻辑核心上实现并行
- 单核心
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
输出,看出是执行完一个goroutine ,再执行另一个
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
- 多核心
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
输出,看出是并发执行
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 9
5,Go语言中的操作系统线程和goroutine的关系:
- 1.一个操作系统线程对应用户态多个goroutine。
- 2.go程序可以同时使用多个操作系统线程。
- 3.goroutine和OS线程是多对多的关系,即m:n
四、channel
1,CSP模型
并发执行函数的目的是让多个任务同时进行,但仅仅并发执行函数是不够的,因为这些函数可能需要相互交换数据。在并发环境中,共享内存虽然可以用于数据交换,但容易引发竞态问题,而使用互斥量会影响性能。
Go语言采用了CSP(Communicating Sequential Processes)并发模型,强调通过通信来共享数据,而不是通过共享数据来进行通信。这种方式更加安全且高效。
-
关键点:
-
CSP模型: Go语言采用了CSP模型,强调通过通信来实现协程(goroutine)间的数据交换,而不是直接共享内存。
-
通道(channel): 通道是用于协程间通信的一种机制,类似于一个队列,保证了数据的顺序性。通过在通道中发送和接收数据,协程可以安全地进行交互。
-
通道的特点: 每个通道都有特定的元素类型,通道的操作遵循先进先出原则。通过通道的发送和接收操作,协程之间可以安全地进行数据交换,避免了竞态问题。
-
并发优势: 通过通道,Go语言实现了安全且高效的并发编程,允许协程在不同任务之间进行数据交换,而不需要显式地使用互斥量进行加锁。
-
通过使用通道,Go语言的并发模型强调了协程之间通过通信共享数据,而不是通过共享数据来进行通信,从而避免了许多传统并发模型中常见的问题。这使得并发编程更加安全、简洁和高效。
2,channel 类型
- channel 是一种类型,引用类型
声明格式:
var 变量 chan 元素类型
例如:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
3,创建channel
通道是引用类型,通道类型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>
声明的通道后需要使用make函数初始化之后才能使用。
创建channel的格式如下:
make(chan 元素类型, [缓冲大小]) // 缓冲大小可选
例如:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
4,channel 操作
1)发送
ch <- 10 // 把10发送到ch中
2)接收
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
3)关闭
close(ch)
- 只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。
- 通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
注意:
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。
5,无缓冲通道
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
//出现以下错误
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
- 无缓冲的通道必须有接收才能发送。
- 上面的代码会阻塞在ch <- 10这一行代码形成死锁
启动一个goroutine 解决该问题:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
6,有缓冲通道
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
- 只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。
- 可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。
7,从通道中遍历获取值
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine 将 0~100 的数发到 ch1 中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine 从ch1中接收值,发送给ch2
go func() {
for {
i, ok := <-ch1
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine 打印ch2
for i := range ch2 {
fmt.Println("ch2:", i)
}
}
- 能从关闭通道中获取值
- 通过遍历获取通道中(关闭的通道也行)的值
8,单向通道
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
- 1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
- 2.<-chan int是一个只能接收的通道,可以接收但是不能发送。