青训营伴学笔记——Go 语言进阶 | 青训营笔记

74 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

协程

协程与线程的对比

相比于c++里面的线程来说。GO语言的协程可以充分发挥CPU的多核性能,高效运行程序。

image.png

线程:有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。其栈是MB级别的。

协程:是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。其栈是KB级别的。

协程相比于线程的优势

  • 协程最大的优势在于“轻量级”:可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程可以称为“轻量级线程”的原因。

  • 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;

  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

image.png

协程的创建

只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。


package concurrence

import (
	"fmt"
	"time"
)

func hello(i int) {
	println("hello goroutine : " + fmt.Sprint(i))
}

func HelloGoRoutine() {
	for i := 0; i < 5; i++ {
                // 这里使用的是匿名函数
		go func(j int) {
			hello(j)
		}(i)// 函数输入的是i
	}
	time.Sleep(time.Second)
}


channel (通道)

并发核心单元通过 channel 就可以发送或者接收数据进行通讯,可以降低编程的难度。

协程运行在相同的地址空间,因此访问共享内存必须做好同步。多程更加推荐通过通信共享内存而不是共享内存来实现通信,那么我们就会使用到通道。 image.png

channel 的使用

和其他数据结构相似(map、切片),channel也一个对应make创建的底层数据结构的引用

当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。

make(chan Type)  //等价于make(chan Type, 0),无缓冲通道
make(chan Type, capacity)// 有缓冲通道

image.png 无缓冲的通道是指在接收前没有能力保存任何数据值的通道。

package concurrence

func CalSquare() {
        // src 是无缓存 channel
	src := make(chan int)
        // dest 是缓存大小为3的 channel
	dest := make(chan int, 3)
        // A 子协程发送0~9,放到channel src中
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
                close(src)// 关闭通道
	}()
        // B 子协程从 src 中提取A子协程发送的0~9,计算平方后放进 dest 中
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
                close(dest)// 关闭通道
	}()
        // 主协程,从 dest 提取出 B 子协程的计算结果并输出
	for i := range dest {
		//复杂操作
		println(i)
	}
}

当没有值需要发送到channel的话,也让接收者也能及时知道没有多余的值可接收将是有用的,就可以让接收者停止不必要的接收等待。我们可以通过内置的close函数来关闭channel实现。

这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

阻塞: 由于某种原因数据没有到达,当前go程(线程)持续处于等待状态,直到条件满足,才解除阻塞。

同步: 在两个或多个go程(线程)间,保持数据内容一致性的机制。

并发安全 Lock

同时只有一个gotoutine可以访问共享资源 有时候go代码中可能会存在多个goroutine同时操作一个资源,这种情况会发生竞争问题。这时我们就需要锁来保证同时只有一个gotoutine可以访问共享资源,以此来保证资源的安全。

package concurrence

import (
	"sync"
	"time"
)

var (
	x    int64
	lock sync.Mutex
)

func addWithLock() {
	for i := 0; i < 2000; i++ {
                // 当对x执行操作时加锁防止其他协程对此进行操作
		lock.Lock()
		x += 1
		lock.Unlock()
	}
}
func addWithoutLock() {
	for i := 0; i < 2000; i++ {
                // 没有加锁
		x += 1
	}
}

func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock:", x)
}

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()
			hello(j)
		}(i)
	}
	wg.Wait()
}