12、go的协程和管道

协程概念

又称微线程,纤程,协程是一种用户态的轻量级线程

作用:在执行A函数时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(一个线程中有多个协程)
image
代码案例:开启一个协程非常简单,调用函数时候前面加个go就好了

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("hello golang + ", strconv.Itoa(i))
		// 阻塞1秒
		time.Sleep(time.Second)
	}
}

func main() { // 主线程
	// 开启一个协程
	go test()
	// 主线程也打印10次
	for i := 0; i < 10; i++ {
		fmt.Println("hello 王彪 + ", strconv.Itoa(i))
		// 阻塞1秒
		time.Sleep(time.Second)
	}
}

主死从随

主线程和协程执行流程:
image
验证代码:让协程执行的久一点,主线程结束的早一点

func test() {
	// 从线程打印100次
	for i := 0; i < 100; i++ {
		fmt.Println("hello golang + ", strconv.Itoa(i))
		// 阻塞1秒
		time.Sleep(time.Second)
	}
}

func main() { // 主线程
	// 开启一个协程
	go test()
	// 主线程也打印10次
	for i := 0; i < 10; i++ {
		fmt.Println("hello 王彪 + ", strconv.Itoa(i))
		// 阻塞1秒
		time.Sleep(time.Second)
	}
}

打印:
image
主线程结束了,所以协程也结束了,可以看到协程还多打印了一次,说明在主线程结束的一瞬间,协程还可以苟延残喘一下。
另一种情况,如果协程执行的很快,主线程还没结束,那就正常协程结束就好了,主线程继续执行。

启动多个协程

var w sync.WaitGroup // 只定义无需赋值
func main() {
	w.Add(5)
	for i := 0; i < 5; i++ {
		// 启动五个协程,匿名函数
		go func(n int) {
			defer w.Done()
			// 因为闭包的概念,如果这里不加参数接收的话,直接打印i,可能会打印5,
			fmt.Println(n) // 乱序打印0到4
		}(i)
	}
	// 主线程要等待,不然协程很可能没执行完就被主死从随
	w.Wait()
}

多协程操作同一数据

先看问题代码:

// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值

func add() {
	defer wg.Done() // 每次启动完协程以后,就-1
	for i := 0; i < 100000; i++ {
		totalNum = totalNum + 1
	}
}
func sub() {
	defer wg.Done() // 每次启动完协程以后,就-1
	for i := 0; i < 100000; i++ {
		totalNum = totalNum - 1
	}
}

func main() {
	wg.Add(2) // 每次启动就+2
	// 启动协程
	go add()
	go sub()
	wg.Wait() // 主线程一直阻塞,什么时候协助执行完了,wg就减成0了,就停止阻塞
	fmt.Println(totalNum)
}

正常来说,最后会打印0,但其实不是,每次执行都是不同的数据且不是0
问题出错的可能原因:
image
解决问题:加入互斥锁

使用互斥锁同步协程

// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
// 加入互斥锁
var lock sync.Mutex
func add() {
	defer wg.Done() // 每次启动完协程以后,就-1
	for i := 0; i < 100000; i++ {
		// 加锁
		lock.Lock()
		totalNum = totalNum + 1
		// 解锁
		lock.Unlock()
	}
}
func sub() {
	defer wg.Done() // 每次启动完协程以后,就-1
	for i := 0; i < 100000; i++ {
		lock.Lock()
		totalNum = totalNum - 1
		lock.Unlock()
	}
}

func main() {
	wg.Add(2) // 每次启动就+2
	// 启动协程
	go add()
	go sub()
	wg.Wait() // 主线程一直阻塞,什么时候协助执行完了,wg就减成0了,就停止阻塞
	fmt.Println(totalNum)
}

运行多遍,发现都为0,没问题

WaitGroup控制协程退出

var wg sync.WaitGroup	// 只定义无需赋值
func main() {
	for i := 0; i < 5; i++ {
		wg.Add(1)	// 协程开始时候+1
		go func(n int) {
			fmt.Println(n)
			wg.Done()		// 协程执行完-1
		}(i)
	}
	// 主线程一直阻塞,等到wg减到0时候,就停止阻塞
	wg.Wait()
}

读写锁

读的时候,数据之间不受影响,写和读的时候就会受影响

var wg sync.WaitGroup
// 加入互斥锁
var rwLock sync.RWMutex

func read() {
	defer wg.Done()
	rwLock.RLock() // 读数据锁不产生影响
	fmt.Println("开始读数据")
	time.Sleep(time.Second)
	fmt.Println("读取数据成功")
	rwLock.RUnlock()
}
func write() {
	defer wg.Done()
	rwLock.Lock()
	fmt.Println("开始写数据")
	time.Sleep(time.Second * 10)
	fmt.Println("数据写成功")
	rwLock.Unlock()
}

func main() {
	wg.Add(6) // 每次启动就+2
	// 测试读多写少
	for i := 0; i < 5; i++ {
		go read()
	}
	go write()
	wg.Wait() // 主线程一直阻塞,什么时候协助执行完了,wg就减成0了,就停止阻塞
}

