一. 进程 线程 协程
1.概念
进程:进程基本上是一个正在执行的程序,它是操作系统中最小的资源分配单位。
线程:线程是进程的子集,也称为轻量级进程。一个进程可以有多个线程,这些线程由调度器独立管理。一个进程内的所有线程都是相互关联的。线程是操作系统中最小的调度单位。
协程:可以看作轻量级线程,他的内存占用少只要 2k,且上下文切换成本低,是一个独立执行的函数,由 go 语言启动,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
2.对比
进程上下文切换开销:
1.地址空间
2.硬件上下文
线程上下文切换开销:
1.硬件上下文
2.同一进程下不切换地址空间
goroutine 切换开销:
1.用户态,不用象线程和进程一样多进行一次内核用户态切换
2.只需要保存/恢复三个寄存器的值,开销远远小于线程
其余优点:goroutine 的栈空间为 2k,线程为 2m,进程是 10m
由进程,线程,goroutine 的上下文切换可以明显看出是一个逐步减负的过程,这个过程可以结合它们的结构来理解,coverco 故而自带 goroutine 的 go 语言在高并发开发中有着得天独厚的优势。
3.编程思想
异步场景中需要对线程和协程进行人为的控制,就需要用到Context sync.WaitGroup 以及channel等技术。
二.Context
1.概念
context 是 golang 中的经典工具,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制. 除此之外,context 还兼有一定的数据存储能力. 本着知其然知其所以然的精神,本文和大家一起深入 context 源码一探究竟,较为细节地对其实现原理进行梳理.
2.原理
参考:https://zhuanlan.zhihu.com/p/597234214
3.代码案例
func WithCancle() {
time1 := time.Now()
wg.Add(1)
ctx, cancel := context.WithCancel(context.Background())
go func() {
ip, err := GetIp(ctx) //一个新的context对象传入协程
if err != nil {
fmt.Println(err)
}
fmt.Println(ip)
wg.Done()
}()
go func() {
time.Sleep(2 * time.Second)
cancel()
}()
wg.Wait()
fmt.Println("Done", time.Since(time1))
}
func GetIp(ctx context.Context) (ip string, err error) {
go func() {
select {
case <-ctx.Done():
fmt.Println("协程取消", ctx.Err())
}
wg.Done()
err = ctx.Err()
return
}()
time.Sleep(4 * time.Second)
ip = "172.0.9.1"
return
}
三.指针
1.概念
每一个变量都会分配一块内存,数据保存在内存中,内存有一个地址,就像门牌号,通过这个地址就可以找到里面存储的数据。
指针就是保存这个内存地址的变量。
在 Go 语言中, 指针包括两个核心概念:
- 类型指针,允许对这个指针类型的数据进行修改。传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃
2.代码案例
//形参与局部变量等同
//函数传参:值传递,实参将自己的值拷贝一份给形参
func Point() {
var x = 99
y := &x
fmt.Printf("x:%v, \n y:%v \n", x, y)
a, b := 10, 20
swap(a, b)
fmt.Println("Point1 a:", a, "b:", b)
swap2(&a, &b)
fmt.Println("Point2 a:", a, "b:", b)
swap3(&a, &b)
fmt.Println("Point3 a:", a, "b:", b)
swap4(&a, &b)
fmt.Println("Point4 a:", a, "b:", b)
swap5(a, b)
fmt.Println("Point5 a:", a, "b:", b)
}
func swap(a, b int) {
a, b = b, a
fmt.Println("swap a:", a, "b:", b)
}
func swap2(a, b *int) {
a, b = b, a //注意此处的a b 已经是内存地址了,地址值交换了没有意义,地址指向的值还是没有改变
fmt.Println("swap2 a:", a, "b:", b) //swap2 a: 0xc0000aef28 b: 0xc0000aef20
}
func swap3(a, b *int) {
*a, *b = *b, *a
fmt.Println("swap3 a:", *a, "b:", *b)
}
func swap4(a, b *int) {
*a = 11
*b = *a
fmt.Println("swap4 a:", *a, "b:", *b)
}
func swap5(a, b int) {
p := &a
q := &b
fmt.Println("swap5 p:", p, "q:", q) //swap5 p: 0xc0000aef30 q: 0xc0000aef38 此处的内存地址与swap2的内存地址已经不同了,说明函数传参:值传递,实参将自己的值拷贝一份给形参
}
四.Channel
1.概念
在 Go 语言中,Channel 是一种数据结构,用于在多个 goroutine 之间传递消息。它类似于管道的概念,支持两个基本操作:发送和接收。Channel 通过 <-
符号来发送和接收数据,确保 goroutine 之间能够安全地通信。
type hchan struct { qcount uint // 缓冲区中元素的数量 dataqsiz uint // 缓冲区的大小 buf unsafe.Pointer // 缓冲区指针 elemsize uint16 // 每个元素的大小 closed uint32 // 是否已关闭 sendx uint // 发送索引 recvx uint // 接收索引 recvq waitq // 等待接收的 goroutine 队列 sendq waitq // 等待发送的 goroutine 队列 lock mutex // 锁 } |
qcount
:缓冲区中当前元素的数量。dataqsiz
:缓冲区的大小,若为 0 则为无缓冲 Channel。buf
:指向缓冲区的指针,用于存储 Channel 中传递的数据。elemsize
:Channel 中元素的大小,用于内存对齐。closed
:标记 Channel 是否已关闭。sendx
和recvx
:发送和接收操作的索引,指示在缓冲区中数据的读取和写入位置。recvq
和sendq
:等待接收和发送的 goroutine 队列。当 Channel 无法立即进行传输时,goroutine 会被加入这些队列中等待被唤醒。lock
:保证对 Channel 操作的互斥。
2.Channel 的类型
Go 语言中的 Channel 有两种类型:无缓冲 Channel 和带缓冲 Channel。
无缓冲 Channel:发送和接收操作必须同时准备好,才能进行数据的传输。这意味着发送操作会阻塞直到接收操作准备好,反之亦然。
带缓冲 Channel:创建时可以指定缓冲区大小。发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区为空时才会阻塞。
3.channel的特性
(1) 线程安全:channel是线程安全的,多个协程可以同时读写一个channel,而不会发生数据竞争的问题。这是因为Go语言中的channel内部实现了锁机制,保证了多个协程之间对channel的访问是安全的。
(2) 阻塞式发送和接收:当一个协程向一个channel发送数据时,如果channel已经满了,发送操作会被阻塞,直到有其他协程从channel中取走了数据。同样地,当一个协程从一个channel中接收数据时,如果channel中没有数据可供接收,接收操作会被阻塞,直到有其他协程向channel中发送了数据。这种阻塞式的机制可以保证协程之间的同步和通信
(3) 顺序性:通过channel发送的数据是按照发送的顺序进行排列的
(4) 可以关闭: close(ch)
(5) 缓冲区大小: ch := make(chan int, 10)
4.使用 Channel 的注意事项
- 不要重复关闭 Channel:Channel 只能被关闭一次,重复关闭会引发 panic。
- 不要从 nil Channel 中收发数据:nil Channel 会导致永久阻塞。
- 关闭 Channel 后只可接收,不可发送:从已关闭的 Channel 接收不会阻塞,但向已关闭的 Channel 发送数据会引发 panic。
5.Channel 的应用场景
- goroutine 间通信:Channel 是 goroutine 之间传递数据的最佳方式。
- 任务调度:可以使用 Channel 实现任务的分发和处理,如工作池模型。
- 信号通知:Channel 可用于实现同步信号,例如通过一个
done
Channel 通知其他 goroutine 某个任务已完成。
6.代码案例
func Channel1() {
//定义channel
ch := make(chan int)
//1 将数据发送到channel
ch <- 11
//接收发送的数据
aa := <-ch
fmt.Println(aa)
//关闭channel
close(ch)
}
func Channel2() {
rand.Seed(time.Now().UnixNano())
a, b := longTimeRequest(), longTimeRequest()
fmt.Println(sumSquares(<-a, <-b))
/*
sumSquares函数的两个参数获取是同时的. 两个channel receive 会阻塞,直到channel send操作执行. 参数获取时间大概需要3s而不是6s.
*/
}
func longTimeRequest() <-chan int32 {
r := make(chan int32)
go func() {
// 模拟任务花费时间
time.Sleep(time.Second * 3)
r <- rand.Int31n(100)
}()
return r
}
func sumSquares(a, b int32) int32 {
return a*a + b*b
}
// channel for range
func ChannleRange() {
//管道传值
wg.Add(2)
ch := make(chan int, 10)
go func() {
defer func() {
close(ch)
wg.Done()
}()
for i := 0; i < 10; i++ {
fmt.Println("ch <-", i)
ch <- i
}
fmt.Println("add ok!!")
}()
go func() {
for {
if v, ok := <-ch; ok { //判断 channel 是否被关闭
fmt.Println(v, ":=<-ch")
} else {
fmt.Println("ch is close")
wg.Done()
return //注意这里的处理,当监听到ch已经关闭,就要告知上层进程本线程已经结束,wg.Done()并不能直接终止线程,所以后面还需要用return 返回
}
}
}()
wg.Wait()
fmt.Println("ch finish")
}
func SelectChannel() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(1 * time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- 2
}()
for i := 0; i < 2; i++ {
select {
case data, ok := <-ch1:
if ok {
fmt.Println("从 ch1 接收到数据:", data)
} else {
fmt.Println("通道已被关闭")
}
case data, ok := <-ch2:
if ok {
fmt.Println("从 ch2接收到数据: ", data)
} else {
fmt.Println("通道已被关闭")
}
}
}
select {
case data, ok := <-ch1:
if ok {
fmt.Println("从 ch1 接收到数据:", data)
} else {
fmt.Println("通道已被关闭")
}
case data, ok := <-ch2:
if ok {
fmt.Println("从 ch2接收到数据: ", data)
} else {
fmt.Println("通道已被关闭")
}
default:
fmt.Println("没有接收到数据,走 default 分支")
}
}
func OverTimeChannel() {
ch := make(chan int)
go func() {
time.Sleep(5 * time.Second)
ch <- 11
}()
select {
case data, ok := <-ch:
if ok {
fmt.Println("接收到数据:", data)
} else {
fmt.Println("通道被关闭")
}
case <-time.After(3 * time.Second):
fmt.Println("超时了!")
}
//注意 这里的case 不是监听同一个channel 的不同情况,而是不同渠道的不同处理方式
}
func GoroutineChannel() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch1 <- i
time.Sleep(time.Second)
}
}()
go func() {
for j := 0; j < 10; j++ {
ch2 <- j
time.Sleep(time.Second)
}
}()
for i := 0; i < 20; i++ {
select {
case data := <-ch1:
fmt.Println("data from ch1:", data)
case data := <-ch2:
fmt.Println("data from ch2:", data)
//default: //使用 default 实现非阻塞读写,不要default流程就会阻塞一直监听channel数据发送
// fmt.Println("no data ready")
// time.Sleep(time.Second)
}
}
fmt.Println("finish")
}