GO并发详解

Posted 就想吃饱饭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GO并发详解相关的知识,希望对你有一定的参考价值。

并发和并行

并发:逻辑上具有处理多个任务的能力。一般并发的数量要小于CPU的数量,这些并发的任务通过间隔执行的方式来执行,这里一般是在单核CPU上面。

并行:物理上具备处理多个任务的能力。物理CPU的核数和并行的任务数相同,是并发的理想目标,理论上同一时刻内一个CPU执行一个任务。


计算机是如何实现并发的?

计算机的分时调用是并发的根本,CPU通过快速的切换作业来执行不同的作业,基本的调度单位在执行的时候可以被阻塞掉,此时就会将CPU资源让出来,等到该调度单位再次被唤醒的时候,又可以使用CPU资源,而操作系统保证了整个的调度过程。


线程,进程和协程

进程:是系统资源分配的最小单位,系统是由一个个进程组成的,包括文本区,数据区和堆栈区。进程的创建和销毁都比较消耗资源和时间。进程是抢占式的争夺CPU的资源,单核CPU在同一时刻只能有一个进程在执行。

线程:是CPU调度的最小单位,线程属于进程,它共享进程的整个内存空间。多线程是不安全的,进程内的一个线程崩溃会导致整个进程崩溃。


协程:协程是属于线程的,协程的程序是在线程里面跑的。协程没有线程的上下文切换,协程的切换是程序员自己控制的。协程是原子操作的,不存在代码执行一半,被终止的情况。


进程切换分两步

1.切换页目录以使用新的地址空间

2.切换内核栈和硬件上下文。


对于linux来说,线程和进程的最大区别就在于地址空间。

对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。


Goroutine

Go语言里的一种轻量级线程—协程。

  1. 相对线程,协程的优势就在于它非常轻量级,进行上下文切换的代价非常的小。
  2. 对于一个goroutine ,每个结构体G中有一个sched的属性就是用来保存它上下文的。这样,goroutine 就可以很轻易的来回切换。
  3. 由于其上下文切换在用户态下发生,根本不必进入内核态,所以速度很快。而且只有当前goroutine 的 PC, SP等少量信息需要保存。
  4. 在Go语言中,每一个并发的执行单元为一个goroutine。


Go 语言之goroutine的调度原理

​https://mp.weixin.qq.com/s/hTgIyJN7p-wrDfLj1bP1wQ​


golang 并发 && chan

channels 是 goroutines之间通信的工具, 可以理解为管道, 虽然go也提供共享变量的方式, 但是更加推荐使用channel

channel是有类型的,并且还有方向,可以是单向的,也可以是双向的,类似于unix中的pipe(管道)。

channel是通过<-和->来完成读写操作的

channel<-value(表示往channel写数据

<-channel表示从channel读数据

package main

import (
"fmt"
"runtime"
)

func GO(c chan bool, i int) {
a := 1
for i := 0; i < 100000; i++{
a += i
}
fmt.Println(i, a)
c <- true //向chan发送
}


func mian() {
// 调整并发的运行性能 最大效率地利用CPU
runtime.GOMAXPROCS(runtime.NumCPU())
c := make(chan bool, 10)
for i := 0; i<10; i++{
go GO(c, i)
}

for i := 0; i<10; i++{
<-c // 取数据
}
}


golang 并发 && Sync


Sync包同步提供基本的同步原语,如互斥锁。 除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。 通过Channel和沟通可以更好地完成更高级别的同步。并且此包中的值在使用过后不要拷贝。


waitgroup 用来等待一组goroutines的结束,在主Goroutine里声明,并且设置要等待的goroutine的个数,每个goroutine执行完成之后调用 Done,最后在主Goroutines 里Wait即可。

sync.WaitGroup提供了三个方法:

Add:添加或减少goroutine的数量。

Done:相当于Add(-1)。

Wait:阻塞住等待WaitGroup数量变成0.


package mian

import (
"fmt"
"runtime"
"sync"
)

/*
sync.WaitGroup提供了三个方法:
  Add:添加或减少goroutine的数量。
  Done:相当于Add(-1)。
  Wait:阻塞住等待WaitGroup数量变成0.
*/

func GO(wg *sync.WaitGroup, i int) {
a := 1
for i := 0; i < 100000; i++{
a += i
}
fmt.Println(i, a)
wg.Done()
}


func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i<10; i++{
go GO1(&wg, i)
}
wg.Wait()
}



golang 并发 && Select

select是Go语言中的一个控制语句,它有select,case, default共同构成

select 使用场景

协程之间的通讯采用的是channel。channel不管是有缓存的,还是无缓存的都会有阻塞的情况出现,只不过无缓存的阻塞会更加频繁。而协程长时间阻塞了之后,Go语言本身又没有提供这种超时的解决机制,所以开发者需要自己考虑实现这种超时机制。这种超时机制在Go语言中则是使用select来解决的。


使用select 向chan发送


package mian

import (
"fmt"
)
// 使用select 向chan发送
func mian() {
c := make(chan int)
go func() {
for v := range c{
fmt.Println(v)
}
}()

for {
select {
case c<-0: //随机发送
case c<-1:
}
}
}



使用select 取数 设置超时 或者 default


package mian

import (
"fmt"
"time"
)
// 使用select 取出
func mian() {
c, c1, c2 := make(chan bool, 2), make(chan int), make(chan string)
go func() {
for {
fmt.Println("FOR -------------------------------------")
select {
case v, ok := <-c1:
if !ok{
fmt.Println("c1 != ok")
c <- true
break
}
fmt.Println("c1",v)
case v, ok := <-c2:
if !ok{
fmt.Println("c2 != ok")
c <- true
break
}
fmt.Println("c2",v)
case <-time.After(3 * time.Second): //设置超时
c <- true
fmt.Println("超时")
//default: // 注释掉 有def时 chan没有数据 就一直 default, so就不走超时case
// fmt.Println("NO case ok")
// time.Sleep(time.Second * 3)
}
}
}()

c1 <- 1
c2 <- "hello"
c1 <- 2
c2 <- "word"

// 注释下面关闭chan操作时,会走超时的case
//close(c1)
//close(c2)

for i:[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础

[Go] 并发和并行的区别

GO并发详解

Go语言:sync包控制并发详解!

GO的并发之道-Goroutine调度原理&Channel详解

Go语言自学系列 | golang并发编程之原子操作详解