允许代码后测试发现:写数据时候锁生效,会阻塞,读的时候是可以并行的

管道

管道就是数据结构-队列,数据是先进先出,自身也线程安全的,多个协程访问时不用加锁,管道也有数据类型的。
管道定义好容量后,不能多存入或者多取出数据,会报错

func main() {
	// 定义一个int类型的管道
	var intChan chan int
	// 通过make初始化,管道可以存放3个int类型的数据
	intChan = make(chan int, 3)

	// 证明管道是引用类型
	fmt.Printf("intChan的值:%v", intChan) // 0xc00008e000

	// 给管道里存放数据
	intChan <- 10
	num := 20
	intChan <- num
	//intChan <- 40
	// 不能存放大于容量的数据
	//intChan <- 80
	// 输出管道的长度
	fmt.Printf("管道实际长度:%v, 管道容量:%v", len(intChan), cap(intChan))
	fmt.Println()
	// 取出管道里的值
	num1 := <-intChan
	fmt.Println(num1) // 10
	num2 := <-intChan
	fmt.Println(num2) // 20
}

管道的关闭

管道关闭后,可以取数据,但不可以往管道里存放数据了

func main() {
	// 定义一个int类型的管道
	var intChan chan int
	// 通过make初始化,管道可以存放3个int类型的数据
	intChan = make(chan int, 3)

	// 给管道里存放数据
	intChan <- 10
	intChan <- 20

	// 关闭管道
	close(intChan)

	// 再次往里面写入数据,会报错
	//intChan <- 30

	// 管道关闭后读数据,可以正常使用
	fmt.Println(<-intChan)	// 10
}

管道的遍历

func main() {
	// 定义一个int类型的管道
	var intChan chan int
	intChan = make(chan int, 100)
	// 往管道放100个数据
	for i := 0; i < 100; i++ {
		intChan <- i
	}
	// 遍历管道前要先关闭管道,不然遍历管道结束后还会遍历(读取管道数据),就会报错
	close(intChan)
	//遍历管道
	for v := range intChan {
		fmt.Println(v)
	}
}

协程和管道协同工作

image

var wg sync.WaitGroup

// 写
func writeData(intChan chan int) {
	defer wg.Done()
	for i := 0; i < 50; i++ {
		intChan <- i
		fmt.Println("写入的数据:", i)
		time.Sleep(time.Second)
	}
	close(intChan)
}

// 读
func readData(intChan chan int) {
	wg.Done()
	// 遍历
	for v := range intChan {
		fmt.Println("读取的数据:", v)
		time.Sleep(time.Second)
	}
}
func main() {
	// 写协程和读协程共同操作同一个管道

	// 定义管道
	ichan := make(chan int, 50)
	// 开启读和写的协程
	wg.Add(2)
	go writeData(ichan)
	go readData(ichan)
	wg.Wait()
}

只读只写管道

默认情况下,管道是可读可写的,我们也可以声明为只写或只读的

func main() {
	// 声明只写
	var intChan2 chan<- int
	intChan2 = make(chan int, 3)
	intChan2 <- 10
	// 读操作时候,是报错的
	//num := <-intChan2
	fmt.Println("intChan2:", intChan2)

	// 声明只读
	var intChan3 <-chan int
	// 允许会报错
	//num := <-intChan3
	//fmt.Println(num)
	// 可以这样不让报错
	if intChan3 != nil {
		num := <-intChan3
		fmt.Println(num)
	}
}

管道的阻塞

只写不读,容量满了还在写,就会出现阻塞错误,如果读和写效率一月,甚至读的慢写的快,都不会出现阻塞

select

解决多个管道选择问题,随机公平的选择一个管道执行,
case后面必须写io操作,不能是等值
default防止select阻塞

func main() {
	intCh := make(chan int, 1)
	go func() {
		time.Sleep(time.Second * 5)
		intCh <- 10
	}()
	// 再定义一个string类型的管道
	stringCh := make(chan string, 1)
	go func() {
		time.Sleep(time.Second * 2)
		stringCh <- "彪哥学golang"
	}()

	select { // 多路复用,随机选一个效率高的,stringCh休眠时间短,所以会打印stringCh
	case v := <-intCh:
		fmt.Println("intCh:", v)
	case v := <-stringCh:
		fmt.Println("stringCh:", v)
	default: // 加上default防止select阻塞,有阻塞直接执行default里面的了
		fmt.Println("防止select阻塞")
	}
}

defer + recover 处理异常

// 输出数字
func printNum() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}

// 做除法操作
func devide() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("除法错误:", err)
		}
	}()
	num1 := 10
	num2 := 0
	result := num1 / num2
	fmt.Println(result)
}
func main() {
	go printNum()
	go devide() // 此协程虽然出现异常,但程序不会报错停止,上面的协程还是会正常打印0-9

	time.Sleep(time.Second * 5)
}
posted @ 2022-11-25 16:00  aBiu--  阅读(89)  评论(0编辑  收藏  举报