Go并发编程 | 青训营

65 阅读2分钟

线程和协程

操作系统中有三个重要的概念,分别是进程、线程和协程。

简单来说,协程又称为用户态线程(以下的线程均指的是内核级线程),它比线程更加轻量化,使用起来更灵活,具有更高的性能。具体来说,协程的各种操作所需要的开销要比线程少,因此具有更高的性能。协程线程是内核态的,栈是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之后的语句