线程和协程
操作系统中有三个重要的概念,分别是进程、线程和协程。
简单来说,协程又称为用户态线程(以下的线程均指的是内核级线程),它比线程更加轻量化,使用起来更灵活,具有更高的性能。具体来说,协程的各种操作所需要的开销要比线程少,因此具有更高的性能。协程线程是内核态的,栈是MB级别的;协程是用户态的,其栈是KB级别的。一个线程可以控制多个协程,Go语言自动完成协程的创建,Go一次可以创建上万条协程,因此Go在针对高并发场景上有优势
Go使用如下方法创建协程
go func(形式参数){
函数体
}(实际参数)
import (
"fmt"
"time"
)
func hello(i int) {
println("Hello goroutine" + fmt.Sprint(i))
}
func main() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
其输入如下:
Hello goroutine1
Hello goroutine4
Hello goroutine2
Hello goroutine0
Hello goroutine3
可以看出协程的并发性 进行随机
Channel
通道是Go进行通信的重要手段
make(chan 元素类型, [缓冲区大小])
// example
make(chan int) //无缓冲通道
make(chan char, 2)
无缓冲通道中1发送的信息回立即传送到2中,会出现同步问题。而有缓冲的通道则会先放置到缓冲区中再送入2,带缓冲通道的可以解决一些速度不匹配问题
package main
func main() {
src := make(chan int) // 无缓冲通道
dest := make(chan int, 3) // 缓冲区为3的通道
// 生产者协程,用于生产数字
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i //将数据i冲入通道src中
}
}()
// 消费者进程,取出src中的数字并且将其平方后放入dist通道
go func() {
defer close(dest)
for i := range src {
dest <- i * i //将i的平方冲入dist通道中
}
}()
for i := range dest {
println(i)
}
通过结果可以看出,通道可以保证输出的顺序
并发安全:锁Lock
使用锁可以确保对临界资源的互斥访问,从而避免同步互斥发生的数据不匹配问题
在加锁的情况下,防止不同协程同时访问同一个资源导致的错误现象,例如:x=10,协程1,2分别同时访问x执行x=x+1的操作。
那么正确的情况应该是x返回12 就是在协程1执行x操作后返回x=11之后协程2再进行x操作返回x=12
错误的情况就是返回x=11,协程1,2同时获取x=10执行x=x+1操作的错误
WaitGroup
Go提供了WaitGroup来实现并发任务的同步,其中主要的三个方法:
Add(delta int) //有多少个并发的协程 Done() // 表示协程已完成,会将计数器的值-1 Wait() //在计数器为0之前,一直阻塞不向下执行
这类似于一个计数器,刚开始使用Add表示有n个协程,而Done方法表示协程已完成,会将n–,当n!=0的时候,会一直触发Wait()方法,使得程序阻塞在Wait处;当n=0的时候表示所有协程均已完成,程序会继续执行Wait之后的语句