并发编程

42 阅读13分钟

进程、线程、协程

  • 进程:操作系统分配资源的最小单元。
  • 线程:CPU调度的最小单位(程序执行流的最小单元),是进程的一个执行单元
  • 协程:用户级线程,相比较线程而言,更轻量,调度由程序自身控制。

多进程和多线程存在的问题

线程切换消耗资源多。(php、python启动一个线程的调度工作给操作系统做,而java则给jvm去调度(jvm实际也是给操作系统做)。)

并发和并行

并发是任务的交替执行(如:时间片轮转进程调度算法),而并行是任务的同时执行。

Goroutine

go的协程,也叫轻量级线程,绿程。

main函数也是routine,只不过他是主routine

协程优点

1、内存占用小

2、切换快。

使用go关键字创建子routine

go func(){}
package main

import (
	"fmt"
	"time"
)

// 主协程
func main() {
	go func() {
		fmt.Println("子协程")
	}()
	fmt.Println("主协程")
    //主死随从,不等待一会,子协议还没打印程序就结束了
	time.Sleep(2 * time.Second) // 演出
}

/*
 * 主协程
 * 子协程
 */
package main

import (
	"fmt"
	"time"
)

// 主协程
func main() {
	// 匿名函数启动goroutine
	for i := 0; i < 100; i++ {
		go func(i int) {
			fmt.Println(i)
		}(i) // 闭包问题

	}
	time.Sleep(2 * time.Second)
}

/*
 * 结果:无序的输出1到100
 * 5
 * 15
 * 15
 * ...
 * 87
 * 91
 */

gmp

waitgroup

作用:等待一组协程执行完毕。在设定等待Goroutine数量执行完成前阻塞,等设定Goroutine数量执行完毕才会解除阻塞。

用法:在父协程调用Add方法来设定等待的协程数量。每个被等待协程应在结束时调用Done方法。同时,主协程里可以调用Wait方法阻塞至所有协程结束。

缺点:在实际应用中,一般是等待一个协程组,若流程正常,则等待所有协层结束。反之,如果其中一个协程异常,则会结束整个协程组停止运行释放资源。waitgroup是无法实现的这个功能的。(可以用channel实现)

初始化

var wg sync.WaitGroup   //var声明waitgroup不需要在额外进行显示初始化操作,go会初始化为零值状态
wait := sync.WaitGroup{} // 短变量声明

数据结构

type WaitGroup struct {
	noCopy noCopy
	state atomic.Uint64 // / 高 32 bit 是计数值(counter), 低 32 bit 是 waiter 的计数。
	sema  uint32
}

noCopy

一个特殊的结构体,表示该值不允许值复制,用go vet可以检查出该错误。

state

sema

信号量

三个内置方法

  • Add : 用于向 WaitGroup 中添加指定数量的等待的 goroutine,即修改state中的counter值
  • Done: 结束就是 Add(-1)
  • Wait:等待一组goroutine执行完毕,Wait会检查 counter 的值,如果不为 0,则当前的 goroutine 会被阻塞。当 counter 的值为 0 时,阻塞解除
package main

import (
	"fmt"
	"sync"
)

// 主协程
func main() {
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println(i)
		}(i) // 闭包问题

	}
	//等到监控的子协层结束
	wg.Wait()
	fmt.Println("执行完成")
}

/*
 * 结果:无序的输出1到100
 * 5
 * 15
 * 15
 * ...
 * 87
 * 91
 */

并发会存在一个竞争问题,锁是用来解决这个问题的。

锁的本质就是将并发的代码进行串行化了,所以使用锁肯定会影响性能,所以设计锁的时候尽量保持并行。

tips: 锁复制了就失去锁的作用的。所以在使用时不能进行复制,不过复制了也不会报错。

互斥锁

互斥锁(Mutex)是一种用于多线程编程的同步机制。

例子: 非原子化操作。

package main

import (
	"fmt"
	"strconv"
	"sync"
)

var wg sync.WaitGroup
var total int

func add() {
	defer wg.Done()
	total += 1
}

func sub() {
	defer wg.Done()
	total -= 1
}

// 主协程
func main() {
	for i := 0; i < 10000; i++ {
		wg.Add(2)
		go add()
		go sub()
		wg.Wait()
		fmt.Println("第" + strconv.Itoa(i) + "次,值为" + strconv.Itoa(total))
	}
}

/*
 * 结果:并不是都是0
 * 第0次,值为0
 * ...
 * 第7634次,值为1
 * ...
 * 第9978次,值为2
 * ...
 */

原因:

结果并不一样等于0,出现这个情况的原因是total += 1并不是原子化操作

