GO语言进阶| 青训营笔记

67 阅读4分钟

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

1.并发与并行

1.并发:多个线程在一个核的cpu下运行 2.并行:多个线程在多个核的cpu下运行

2.线程与协程

2.1简介

1.线程是cpu调度的一个基本单位,线程不拥有自己的系统资源,它与同属于同一进程的其他线程共享进程所拥有的全部资源,多个线程之间通过共享内存等线程间的通信方式来通信,线程拥有自己独立的栈和共享的堆。

2.协程是轻量级线程,一个线程可以拥有多个协程,与线程相比,协程不受操作系统调度,协程调度器按照调度策略把协程调度到线程中执行,协程调度器由应用程序的runtime包提供,用户使用go关键字即可创建协程。

相对来说,协程存在于用户态,切换效率较线程高。

2.2创建协程

使用 go func(){具体实现代码}()可创建一个协程。当一个子协程创建后,它会与主协程互相竞争cpu,导致每一次输出的顺序都不同

func main() {
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println("子协程:",i)
		}
	}()
	for i := 0; i < 10; i++ {
		fmt.Println("主协程:",i)
	}
}

image.png

除此之外,也可以使用go 函数名()的方法直接使一个已经定义好的函数作为一个子协程开始工作。

func hello(i int) {
	for i := 0; i < 10; i++ {
		fmt.Println("子协程:",i)
	}
}

func main() {
	go hello(0)
	for i := 0; i < 10; i++ {
		fmt.Println("主协程:",i)
	}
}

2.3协程执行

如下图所示,当主协程中开启一个子协程后,预期输出为

子协程hello

主协程hello

但是实际上只输出了一句主协程hello

func hello() {
	fmt.Println("子协程hello")
}

func main() {
	go hello()
	fmt.Println("主协程hello")
        //time.Sleep(time.Second)
}

image.png

这是因为程序开始执行时,cpu会首先调度主协程,运行到go hello()语句时将子协程放进本地协程队列中等待调度,但是主协程执行完所有语句后会直接结束程序,这时子协程就没机会被调度了。

解决方法很简单,只需要在主协程的最后加上一个time.sleep()方法使主协程休眠一段时间即可,如取消上段代码的注释内容。主协程休眠之后,cpu会随机寻找各个协程本地队列的每一个协程进行调度,这时子协程才有机会执行。

image.png

2.4协程安全

协程执行时有着隐藏的安全问题。如下段代码所示,定义有两个方法实现对全局变量x的累加,其中一个方法加有锁,每进行一次累加之前加锁使协程强制占用cpu资源直至解锁。加锁方法可以稳定使预期输出为6000,而不加锁的方法每次都会输出不一样的值,存在着安全隐患。

var x int
var lock sync.Mutex

func addWithLock() {
	for i := 0; i < 1000; i++ {
		lock.Lock()
		x += 1
		lock.Unlock()
	}
}

func addWithoutLock() {
	for i := 0; i < 1000; i++ {
		x += 1
	}
}

func main() {
	x = 0
	for i := 0; i < 6; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	fmt.Println("addWithoutLock:", x)

	x = 0
	for i := 0; i < 6; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	fmt.Println("addWithLock:", x)
}

image.png

2.5通道channel

通道channel是go语言中的一种特殊类型,由具体数据类型定义的处理协程的结构,类似于队列,遵循先进先出的规则。

 //声明规则
 var 变量 chan 元素类型

使用无缓冲通道发送、接收数据。一个协程发送值,另一个协程接收值,此时如果任意一个协程没有被执行都会进入阻塞状态。如果发送协程执行后会进入阻塞,直到接收协程成功接收数据,或如果接收协程先执行则会进入阻塞,直到发送协程给其发送数据。

func recv(i chan int) {
    num := <-i
    fmt.Println("已接收", num)
}
func main() {
    ch := make(chan int)
    go recv(ch) 
    ch <- 10
    fmt.Println("已发送")
}

除此之外还可以建立一种有缓冲通道,定义时需要给定一个容量参数,意味着能够使通道暂时存储所传入的数据,只有数据量达到容量后才会发生阻塞。

ch := make(chan int, 1)

总结

本次学习学到了go语言关于协程的相关知识,协程作为一种轻量级线程,运行效率比线程要高,对于拥有高并发特性的go语言来说有着非常重要的地位。学习协程需要搞清楚协程的底层调用原理,比如资源抢占、协程休眠、调用顺序等相关知识点。