并发:多线程程序在一个核的cpu上运行。
并行:多线程程序在多个核的cpu上运行。
由上可知并发不是并行,并行是直接利用多核实现多线程的运行,并发则主要由切换时间片来实现”同时”运行,go可以设置使用核数,以发挥多核计算机的能力。
Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。Go语言的并发编程特点主要体现在Goroutine协程和Channel通道的使用上。
Goroutine是一种特殊的协程,这是因为普通的协程和操作系统线程是多对一的关系,而在Go语言中,Goroutine和操作系统线程是多对多的关系。具体来说:
在Go语言中,可以通过创建协程(Goroutine)来实现并发执行的任务。当父协程创建一个子协程时,父协程和子协程是相互独立的并发执行单元。父协程可以继续执行其他操作,而不需要等待子协程完成。子协程会在创建后立即开始执行,与父协程并发执行。父协程和子协程之间不存在直接的调用关系,它们是相互独立的执行流程。父协程的结束不会影响子协程的执行。即使父协程结束,子协程仍然可以继续执行,直到完成或被终止。父协程和子协程之间是独立的执行上下文,彼此之间的运行状态不会相互影响。
然而,需要注意的是,如果主协程(即main函数所在的协程)结束了,整个程序会终止,所有的协程也会被强制结束。这意味着如果主协程提前结束,尚未完成的子协程也会被中止。因此,在使用协程进行并发编程时,我们需要确保主协程不会过早地结束,以确保子协程能够完成任务,可以考虑采用以下方法:
使用time.Sleep使协程睡眠确保并发子协程完成
使用sync.WaitGroup等待组确保并发子协程完成
time包提供了时间相关的功能,其中最常用的是time.Sleep函数,它可以让当前的Goroutine休眠一段时间。通过结合Goroutine和time.Sleep,我们可以实现协程的并发执行。
package main
import (
"fmt"
"time"
)
func main() {
go task("Task 1") // 启动协程1
go task("Task 2") // 启动协程2
// 主协程休眠一段时间,确保协程有足够的时间执行
time.Sleep(3 * time.Second)
}
func task(name string) {
for i := 0; i < 5; i++ {
fmt.Println(name+":", i) // 打印任务名称和当前迭代值
time.Sleep(500 * time.Millisecond)
}
}
在上面的示例中,我们通过启动两个协程(task1和task2)来实现并发执行。主协程(main函数)休眠3秒钟,确保协程有足够的时间执行。这样,我们就实现了协程的并发执行。
sync.WaitGroup文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#waitgroup
sync包提供了一些同步原语,如WaitGroup等待组,它可以用来等待一组协程的完成。通过WaitGroup,类似于操作系统中的PV信号量,可以实现协程的并发执行和同步等待。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) // 设置等待组的计数器为2,表示有两个协程需要等待
go func() {//开启协程1
defer wg.Done() // 协程完成后调用Done方法,减少等待组的计数器
task("Task 1")
}()
go func() {//开启协程1
defer wg.Done() // 协程完成后调用Done方法,减少等待组的计数器
task("Task 2")
}()
wg.Wait() // 等待所有协程完成,完成后才结束main协程
}
func task(name string) {
for i := 0; i < 5; i++ {
fmt.Println(name+":", i) // 打印任务名称和当前迭代值
}
}
在上述示例中,我们使用sync包中的WaitGroup来实现协程的并发执行和同步等待。通过调用wg.Add方法设置等待组的计数器为2,然后在每个协程中使用defer wg.Done()来减少计数器。最后,通过wg.Wait()等待所有协程完成。
Channel文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/
在并发编程中,单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义,对共享资源的正确访问需要精确的控制。
在目前的绝大多数语言中,都是通过加锁等线程同步方案来解决这一困难问题,而Go语言却另辟蹊径,它将共享的值通过Channel传递(实际上多个独立执行的线程很少主动共享资源)。channel像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序,可以让一个goroutine发送特定值到另一个goroutine的通信机制。在任意给定的时刻,最好只有一个Goroutine能够拥有该资源,数据竞争从设计层面上就被杜绝了。
这也是Go语言作者提出的并发编程哲学:不要通过共享内存来通信,而应通过通信来共享内存。
虽然我们在 Go 语言中也能使用共享内存加互斥锁进行通信,但是 Go 语言提供了一种不同的并发模型,即通信顺序进程(Communicating sequential processes,CSP)。Goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介,Goroutine 之间会通过 Channel 传递数据。
CSP(Communicating sequential processes)是一种并发编程模型,它强调通过通信来实现并发。在CSP中,程序被分解成一组独立的进程,这些进程通过通道进行通信。通道是一种同步的通机制,它允许进程之间传递数据。CSP模型的一个重要特点是,进程之间的通信是通过发送和收消息来实现的,而不是通过共享内存。
CSP模型的一个优点是,它可以避免一些常见的并发编程问题,例如死锁和竞态条件。这是因为CSP模型中的进程是独立的,们不会相互干扰或阻塞彼此。此外,CSP模型还可以使并发程序更易于理解和调试,因为它们的行为是通过进程之间的通信来定义的。
上图中的两个 Goroutine,一个会向 Channel 中发送数据,另一个会从 Channel 中接收数据,它们两者能够独立运行并不存在直接关联,但是能通过 Channel 间接完成通信。
package main
import "fmt"
func main() {
ch := make(chan int) // 创建一个int类型的通道
go func() {
ch <- 10 // 发送数据到通道
}()
data := <-ch // 从通道接收数据
fmt.Println(data)
}
在上述示例中,我们通过make函数创建了一个无缓冲的int类型的通道ch,无缓冲的通道只有在有人接收值的时候才能发送值。就。因此,在匿名函数中使用ch <- 10
将数据10发送到通道,就必须还要通过data := <-ch
从通道中接收数据并打印。
生产者消费者模式是并发编程中的常见模式,其中生产者生成数据并将其放入通道,而消费者从通道中取出数据并进行处理。以下是多对多的生产者消费者模式
package main
import (
"fmt"
"sync"
)
// producer 向通道发送数据
func producer(ch chan<- int, id int) {
for i := 0; i < 5; i++ {
ch <- i * id
}
}
// consumer 从通道接收数据
func consumer(ch <-chan int, id int) {
for i := range ch {
fmt.Printf("消费者 %d 接收到数据: %d\n", id, i)
}
}
func main() {
ch := make(chan int, 10)
// wg 用于等待所有协程完成
var wg sync.WaitGroup
// producerWg 用于等待所有生产者协程完成,根据该等待组判断何时关闭通道
var producerWg sync.WaitGroup
// 启动多个生产者协程
for i := 0; i < 3; i++ {
producerWg.Add(1) // 增加生产者等待组计数器
wg.Add(1) // 增加总等待组计数器
go func() {
producer(ch, i+1)
wg.Done() // 减少总等待组计数器
producerWg.Done() // 减少生产者等待组计数器
}()
}
// 启动多个消费者协程
for i := 0; i < 3; i++ {
wg.Add(1) // 增加总等待组计数器
go func() {
consumer(ch, i+1)
wg.Done() // 减少总等待组计数器
}()
}
// 等待所有生产者协程完成后关闭通道
producerWg.Wait()
close(ch)
// 等待所有协程完成
wg.Wait()
}
在上面的示例中,我们创建了一个int类型的通道ch,并将其作为参数传递给生产者和消费者协程。生产者协程通过ch <- i
将数据发送到通道,并打印相关信息。消费者协程使用num := range ch
循环接收通道中的数据,并进行处理。通过关闭通道close(ch)
来通知消费者协程数据已经全部发送完毕。
在某些场景下我们需要同时从多个通道接收数据,Go内置了select关键字,可以同时响应多个通道的操作。
package main
import (
"fmt"
"time"
)
func main() {
// 创建两个缓冲区大小为 1 的通道
ch1 := make(chan int, 1)
ch2 make(chan int, 1)
// 向 ch1 发送数据的 goroutine
go func() {
time.Sleep(2 * time.Second)
ch1 <- 1
}()
// 向 ch2 发送数据的 goroutine
go func() {
time.Sleep(1 * time.Second)
ch2 <- 2
}()
// 无限循环,等待从通道接收数据
for {
select {
case x := <-ch1:
fmt.Println("Received from ch1:", x)
return
case x := <-ch2:
fmt.Println("Received from ch2:", x)
return
default:
// 如果两个通道都已满,则打印一条消息并等待 500 毫秒
fmt.Println("All channels are full")
time.Sleep(500 * time.Millisecond)
break
}
}
}
在并发编程中,要注意并发安全性。并发安全性指的是在并发环境下,多个协程访问共享资源时,能够正确地进行同步和互斥,避免数据竞争和不一致的结果。
在Go语言中,可以通过以下方式实现并发安全:
Mutex文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#mutex
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
RWMutex文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#rwmutex
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
package main
import (
"fmt"
"sync"
)
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
wg.Done()
}
func read() {
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64 // 定义一个int64类型的计数器变量
func main() {
var wg sync.WaitGroup
// 启动10个goroutine,每个goroutine调用atomic.AddInt64方法自增计数器
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("Counter value:", counter)
}
package main
import (
"fmt"
"time"
)
// 定义go协程池结构体
type GoPool struct {
MaxLimit int // 最大并发度
tokenChan chan struct{} // 控制并发执行的令牌通道
}
func main() {
gopool := NewGoPool(3) // 最大并发度为 3
defer gopool.Wait()
//开启10个线程,但最大并发度为3
for i := 0; i < 10; i++ {
taskID := i
gopool.Submit(func() {
// 模拟任务处理逻辑
fmt.Printf("任务 %d 开始执行\n", taskID)
time.Sleep(time.Second)
fmt.Printf("任务 %d 完成\n", taskID)
})
}
}
// NewGoPool 创建一个 GoPool 对象,max 设置最大并行度
func NewGoPool(max int) *GoPool {
p := &GoPool{}
p.MaxLimit = max
p.tokenChan = make(chan struct{}, p.MaxLimit)
for i := 0; i < p.MaxLimit; i++ {
p.tokenChan <- struct{}{} // 初始化 tokenChan
}
return p
}
// Submit 提交任务
// 每个任务都会异步执行,并且会在执行完后释放一个令牌
func (gp *GoPool) Submit(fn func()) {
token := <-gp.tokenChan // 如果没有可用令牌,则会阻塞
go func() {
fn()
gp.tokenChan <- token // 执行完成后释放令牌,以便其他任务可以获得执行机会
}()
}
// Wait 等待所有任务执行完成
func (gp *GoPool) Wait() {
for i := 0; i < gp.MaxLimit; i++ {
<-gp.tokenChan // 等待所有令牌被释放
}
close(gp.tokenChan) // 关闭令牌通道,释放资源
}
func (gp *GoPool) Size() int {
return len(gp.tokenChan) // 返回当前令牌通道中的可用令牌数量
}
这段代码创建了一个名为 “GoPool” 的结构体,它实现了一个简易的 goroutine 池来限制并发度。结构体包含最大并发度和控制同一时间能够执行 goroutine 数量的令牌通道。以下是函数及其功能:
main 函数演示了如何使用 GoPool 来异步运行一组任务。具体而言,使用 for 循环提交了十个匿名函数。 这些函数模拟处理逻辑,并在开始和结束时打印与任务 ID 相关的消息。在这种情况下,goroutine 池的最大并行度设置为3,这意味着在任何给定时间,只能有三个任务同时运行。
semaphore文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#semaphore
package main
import (
"fmt"
"olang.org/x/sync/semaphore"
"sync"
)
var sem *semaphore.Weighted = semaphore.NewWeighted(1) // 创建一个权重为1的信号量
func main() {
var wg sync.WaitGroup
// 启动10个goroutine,每个goroutine获取信号量并输出信息
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sem.Acquire(nil, 1) // 获取信号量
defer sem.Release(1) // 释放信号量
fmt.Println("Goroutine", id, "is running")
}(i)
}
wg.Wait()
}
在 Go 语言中,recovery 是一种机制,用于在程序发生 panic 时恢复程序的执行。当程序发生 panic 时,recovery 可以获 panic,并在程序崩溃前进行一些处理,例如输出日志、释放资源等。
在Go语言中,可以使用panic()
函数来抛出一个异常,从而在协程中添加错误。例如,我们可以在子协程中添加一个除数为0的错误,如下所示:
package main
import (
"fmt"
"time"
)
func childRoutine() {
defer func() {
if r := recover(); r != nil {
// 处理子协程中的错误
fmt.Println("子协程出现错误:", r)
}
}()
// 子协程中的代码
a, b := 10, 0
c := a / b // 除数为0,会抛出异常
fmt.Println("子协程执行完毕", c)
}
func main() {
// 启动子协程
go childRoutine()
// 主协程中的代码
time.Sleep(time.Millisecond * )
fmt.Println("主协程执行完毕")
}
在这个示例中,我们在子协程中定义了两个变量a
和b
,并将b
的值设置为0然后,我们尝试将a
除以b
,这会抛出一个异常。在defer
语句中,我们使用recover()
函数捕获这个异常,并在控制台输出错误信息。
当我们运行这个程序时,子协程会抛出一个异常,但是由于我们使用了recover()
函数来捕获异常,程序不会崩溃,而是会在控制台输出错误信息,并继续执行其他协程。这样,我们就可以在协程中添加错误,避免程序崩溃,更好地管理协程并处理异常,确保程序的稳定性。
Context文档介绍:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/
在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。
如下图所示,我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。
以下是 context 在 Go 并发编程中的一些应用:
以下是实现了这三种功能的简单代码示例
package main
import (
"context"
"fmt"
"time"
)
type keyType string
const key keyType = "value"
// worker 是一个 Goroutine,它会不断检查 context 中的元数据值
// 当元数据值达到 5 或超过 2 秒时,Goroutine 将被取消。
func worker(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("已超时,结束协程")
return
default:
id := ctx.Value(key).(int) // 从 context 中获取数据值
if id >= 5 {
fmt.Println("id超过5,结束协程")
return
}
fmt.Printf("接收到查询id为%d\n", id)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
// 创建一个带有超时功能的 context,超时时间为 2 秒
//context.Background() 作为根 context,返回回一个空的 context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// 使用 context.WithValue 为 context 添加一个名为 value 的元数据,初始值为 0
ctx = context.WithValue(ctx, key, 0)
// 逐渐增加 context 中的元数据值,并在循环结束后等待 3 秒,以确保 Goroutine 有足够的执行。
for i := 1; i <= 10; i++ {
time.Sleep(300 * time.Millisecond)
ctx = context.WithValue(ctx, key, i) // 更新 context 中的元数据值
// 启动 worker Goroutine
go worker(ctx)
}
time.Sleep(3 * time.Second)
}
GMP模型是一个高效的并行计算模型,它是Go语言运行时系统的一部分,负责将Go程序中的goroutine分配给多个处理器,以实现并行计算,从而提高程序的性能。
GMP调度模型支持任务窃取(task stealing)机制。当一个处理器空闲时,它可以从其他处理器的工作队列中窃取一个gor进行处理。这种机制可以使得goroutine的负载更加均衡,从而提高程序的性能。
GMP调度模型支持动态调整处理器数量的功能。当程序的负载发生变化时,调度器可以动态地增加或减少处理器的数量,以适应程序的需求。这种机制可以使得计算资源的利用更加高效。
GMP介绍文档:https://www.topgoer.cn/docs/golang/chapter09-11
M(machine)
P(processor)
G(goroutine)
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。
抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。
全局 G 队列:在新的调度器中依然有全局 G 队列,当绑定的P中没有G可以执行的时候,就去全局G队列中找
M0:m0就是进程启动后的初始线程
G0:代表着初始线程的stack