跳至主要內容

协程

LincZero大约 3 分钟

协程

协程 (Goroutine)

启动协程 (go关键字)

语言级的并发,语法非常简单,只要调用函数 (还能是立即执行函数) 前加一个 go 关键词,体现了go的"优雅哲学"

package main 

import(
	"fmt"
    "strconv"
    "time"
)

// 协程函数 (功能为持续输出)
func test(dur int) {
    for i:=1; i<=dur; i++ {
        fmt.Println("hello golang + " + strconv.Itoa(i))
        // 阻塞1s
        tiem.Sleep(time.Second)
    }
}

// 主线程
func main() {
    go test(20)	
    test(10)
}

启动多个协程

package main 

import(
	"fmt"
    "time"
)

func main() {
    for i:=1; i<=5; i++ {
        go func(n int){
            fmt.Println(n)
        }(i)
    }
    
    time.Sleep(time.Second * 2)
}
// 最后输出乱序的 1 2 3 4 5

退出/等待协程 (sync包.WaitGroup类)

对应 sync 包里的 WaitGroup 类。需要使用三个方法:Add、Done、Wait

// 原型

// 计数器增加
// @param delta 可以是负数。向内部计数加上delta
//   - 当内部计数器等于0,Wait方法阻塞等待的所有线程都会释放
//   - 当内部计数器小于0,异常panic。
// @detail 调用顺序:
//   - 本方法一般在创建新线程或其他等待事件前调用
//   - 注意Add加正数的调用应该在Wait之前,否则Wait可能只会等待很少的线程。
func (wg *WaitGroup) Add(delta int)	

// 计数器减少
// @detail 减一WaitGroup计数器的值,应在线程的最后执行
func (wg *WaitGroup) Done()

// 阻塞等到WaitGroup计数器减为0
func (wg *WaitGroup) Wait()

案例

package main 
import(
	"fmt"
    "sync"
)

var wg sync.WaitGroup	// 只定义而无需赋值
func main() {
    for i:=1; i<=5; i++ {
        wg.Add(1)		// 或改写成在循环之前 wg.Add(5),适用于知道要启动多少个协程的情况
        go func(n int){
            fmt.Println(n)
            wg.Done()	// 或改写成在函数开始 defer wg.Done(),可以防止忘记
        }(i)
    }
    wg.Wait()	// 等待子协程结束 (计数器为0) 才通过 
}

协程共享数据、锁

冲突问题

package main 
import(
	"fmt"
    "sync"
)

var totalNum int			// 共享变量
var wg sync.WaitGroup		// 协程计数器

func add(){
    defer wg.Done()
    for i:=0; i<10000; i++{
        totalNum = i+1
    }
}

func sub(){
    defer wg.Done()
    for i:=0; i<10000; i++{
        totalNum = i-1
    }
}

func main() {
    wg.Add(2)
    go add()
    go sub()
    wg.wait()
    fmt.Println(totalNum)	// 这个输出的取值为 [-10000, +10000] 而非0。因为加减的底层是:拷贝到寄存器 -> 加法 -> 拷贝回内存
}

互斥锁 (sync包.Mutex类)

使用sync包的Mutex

package main 
import(
	"fmt"
    "sync"
)

var totalNum int			// 共享变量
var wg sync.WaitGroup		// 协程计数器
var lock sync.Mutex			// 互斥锁

func add(){
    defer wg.Done()
    for i:=0; i<10000; i++{
        lock.Lock()			// 加锁解锁
        totalNum = i+1
        lock.Unlock()
    }
}

func sub(){
    defer wg.Done()
    for i:=0; i<10000; i++{
        lock.Lock()			// 加锁解锁
        totalNum = i-1
        lock.Unlock()
    }
}

func main() {
    wg.Add(2)
    go add()
    go sub()
    wg.wait()
    fmt.Println(totalNum)	// 这个输出为0,成功,预期输出
}

读写锁 (sync包.RWMutex类)

读写锁性能比互斥锁高,经常用于读次数远远多于写次数的场景

读锁之间不冲突,读写同时发生可能有影响。

另外,写锁一般比读锁优先级比较高,避免写锁饥饿问题。

package main 
import(
	"fmt"
    "sync"
)

var totalNum int			// 共享变量
var wg sync.WaitGroup		// 协程计数器
var lock sync.RWMutex		// 读写锁

func read(){
    defer wg.Done()
    lock.RLock()			// 加锁解锁 (读)
    fmt.Println("开始读")
    time.Sleep(time.Second)
    fmt.Println("结束读")
    lock.RUnlock()
}

func write(){
    defer wg.Done()
    lock.Lock()				// 加锁解锁 (写)
    fmt.Println("开始写")
    time.Sleep(time.Second*2)
    fmt.Println("结束写")
    lock.Unlock()
}

func main() {
    wg.Add(6)
    for i:= 0; i<5; i++ {
        go read()
    }
    go write()
    wg.wait()
}

原理

主死从随

除了协程任务完成了或手动关闭后,协程会死亡外。

“主死从随”,意思是线程死亡时,他所创建的协程也会死亡(线程有其创建协程的所有权,自动生命周期)