total += 1分为三步,

  1. 读取total ,
  2. 计算total + 1,
  3. 计算结果写入到a。

total -= 1也是同理。

而这六步会存在穿插执行,所以导致结果不一样。

解决方法就是将total += 1变为原子化。

两个方法

1、使用互斥锁Mutex

2、使用atomic包

package main

import (
	"fmt"
	"strconv"
	"sync"
)

var wg sync.WaitGroup
var total int
var lock sync.Mutex

func add() {
	lock.Lock()
	total += 1
	lock.Unlock()
    wg.Done()
}

func sub() {
	lock.Lock()
	total -= 1
	lock.Unlock()
    wg.Done()
}

// 主协程
func main() {
	for i := 0; i < 100000; i++ {
		wg.Add(2)
		go add()
		go sub()
		wg.Wait()
		fmt.Println("第" + strconv.Itoa(i) + "次,值为" + strconv.Itoa(total))
	}
}

/*
 * 结果:都是0
 * 第0次,值为0
 * ...
 * 第99999次,值为0
 * ...
 */
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var wg sync.WaitGroup
var total int32
var lock sync.Mutex

func add() {
	atomic.AddInt32(&total, 1)
	wg.Done()
}

func sub() {
	atomic.AddInt32(&total, -1)
	wg.Done()
}

// 主协程
func main() {
	for i := 0; i < 100000; i++ {
		wg.Add(2)
		go add()
		go sub()
		wg.Wait()
		fmt.Printf("第%d次,值为%d\r\n", i, total)
	}
}

/*
 * 结果:都是0
 * 第0次,值为0
 * ...
 * 第99999次,值为0
 * ...
 */

读写锁

读锁:允许读锁的goroutine进行访问,但不允许写锁定的gorouting进行操作,会阻塞到所有的读锁定的goroutine执行完毕才能进行操作

写锁: 当一个goruntine获取写锁定时候,其他goroutine都会被阻塞,直到写锁被释放。

为什么需要读写锁:

在web场景中,读数据远多于写数据。在多个gorouting的时候,存在三种情况。

  1. 读与读,需要并发
  2. 读与写、写与写, 需要互斥
  3. 写的过程不可读。(例如修改商品价格的情况。)
package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var total int
var rwlock sync.RWMutex

func add() {
	rwlock.Lock() // 加写锁,防止别的写、读执行。
	total += 1
	fmt.Printf("写入%d\r\n", total)
	time.Sleep(3 * time.Second)
	rwlock.Unlock()
	wg.Done()
}

func sub() {
	rwlock.RLock() /// 加读锁,不会阻止其他协程的读
	time.Sleep(1000 * time.Millisecond)
	fmt.Printf("读取%d\r\n", total)
	rwlock.RUnlock()
	wg.Done()
}

// 主协程
func main() {
	wg.Add(2)
	go add()
	for i := 0; i < 100000; i++ {
		wg.Add(1)
		go sub()
	}
	go add()
	wg.Wait()
	fmt.Println("结束了")
}

// 结果遇到写的过程中阻塞3秒。在打印出读的操作
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go add()
	}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go sub()
	}
	wg.Wait()
	fmt.Println("结束了")
}

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var total int
var rwlock sync.RWMutex

func add() {
	rwlock.Lock() // 加写锁,防止别的写、读执行。
	total += 1
	fmt.Printf("写入%d\r\n", total)
	time.Sleep(3 * time.Second)
	rwlock.Unlock()
	wg.Done()
}

func sub() {
	rwlock.RLock() /// 加读锁,不会阻止其他协程的读
	time.Sleep(1000 * time.Millisecond)
	fmt.Printf("读取%d\r\n", total)
	rwlock.RUnlock()
	wg.Done()
}

// 主协程
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go add()
	}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go sub()
	}
	wg.Wait()
	fmt.Println("结束了")
}

/*
 * 结果:
 * 读取0
 * 写入1
 * 读取1
 * 读取1
 * 读取1
 * 读取1
 * 读取1
 * 读取1
 * 读取1
 * 读取1
 * 读取1
 * 写入2
 * 写入3
 * 写入4
 * 写入5
 * 写入6
 * 写入7
 * 写入8
 * 写入9
 * 写入10
 * 结束了
 */

channel

什么是channel?

channel 是用来做 goroutine 通信使用的,是goroutine 之间的通信机制。

Go 的并发哲学是:“不要通过共享内存通信;而是通过通信共享内存。”

声明channel

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan []int // 声明一个传递int切片的通道

tips:声明channel后必须要进行初始化才能使用,不然会发生阻塞。

声明并初始化channel

创建语法:make(chan [type], int)

