golang 并发

文章目录

    • 1、并发和并行
    • 2、线程和协程的区别
    • 3、golang 并发实现
      • 基础知识
        • goroutine
        • channel 通道
        • defer关键字
      • go调度模型
      • 实现方式
        • syn包
        • channel
        • select 语句
        • GOMAXPROCS控制
      • 经典例子分析
        • case1
        • case2
        • case3
      • Semaphore信号量
      • 自旋锁

1、并发和并行

go语言协程依赖于线程,即使处理器运行的是统一线程,在线程内部go语言调度器也会切换多个协程执行,这个时候协程是并发的。如果多个协程被分配给了不同的线程,这些线程被不同的CPU核心处理,协程就是并行处理的。
故多核处理场景下,go语言的协程是并发

并发
golang 并发_第1张图片
并行
golang 并发_第2张图片

2、线程和协程的区别

协程是轻量级的线程

1、调度方式
协程是用户态的,协程和线程的对应关系是M:N。Go语言调度器可以将多个协程调度到同一个线程中,一个协程也可能切换到多个协程中执行
2、上线文切换速度
协程的速度要快于线程,是因为协程切换不用经过操作系统用户态和内核太的切换,并且协程切换的时候保留的寄存器要少于线程,线程切换大约需要1~2微秒,协程约为0.2微秒
3、调度策略
线程的调度大多数时间是抢占式的,操作系统调度器为了均衡每个线程的执行周期,会定时发出中断信号轻质执行线程上下文切换。而Go语言中的协程一般情况下是协作式调度,当一个协程处理完自己的任务后,可以主动将执行权限让给其他协程。这意味着协程可以更好的在规定时间内完成自己的工作,不会被轻易抢占。当一个协程运行了过长的时间,Go语言调度器才会抢占其执行
4、栈大小
线程的栈大小为2MB(避免栈一处),协程的栈默认为2KB。同时协程的栈在运行的时候是不能更改的,运行时会动态检测栈大小,动态扩容

3、golang 并发实现

goroutine 不是os线程、不是绿色线程(由语言运行时管理的线程),是协程。协程是一种非抢占式的简单并发子goroutine(函数、闭包或方法),也就是说,它们不能被中断。取而代之的是,协程有多个点,允许暂停或重新进入 —Go语言并发之道

  • goroutine是go的并发体
  • channelsgoroutine的通信机制(实际就是发送数据)

基础知识

goroutine

goroutine是一种轻量级的实现,可以在单个进程中执行成千上万的并发任务,是go语言并发设计的核心.
将关键字go放在一个函数的前面,这个函数执行时就会成为一个独立的并发线程,这线程就被称为goroutine

channel 通道

通道(chan)是一种特殊的类型,是一种引用类型。
主要分类有单向通道无缓冲的通道带缓冲的通道
主要作用是同步信号(shutdown/close/finish) 消息传递(queue/stream) 互斥(mutex)

//go中对chan的定义   /runtime/chan.go
type hchan struct {
	qcount   uint           // total data in the queue    数据总量
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32      //标记channel已经关闭
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters    和ring buffer相关
	sendq    waitq  // list of send waiters    和ring buffer相关

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex    //保护所有数据结构,但在某些quick path场景下不必加锁
}

type waitq struct {
	first *sudog
	last  *sudog     //sudog dequeue中等待的goroutine以及他的数据存储
}

底层原理参考

  • 任何时间只能有一个goroutine访问chan
  • chan 里面的数据满足FIFO规则
//声明通道
var 通道变量  chan  通道类型
//创建通道
通道实例  :=  make(chan 通道类型)
//给通道发送数据
通道变量  <-//读取通道数据
data, ok := <-通道变量
defer关键字

注意defer会对表达式进行提前求值

go调度模型

go调度模型

golang 并发_第3张图片
G:goroutine
M:系统线程 -> 执行代码的实体
P:Processor调度实体

实现方式

syn包
  • sync.WaitGroup 等待组讲解比较好的文章

进行多个任务的同步,主要操作有Add()、Done()、Wait()等操作

  • 互斥锁和读写锁

互斥锁 sync.Mutex:[互斥锁原理] (https://www.jb51.net/article/258684.htm)一个goroutine独占资源 互斥性、公平调度,饥饿处理
读写锁 sync.RWMutex:不可递归调用,读写互斥,多读之间并发

使用注意事项:

1、配套使用Lock、UnLock
2、运行时离开当前逻辑就释放锁
3、锁的粒度越小越好,加锁后尽快释放锁
4、没有特殊原因,尽量不要defer释放锁
5、RWMutex的读锁不要嵌套使用

  • List item
  • cond

sync.Cond 主要用于goroutine之间的协作,主要有三个函数Broadcast() , Signal(), Wait(), 一个成员变量,L Lock
Broadcast() :唤醒在本cond上等待的所有的goroutine
Signal():选择一个goroutine进行唤醒
Wait():让goroutine进行等待

  • once

sync.Once 中有一个Do()方法,无论是否更换Do()里面的东西,这个方法只会执行一次

func main() {
	var count int
	increment := func() {
		count++
	}

	var once sync.Once
	var increments sync.WaitGroup
	increments.Add(100)
	for i:=0;i<100;i++{
		go func() {
			defer increments.Done()
			once.Do(increment)
		}()
	}

	increments.Wait()
	fmt.Printf("count is %d\n", count)
}
//最终输出为 1

池(pool)是Pool模式的并发安全实现,需要参照pool设计模式

channel
select 语句
GOMAXPROCS控制
runtime.GOMAXPROCS(runtime.NumCPU())   //充分利用cpu资源

Go语言的并发通过goroutine`实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

经典例子分析

case1
func handler(){
	ch := make(chan string)
	go func() {
		time.Sleep(3*time.Second)
		ch <- "job result"
	}()

	select {
	case result := <- ch:
		fmt.Println(result)
	case <-time.After(time.Second):
		return
	}
}

上述代码会造成goroutine泄漏,原因在于channel没有缓存
可能造成goroutine泄漏的原因有:
1、channel没有缓存
2、select命中timeout逻辑
3、导致channel没有消费者
4、最终导致anonymous goroutine泄漏

case2

var mu sync.RWMutex
func main() {
	go A()
	time.Sleep(2*time.Second)

	fmt.Println("main call lock")
	mu.Lock()
	defer mu.Unlock()
}

func A()  {
	fmt.Println("A call rlock")
	mu.RLock()
	fmt.Println("A rlocked")
	defer mu.RUnlock()
	B()
}

func B(){
	time.Sleep(5*time.Second)
	C()
}

func C()  {
	fmt.Println("C call rlock")
	mu.RLock()
	fmt.Println("C rlocked")
	mu.RUnlock()
}

运行结果如下:
golang 并发_第4张图片

谨慎使用锁的递归调用,上面的还需再看一看

case3

go的map不可以并发

go内置map的几个要点:参考

  • hash算法:AES
  • 冲突解决:链地址法(和java类似) Python采用开放定址法
  • range go每次rangemap的时候顺序都会不同,因为go故意实现了random 【需要代码论据】
  • 装填因子 6.5
  • rehash 渐进式rehash (和redis类似)

Semaphore信号量

自旋锁

你可能感兴趣的:(Go,多线程,go语言,golang)