Golang 进阶3—— 协程&管道
在任务切换回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证该线程能够最大限度处于就绪态,即随时都可以被CPU执行的状态,相当于我们在用户级别将自己的IO操作最大限度隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,IO比较少,从而会更多的将cpu的执行权限分配给我们线程。线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,从而更加轻量级。自身
Golang 进阶3—— 协程&管道
注意,该文档只适合有编程基础的同学,这里的go教程只给出有区别的知识点
注意 程序 & 进程 & 协程 的区别
1. 协程
又称为微线程, 纤程, 协程是一种用户态的轻量级线程
作用: 在执行 A 函数的时候, 可以随时中断, 去执行B函数,然后中断继续执行A函数(可以自由切换), 注意这一切换并不是函数调用(没有调用语句), 过程很像多线程,然而协程中只有一个线程在执行 (协程的本质是个单线程)。线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,从而更加轻量级。
对于单线程下, 我们不可避免程序中出现IO 操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到IO阻塞时就将寄存器上下文和栈保存到某个地方,然后切换到另外一个任务去计算。在任务切换回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证该线程能够最大限度处于就绪态,即随时都可以被CPU执行的状态,相当于我们在用户级别将自己的IO操作最大限度隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,IO比较少,从而会更多的将cpu的执行权限分配给我们线程。
1.1 main文件
import (
"fmt"
"time"
"strconv"
)
func test () {
for i := 1; i <= 10; i++ {
fmt.Println("test, golang", strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {// 主线程
go test() // 启动一个协程, 在语句前面加上 go
for i := 1; i <= 9; i++ {
fmt.Println("main, golang", strconv.Itoa(i))
// 阻塞 1 秒
time.Sleep(time.Second)
}
}
1.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
main, golang 1
test, golang 1
test, golang 2
main, golang 2
main, golang 3
test, golang 3
test, golang 4
main, golang 4
main, golang 5
test, golang 5
test, golang 6
main, golang 6
main, golang 7
test, golang 7
test, golang 8
main, golang 8
main, golang 9
test, golang 9
// 从结果可以看出,当主线程死掉之后,协程会直接死掉,不会继续执行。(本来还要继续输出: test, golang 10)—— 主死从随
// 不加 go 的情况
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
test, golang 1
test, golang 2
test, golang 3
test, golang 4
test, golang 5
test, golang 6
test, golang 7
test, golang 8
test, golang 9
test, golang 10
main, golang 1
main, golang 2
main, golang 3
main, golang 4
main, golang 5
main, golang 6
main, golang 7
main, golang 8
main, golang 9
1.3 启动多个协程
import (
"fmt"
"time"
)
func main() {// 主线程
for i := 0; i < 10; i++ {
go func (i int) {
fmt.Println(i)
}(i)
}
time.Sleep(time.Second * 2)
}
1.4 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0
1
9
5
6
3
7
4
8
2
2. WaitGroup
WaitGroup 用于等待一组线程结束的结束。 父线程调用 Add 方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。 ——> 解决主线程在子协程结束之后自动结束。(详细可以查看api中的sync 包)
2.1 main文件
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {// 主线程
// 启动 5 个协程
for i := 0; i < 5; i++ {
wg.Add(1) // 计数器加一
go func (i int) {
fmt.Println(i)
defer wg.Done() // 协程执行完毕,计数器减一
}(i)
}
wg.Wait() // 等待协程执行结束, 当计数器达到 0 的时候才结束
}
2.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0
1
3
4
2
2.3 多线程对同一个数进行操作 —— 要加锁 (互斥锁)
// 竞争条件:sum 变量被多个 goroutine 并发访问,而没有适当的同步机制(如互斥锁)。这会导致数据的竞争条件,使得 sum 的值可能计算不正确。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var mu sync.Mutex // 互斥锁
var sum int
func add () {
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func (i int) {
mu.Lock() // 加锁
sum += i
mu.Unlock() // 解锁
defer wg.Done()
}(i)
}
}
func sub () {
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func (i int) {
mu.Lock() // 加锁
sum -= i
mu.Unlock() // 解锁
defer wg.Done()
}(i)
}
}
func main() {// 主线程
add()
sub()
wg.Wait() // 等待协程执行结束, 当计数器达到 0 的时候才结束
fmt.Println(sum)
}
2.4 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0
2.5 另外一种锁
RWMutex是一种读写锁, 其经常用于读的次数远远多于写次数的场合—— 在读的时候,数据之间不产生影响,写和读之间才会产生影响
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var lock sync.RWMutex // 读写锁
func read () {
wg.Add(1)
defer wg.Done()
lock.RLock() // 读锁
fmt.Println("read")
time.Sleep(time.Second)
lock.RUnlock() // 解锁
}
func write () {
wg.Add(1)
defer wg.Done()
lock.Lock() // 写锁
fmt.Println("write")
time.Sleep(time.Second * 3)
fmt.Println("write over")
defer lock.Unlock() // 解锁
}
func main() {// 主线程
for i := 0; i < 10; i++ {
go read()
}
go write()
wg.Wait() // 等待协程执行结束, 当计数器达到 0 的时候才结束
}
2.6 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
read
read
write
write over
read
read
read
read
read
read
read
read
3. 管道
管道的本质就是一个数据结构——队列(先进先出)。 自身线程安全,不需要加锁,channel本身就是线程安全的,多个协程操作同一个管道时候,不会发生资源争抢问题。管道有类型,一个string的管道只能存放string类型的数据。
3.1 main文件
import (
"fmt"
)
/*管道
chan管道关键字
数据类型是指管道的类型,里面放入数据的类型, 管道是有类型的, int类型的管道只能写入int
管道是引用类型,必须初始化才能写入数据,即make之后才能使用
*/
func main() { // 主线程
// 定义管道
var inChan chan int
// 通过make初始化, 管道可以存放3个int类型的变量
inChan = make(chan int, 3)
// 证明是引用类型
fmt.Println(inChan) // 输出管道地址
// 存放数据
inChan <- 1
inChan <- 2
inChan <- 3
// 超过定义的长度,会报错 fatal error: all goroutines are asleep - deadlock!
// inChan <- 4
fmt.Println("管道的长度:", len(inChan))
// 取数据
num1 := <-inChan
fmt.Println(num1)
num2 := <-inChan
fmt.Println(num2)
num3 := <-inChan
fmt.Println(num3)
// 关闭管道, 关闭之后,还可以读数据,但是写不了数据了
close(inChan)
fmt.Println("--------------------------------------------------")
// 管道遍历
for v := range inChan { // 没有索引
fmt.Println("value = ", v)
}
// 重新初始化管道
inChan = make(chan int, 3)
inChan <- 4
inChan <- 5
inChan <- 6
close(inChan)
// 管道遍历 在遍历前如果没有加入 close(inChan) 会报错, fatal error: all goroutines are asleep - deadlock!
// 所以在遍历前要把管道关闭
for v := range inChan { // 没有索引
fmt.Println("value = ", v)
}
}
3.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
0xc000080100
管道的长度: 3
1
2
3
--------------------------------------------------
value = 4
value = 5
value = 6
3.3 协程 + 管道
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
// 写入数据
func writeFunc (ch chan int) {
defer wg.Done()
for i := 1; i <= 50; i++ {
fmt.Println("write:", i)
ch <- i
time.Sleep(time.Second)
}
close(ch)
}
// 读数据
func readFunc (ch chan int) {
defer wg.Done()
for n := range ch {
fmt.Println("read data:", n)
time.Sleep(time.Second)
}
}
func main() { // 主线程
fmt.Println("rw")
wg.Add(2)
ch := make(chan int, 50) // 创建管道,容量为50
go writeFunc(ch)
go readFunc(ch)
wg.Wait()
}
3.4 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
rw
write: 1
read data: 1
write: 2
read data: 2
write: 3
read data: 3
write: 4
read data: 4
write: 5
read data: 5
write: 6
read data: 6
write: 7
read data: 7
...
3.5 声明只读只写管道
func main() { // 主线程
//ch := make(chan int, 10) // 默认情况下,管道可读可写
// 声明只写
// ch2 := make(chan<- int, 10)
// ch2 <- 1
// num1 := <-ch2
// fmt.Println(num1) // invalid operation: cannot receive from send-only channel ch2 (variable of type chan<- int)
// 声明只读
ch3 := make(chan int, 10)
ch3 <- 1
ch3 <- 2
close(ch3)
if ch3 == nil {
fmt.Println("ch3 is nil")
} else {
num2 := <-ch3
fmt.Println(num2)
}
}
3.6 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
1
3.7 管道阻塞
当管道只写入数据,没有读出数据就会进入阻塞状态
var wg sync.WaitGroup
func main() { // 主线程
ch := make(chan int, 10) // 默认情况下,管道可读可写
// 值写入不读 fatal error: all goroutines are asleep - deadlock!
for i := 1; i <= 10; i++ {
wg.Add(1)
defer wg.Done()
go func(i int) {
ch <- i
}(i)
}
wg.Wait()
fmt.Println("over")
}
3.8 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000008108?)
D:/Go/src/runtime/sema.go:71 +0x25
sync.(*WaitGroup).Wait(0xc0000281c0?)
D:/Go/src/sync/waitgroup.go:118 +0x48
main.main()
E:/Goproject/src/gocode/testproject01/main/main.go:20 +0x12e
exit status 2
4. select
功能: 解决多个管道的选择问题, 也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行;
case后面必须进行的是IO操作,不能是等值,随机去选择一个IO执行
default 防止select被阻塞, 加入default
4.1 main
import (
"fmt"
"time"
)
func main() { // 主线程
intCh := make(chan int, 10)
stringCh := make(chan string, 10)
go func () {
time.Sleep(time.Second * 2)
intCh <- 1
}()
go func () {
time.Sleep(time.Second * 3)
stringCh <- "hello"
}()
select {
case data := <-intCh:
fmt.Println("intChan :", data)
case data := <-stringCh:
fmt.Println("stringChan :", data)
default:
fmt.Println("防止select被阻塞")
}
}
4.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
防止select被阻塞
5. refer + recover 解决错误
5.1 main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
// 输出操作
func printNum () {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
// 除法操作
func devide () {
defer wg.Done()
defer func() {
err := recover()
if err != nil {
fmt.Println("程序异常: ", err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("结果: ", res)
}
func main() { // 主线程
wg.Add(2)
go printNum()
go devide()
wg.Wait()
}
5.2 输出结果
(base) PS E:\Goproject\src\gocode\testproject01> go run .\main\main.go
程序异常: runtime error: integer divide by zero
0
1
2
3
4
5
6
7
8
9
更多推荐
所有评论(0)