ch1 := make(chan string) // 创建无缓冲channel,并指明channel中的数据为string,双端等待
ch2 = make(chan [type], Int)//  创建有缓冲channel,Int为最大缓存容量。当发送消息达到int条数会阻塞,Int为0时,即无缓冲

发送

ch1 <- "xxxx"

接收

 x, ok  := <- ch1  //从ch1中获取值。 ok表示是否是未关闭channel获取的,false是通道已关闭获取的。

无缓冲channel和有缓冲channel

无缓冲channel

只有存在接收操作时,才会接受发送操作,不然每次发送数据时,程序都会被阻塞。适用于通讯,B要立刻知道A是否已经完成

package main

import (
	"fmt"
	"time"
)

func main() {
	msg := make(chan string, 0) //无缓存channel
	go func(msg chan string) {  // happen-before的机制
		data := <-msg // 取值。 将msg的值取出复制给data
		fmt.Println(data)
	}(msg)
	msg <- "存放值"
	time.Sleep(3 * time.Second)
}

有缓冲channe

就是将发送和接收操作解耦。 缓冲区满前不会阻塞程序,适用于消费者和生产者之间的通信。

package main
import "fmt"
func main() {
	msg := make(chan string, 1) // 如果channel的空间设置为0的话,放值进去会导致阻塞,死锁。
	msg <- "存放值"                //  将值存进msg
	data := <-msg               // 取值。 将msg的值取出复制给data
	fmt.Println(data)
}

range在channel的应用

for range会一直读取channel的数据,直到channel关闭。

close(channel)

go的一个内置函数,用于关闭channel,参数

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	msg := make(chan int, 2)

	wg.Add(1)
	go func(msg chan int) {
		defer wg.Done()
		for data := range msg {
			fmt.Println(data)
		}
	}(msg)
	msg <- 1
	msg <- 2
	close(msg) // 关闭通道, 关闭后只能读,不能写入

	wg.Wait()
}

单向channel和双向channel

默认情况下,channel是双向channel.

var ch2 chan <- float64 //单向channel, ch2只能写入float64数据
var ch3 <- chan int //单项的channel, ch3只能读取

tip: 单项不能变双向


func main() {
	c := make(chan int, 3)

	var send chan<- int = c //send-only
	var read <-chan int = c //recv-only

	send <- 222
	num := <-read
	fmt.Println(num) // 222
}
package main

import (
	"fmt"
	"time"
)

func producer(out chan<- int) {
	for i := 0; i < 10; i++ {
		out <- i * i
	}
	close(out)
}

func consumer(in <-chan int) {
	for num := range in {
		fmt.Printf("num=%d\r\n", num)
	}
}

func main() {
	c := make(chan int)
	go producer(c)
	go consumer(c)
	time.Sleep(3 * time.Second)
}

/*
 *num=0
 *num=1
 *num=4
 *num=9
 *num=16
 *num=25
 *num=36
 *num=49
 *num=64
 *num=81
 */

多路复用

select关键字

与switch很相似,用于监控多个channel,解决有多个goroutine都在执行时,当某个goroutine执行完毕后需要立刻知道。

特点

  1. select会执行第一个已就绪的channel,如果都没有就绪,则有default流程走default,然后退出select,没有default则一直阻塞到有channel就绪或panic。
  2. 如果有多个case已就绪,则执行随机的case。(为了防止饥饿)
  3. 空的select语句将被阻塞,直至panic;
tips:
  • case后面不一定是读channel,也可以是写channel,只要是对channel的操作就可以;
  • 如果某个分支channel是关闭的,如果是接受语句视为就绪状态,如果是发送则会报错(send on closed channel)。

解决难点场景

package main

import (
	"fmt"
	"time"
)

var done = make(chan string)

func g1() {
	done <- "g1准备就绪!"
}

func g2() {
	time.Sleep(1 * time.Second)
	done <- "g2准备就绪!"
}

func main() {
	go g1()
	go g2()
	ready := <-done
	fmt.Println(ready)
}

/*
 * g1准备就绪!
 */

如果只有单个cannel的时候,可以很简单这样解决,但如果多个channel的话就非常麻烦,select就是解决这个场景的。

package main

import (
	"fmt"
)

func g1(ch chan string) {
	ch <- "g1准备就绪!"
}

func g2(ch chan string) {
	ch <- "g2准备就绪!"
}

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	go g1(ch1)
	go g2(ch2)

	for i := 0; i < 2; i++ {
		select {
		case g1 := <-ch1:
			fmt.Println(g1)
		case g2 := <-ch2:
			fmt.Println(g2)
		}
	}
}

// 随机出现以下结果
/*
 * 结果1
 * g1准备就绪!
 * g2准备就绪!
 */

/**
 * 结果2
 * g1准备就绪!
 * g2s准备就绪
 */

