线程和协程
线程:操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,是进程中的实际运作单位。
**协程:**又称微线程。协程是一种用户态的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
为什么有了线程还需要协程
虽然多线程在前互联网世代已经足够使用,但是线程的局限性也比较明显
1.线程数量有限,一般不会很多
2.线程占据的资源通常比我们需要的多得多,造成浪费
每个系统级线程开辟都会占用空间,这个空间可能是MB级别,但是我们如果使用的线程只需要传递KB级别数据,那么线程看起来就会比较浪费,但是又不可避免。而且线程之间的切换也会占用一些额外开销。
为了解决上述的问题,引入了协程(goroutine):比线程更轻量,更少的资源消耗,动态调配资源
协程的优点
1.协程之间的切换由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
2.不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
在golang中,goroutine的创建消耗非常小,大约是KB级别。因此可以创建更多的协程,尤其是数量越多相对线程优势更加明显,而且goroutine可以动态伸缩,栈溢出风险也比线程更低。
golang的并发与goroutine的使用
var name = "yiyiyinhe"
func changeName() {
name = "change"
}
func sayHi() {
fmt.println("hi, ", name)
go changeName() // 协程
}
只需要使用go关键字,我们就创建了一个简单的协程,但是demo输出的可能是
1.“hi,yiyiyinhe”
2.“hi,change”
这是因为协程的执行顺序是不可预估的
如果是想对一个代码块使用协程,如下所示
var name = "yiyiyinhe"
func sayHi() {
fmt.println("hi, ", name)
go func() { // 匿名函数执行协程
name = "change"
}
}
channel
什么是channel
channel是一种通信机制,它让一个goroutine向另一个goroutine发送值。每个通道都是特定类型(通道元素类型)值的管道。
我们通过信道操作符<-来发送或者接收数据
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
如何创建channel
创建的channel分为有缓存和无缓存两种,区别就是创建的时候是否分配大小
1.无缓存channel
var ch1 = make(chan int)//未分配大小
var ch2 = make(chan int, 0)//分配大小为0也等同给于未分配大小
2.有缓存channel
var :ch3 = make(int, 3)//分配大小为3的有缓存channel
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
还是用上述的例子
var name = "yiyi"
var flag = make(chan bool) // 创建了bool类型的channel
func changeName() {
name = "change"
flag <- true // 发送
}
func sayHi() {
go changeName() // 协程
<-flag // 接收
fmt.println("hi, ", name)
}
这时候的输出结果就一定是"hi,change"
对照着上图,我们知道尾部的接手必须发生在头部的发生操作之前,所以在<-flag这一步的时候头部的发送操作一定是完成的了,也就表明此时的name已经是"change",实现了一个简单的**同步操作 **
main()也可以看做一个协程
在没有使用channel的时候,我们来看一个例子:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Reveal romantic feelings...")
go sendLove()
go responseLove()
waitFor()
fmt.Println("Leaving ☠️....")
}
func waitFor() {
for i := 0; i < 5; i++ {
fmt.Println("Keep waiting...")
time.Sleep(1 * 1e9)
}
}
func sendLove() {
fmt.Println("Love you, mm ❤️")
}
func responseLove() {
time.Sleep(6 * 1e9)
fmt.Println("Love you, too")
}
不难看出输出结果如下
Reveal romantic feelings…
Love you, mm ❤️
Keep waiting…
Keep waiting…
Keep waiting…
Keep waiting…
Keep waiting…
Leaving ☠️…
为什么我们明明已经将回答写进程序中,但是并没有输出呢——这是因为在回应输出之前,main方法已经执行完了,这直接就导致所有的协程都停止工作,自然也就没办法输出了
但是借助channel,我们就可以实现main协程和子协程之间的通信了
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
var answer string
fmt.Println("Reveal fomantic feelings...")
go sendLove()
go responseLove(ch)
waitFor()
answer = <-ch //1.这一步在没有数据发送之前会被阻塞
if answer != "" {
fmt.Println(answer)
} else {
fmt.Println("Dead ☠️....")
}
}
func waitFor() {
for i := 0; i < 5; i++ {
fmt.Println("Keep waiting...")
time.Sleep(1 * 1e9)
}
}
func sendLove() {
fmt.Println("Love you, mm ❤️")
}
func responseLove(ch chan string) {
time.Sleep(6 * 1e9)
ch <- "Love you, too"// 2.第一步必须等待这行代码执行完成才可以继续运行
}
我们借助channel,就实现了一个简易的协程同步控制