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
Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