常用场景-超时

package main

import (
	"fmt"
	"time"
)

func g1(ch chan string) {
	ch <- "g1准备就绪!"
}

func g2(ch chan string) {
	time.Sleep(1 * time.Second)
	ch <- "g2准备就绪!"
}

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	go g1(ch1)
	go g2(ch2)
	timer := time.NewTimer(5 * time.Second)
	for {
		select {
		case g1 := <-ch1:
			fmt.Println(g1)
		case g2 := <-ch2:
			fmt.Println(g2)
		case <-timer.C:
			fmt.Println("超时")
			return
		}
	}
}

/*
 * g1准备处理!
 * g2准备处理!
 * 超时
 */

go中channel的应用场景:

1、消息传递

2、信息广播

3、事件订阅和广播

4、任务分发

5、结果汇总

6、并发控制

7、同步和异步

...

编程题

使用两个goroutine交替打印序列,一个goroutine打印数字,另外一个grountine打印字母,最终效果:

12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

package main

import (
	"fmt"
	"time"
)

var number, letter = make(chan bool), make(chan bool)

func printNumber() {
	i := 1
	for {
		<-number
		fmt.Printf("%d%d", i, i+1)
		i += 2
		letter <- true
	}
}

func printLetter() {
	var str = "ABCDEFGHIJKLMNOPQRSTUVWSYZ"
	i := 0

	for {
		<-letter
		if i >= 26 {
			return
		}
		fmt.Print(str[i : i+2])
		i += 2
		number <- true
	}
}

func main() {
	go printNumber()
	go printLetter()
	number <- true
	time.Sleep(100 * time.Second)
}
//12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WS2526YZ2728

context

作用:

一个用于在程序之间传递上下文信息。用来解决链式通讯共享内存,达到自上而下的通知效果。

tips: channel+ select方案在嵌套的时候,比如一个协程衍生多个协程,然后要同时关闭就比较麻烦。

主要两个功能:

1、携带键值对。

2、管理取消信号,分为主动取消和超时取消

参考资料:www.zhihu.com/tardis/zm/a…

创建上下文

context.Background()

返回一个非nil的空Context,一般是main函数中创建的顶级上下文

context.TODO()

返回一个非nil的空Context,一般是用在不知道用哪个上下文的时候,用来占位。

Background和TODO的区别

两个返回都是空Context,仅仅是用于语义化区分。一个是作为顶级上下文,一个用于占位。

context结构体

type Context interface {
		Deadline() (deadline time.Time, ok bool) //返回context的截止时间
		Done() <-chan struct{}  // 取消信号,一个只读channel
		Err() error  // 返回 context 取消原因
		Value(key any) any // 获取 key 对应的 value
	}

创建派生 Context(子Context)

用这些方法创建出来是一个树形结构。如果父级的context关闭了,子context也会随之关闭。

WithCancel

用于主动取消,返回一个子上下文,以及一个通知取消的信号函数

ctx, cancel := context.WithCancel(context.Background())

WithDeadline

用于在某个时间自动取消

deadline := time.Now().Add(5 * time.Second)
deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)

WithTimeout

用于超时后自动取消

timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)

原理

就是调用WithDeadline,用当前时间加上超时的时间作为第二个参数。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

WithValue

用于设置当前上下文携带的键值对

parentCtx := context.WithValue(context.Background(), "userID", 123)

tip:如果需要传递多个参数,传递结构体做为值。

官方的使用建议

  1. Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
  2. Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
  3. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
  4. The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
  1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。

  2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。

  3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。

  4. 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。

简单使用例子:

6秒后主动结束监控

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func cpuInfo(ctx context.Context) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println("退出cpu监控")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("cpu信息")
		}
	}
}
func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background())
	go cpuInfo(ctx)
	time.Sleep(6 * time.Second)
	cancel() // 结束
	wg.Wait()
	fmt.Println("监控完成")
}

/**
 * cpu信息
 * cpu信息
 * cpu信息
 * 退出cpu监控
 * 监控完成
 */

6秒后超时自动结束并传递userID

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func cpuInfo(ctx context.Context) {
	defer wg.Done()
	fmt.Println(ctx.Value("userID"))
	for {
		select {
		case <-ctx.Done():
			fmt.Println("退出cpu监控")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("cpu信息")
		}
	}
}
func main() {
	wg.Add(1)
	ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)
	parentCtx := context.WithValue(ctx, "userID", 123)
	go cpuInfo(parentCtx)
	wg.Wait()
	fmt.Println("监控完成")
}

/**
 * 123
 * cpu信息
 * cpu信息
 * cpu信息
 * 退出cpu监控
 * 监控完成
 */