Go的并发

一、并发和并行

并发:

同一时间段内,多个任务再执行。(单个CPU,执行多个任务)

Go语言原生支持并发,Go使用Go协程(Goroutine)和信道(Channel)来处理并发

并行

同一时刻,多个任务在执行(多个CPU的支持)

go实现并发

package main

import (
   "fmt"
   "time"
)

// goroutine--->协程 ---2kb大小
// 线程:启动一个线程需要2-5m 也就是python开进程,其余的都是开线程
// go协程会复用线程,
// go协程间使用channel(信道),而不推崇用共享变量通信(锁,死锁)

//启动一个goroutine

func test() {
   fmt.Println("GO!")
}

func main() {
   fmt.Println("主线程开始执行!")
   go test()
   go test()
   go test()
   go test()
   go test()
   for i := 0; i < 10; i++ {
      go func() {
         fmt.Println("Go Go!---", i)
      }()
   }
   time.Sleep(1 * time.Second) // time 从纳秒开始  所以要选择规格
   // go语言中,主线程不会等待goroutine执行完成,要等待结束需要自己处理
   fmt.Println("主线程执行结束!")
}

// Go关键字开启goroutine,一个goroutine只占2kb左右

image-20210303110233257

Go的GMP模型

G(Goroutine)

Goroutine启动之后其实是放在全局的队列里,开多少个就放了多少个,然后通过P交给M执行。

M(当成操作系统真正的线程,实际上是用户线程)

既然M是线程那么他的作用肯定是执行我们的Goroutine(通过P),因为P后面携带一个或多个Goroutine,当线程执行Goroutine时发现有IO他就会自动切换到P后面的下一个Groutine执行,把阻塞的放在后面,如此反复。

用户线程,操作系统线程

python中,开的线程开出用户线程,用户线程跟操作系统线程1:1的对应关系,所有语言开线程都是通知操作系统开线程,而不是直接开,所以用户线程是在操作系统线程之上的。

某些语言用户线程和操作系统线程是N:1的关系

Go语言,用户线程和操作系统线程是N:M关系,但是我们不用管用户线程和操作系统线程,所以我们可以把他当为1:1关系

P(Processor)

现在版本默认情况是CPU核数,老版本是1,,负责和M做交互,将全区队列中的G取出来交给M执行,每个P携带的G的个数比较均匀

1b83ae69d3a70fc4f1ba7fc4292f6c1

操作系统调度器把操作系统的线程丢到CPU去执行,一个CPU执行多个系统线程,当其中一个线程出现了IO交出了时间片,下一个线程就会被替补上,根本没有休息的!

Channel(信道)

协程之间的通信,通过channel实现

定义Channel

package main

import (
	"fmt"
	"time"
)

func test1(a chan string) {
	fmt.Println("Go down!")
	time.Sleep(1 * time.Second)
	//  往信道放值  很像python中的队列
	a <- "111"
}

func main() {
	//1 定义channel
	// 可以定义为空接口
	var c chan string
	//2 信道的零值nil(引用类型,空值为nil吗,当做参数传递时,不需要取值,改就是原值,需要初始化)
	fmt.Println(c)
	//3 信道初始化
	sr := make(chan string,1)
	//4 信道的放值(注意放值和复制)
	sr<- "ssss" // 赋值会报错 c = "sss"
	//5 信道取值
	//<-c
	//6 取出来赋值给一个变量
	a := <-sr //这样类型推到  不用关注取出来的类型
	fmt.Println(a)
	// 默认情况下往信道中放值取值都是阻塞的


	//go test1(c)

}

此时的ch1通道可以称为缓冲通道,在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。定义方法如:ch:=make(chan type, value)。这个value表示缓冲容量,它的大小和类型无关,所以可以给一些通道设置不同的容量,只要它们拥有相同的元素类型。内置的cap函数可以返回缓冲区的容量,如果容量大于0,通道就是异步的了。缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是0或者未设置,通信仅在收发双方准备好的情况下才可以成功。

这种异步channel可以减少排队阻塞,在你的请求激增的时候表现得更好,更具伸缩性。

Channel的小栗子

package main

import (
   "fmt"
   "time"
)

// 程序中有一个数串,求出每一位的平方和立方和,并将它们加起来,打印

func calcSquares(number int, squareop chan int) {
   sum := 0 //总和
   for number != 0 {
      digit := number % 10 // 对10取余数 19999取余9
      sum += digit * digit
      number /= 10 // 除以10 num=1999
   }
   time.Sleep(2*time.Second)
   squareop <- sum

}

func calcCubes(number int, cubeop chan int) {
   sum := 0 //总和
   for number != 0 {
      digit := number % 10 // 对10取余数 19999取余9
      sum += digit * digit * digit
      number /= 10 // 除以10 num=1999
   }
   time.Sleep(1*time.Second)
   cubeop <- sum

}

func main() {
   ctime := time.Now().Unix() // 获取当前时间
   fmt.Println(ctime)
   number := 19999
   sqrch := make(chan int)
   cubech := make(chan int)
   go calcCubes(number, cubech)
   go calcSquares(number, sqrch)
   squares, cubes := <-sqrch, <-cubech
   fmt.Println("最终结果是:", squares+cubes)
   fmt.Println("花费时间是:", (time.Now().Unix() - ctime))
}

image-20210303170659011

信道的关闭,循环和死锁

信道的死锁现象,默认都是阻塞的,一旦有一个放,没有人取,或者有人取没有人放,就会出现死锁

package main

import "fmt"

func main() {
	var c = make(chan int)
	//1 不要强塞
	c <- 111

	//2  没值强取
	fmt.Println(<-c)
}

单向信道

package main

import "fmt"

func main() {
	//只写信道,goroutine中只能写,不能往外读,但是可以人为控制
	var c = make(chan<- int)
	// 只读信道
	var d = make(<-chan int)
	c <- 1
	c <- 2
	c <- 3
}

关闭信道(循环信道)

package main

import (
	"fmt"
	"time"
)

func producer(ch chan int) {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("Put",i)
        fmt.Println(len(ch))
	}
	close(ch)
}

func main() {
	ch := make(chan int)
	go producer(ch)
	//循环信道,最开始没有数据,就一直阻塞在这里,等到producer起来
	// 放一个取一个,放一个取一个,交叉着去执行
	// 他会自动检测信道,如果close了  那循环也结束了
	for v := range ch {
		time.Sleep(1*time.Second)  // 验证是否阻塞
		fmt.Println("Reciber", v)
	}
}

image-20210303180944088

缓冲信道

信道默认是阻塞的,缓冲信道意思就是,信道里默认缓冲一点东西,就不阻塞了,只有放满了,或者没有了,才阻塞

package main

import "fmt"

// 也就是设置queue的大小  满了做什么没满做什么

//

func main() {
   var c = make(chan int, 1) //无缓冲信道数字是0
   c <- 1
   //c <- 2 // 放满了  出现死锁
   // 死锁(一个goroutine会出现)
   a := <- c
   fmt.Println(a)
}

长度和容量

len长度:目前有多少

cap容量:最多可以放多少

func main() {
	var d  = make(chan int,6)
	d <-1
	d <-1
	d <-1
	d <-1
	fmt.Println(len(d))
	fmt.Println(cap(d))
}

image-20210304145557051

WaitGroup

等待所有的Goroutine执行完成

package main

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

// 也就是设置queue的大小  满了做什么没满做什么

//

func process(i int, wg *sync.WaitGroup) {
   fmt.Println("Started", i)
   time.Sleep(2 * time.Second)
   fmt.Println("Ended", i)
   wg.Done()
}

func main() {
   var wg sync.WaitGroup     // 没有初始化,值类型,当做参数传递,需要取地址
   fmt.Println(wg)
   for i := 0; i < 10; i++ {
      wg.Add(1) //启动一个goroutine add+1,里面有个计数器就down-1
      go process(i, &wg)
   }
   wg.Wait() // 一直阻塞在这里,直到10个done,计数器减到0
}

Select(比较有用)

select 语句用于在多个发送/接收信道操作中进行选择。select 语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。该语法与 switch 类似,所不同的是,这里的每个 case 语句都是信道操作。我们好好看一些代码来加深理解吧。

假设要去爬取百度,发出三次请求,由于网络原因不知道谁先回来,select就是谁先回来,就先用谁,类似于io多路复用

package main

// select 语句用于在多个发送/接收信道操作中进行选择。

import (
   "fmt"
   "time"
)


// 两个模拟去数据库拿数据,然后放到信道里
func server1(ch chan string) {
   time.Sleep(6 * time.Second)  // 模拟延时
   ch <- "from server1"
}
func server2(ch chan string) {
   time.Sleep(3 * time.Second)
   ch <- "from server2"

}
func main() {
   output1 := make(chan string)
   output2 := make(chan string)
   // 开启两个携程执行server
   go server1(output1)
   go server2(output2)
   // 类似于switch  判断谁先拿到值,然后执行相应语句
   select {
   case s1 := <-output1:  // 信道1取值
      fmt.Println(s1)
   case s2 := <-output2:  // 信道2取值
      fmt.Println(s2)
   }
}
----------------------------------------------------------------
from server2

就是挑一个最快的去执行

package main

import (  
    "fmt"
    "time"
)

func process(ch chan string) {  
    time.Sleep(10500 * time.Millisecond)
    ch <- "process successful"
}

func main() {
	output1 := make(chan string)
	output2 := make(chan string)
	// 开启两个携程执行server
	go server1(output1)
	go server2(output2)
	// 类似于switch  判断谁先拿到值,然后执行相应语句
	select {
	case s1 := <-output1:  // 信道1取值
		fmt.Println(s1)
	case s2 := <-output2:  // 信道2取值
		fmt.Println(s2)
		//当其他情况都不匹配时,将运行默认情况,就可以做其他事,模拟非阻塞io
	default:
		fmt.Println("Not ready")
	}
}
  1. 除 default 外,如果只有一个 case 语句评估通过,那么就执行这个case里的语句;
  2. 除 default 外,如果有多个 case 语句评估通过,那么通过伪随机的方式随机选一个;
  3. 如果 default 外的 case 语句都没有通过评估,那么执行 default 里的语句;
  4. 如果没有 default,那么 代码块会被阻塞,指导有一个 case 通过评估;否则一直阻塞

死锁与默认情况

package main

func main() {  
    ch := make(chan string)
    select {
    case <-ch:
    }
}

上面的程序中,我们在第 4 行创建了一个信道 ch。我们在 select 内部(第 6 行),试图读取信道 ch。由于没有 Go 协程向该信道写入数据,因此 select 语句会一直阻塞,导致死锁

Mutex

临界区

在学习 Mutex 之前,我们需要理解并发编程中临界区(Critical Section)的概念。当程序并发地运行时,多个 [Go 协程]不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区

x = x + 1

如果只有一个 Go 协程访问上面的代码段,那都没有任何问题。

但当有多个协程并发运行时,代码却会出错,让我们看看究竟是为什么吧。简单起见,假设在一行代码的前面,我们已经运行了两个 Go 协程。

在上一行代码的内部,系统执行程序时分为如下几个步骤(这里其实还有很多包括寄存器的技术细节,以及加法的工作原理等,但对于我们的系列教程,只需认为只有三个步骤就好了):

  1. 获得 x 的当前值
  2. 计算 x + 1
  3. 将步骤 2 计算得到的值赋值给 x

如果只有一个协程执行上面的三个步骤,不会有问题。

我们讨论一下当有两个并发的协程执行该代码时,会发生什么。下图描述了当两个协程并发地访问代码行 x = x + 1 时,可能出现的一种情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jl1HHHRU-1614922292346)(http://file.muyutech.com/error-img.png)]

我们假设 x 的初始值为 0。而协程 1 获取 x 的初始值,并计算 x + 1。而在协程 1 将计算值赋值给 x 之前,系统上下文切换到了协程 2。于是,协程 2 获取了 x 的初始值(依然为 0),并计算 x + 1。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 x,因此 x 等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 x,因此在所有协程执行完毕之后,x 都等于 1。

现在我们考虑另外一种可能发生的情况。

在上面的情形里,协程 1 开始执行,完成了三个步骤后结束,因此 x 的值等于 1。接着,开始执行协程 2。目前 x 的值等于 1。而当协程 2 执行完毕时,x 的值等于 2。

所以,从这两个例子你可以发现,根据上下文切换的不同情形,x 的最终值是 1 或者 2。这种不太理想的情况称为竞态条件(Race Condition),其程序的输出是由协程的执行顺序决定的。

在上例中,如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的

Mutex

Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。

共享变量–一定会涉及到并发安全问题

队列

package main
import (
	"fmt"
	"sync"
)
var x  = 0
func increment(wg *sync.WaitGroup) {
	x = x + 1
	wg.Done()
}
func main() {
	var w sync.WaitGroup
	for i := 0; i < 1000; i++ {
		w.Add(1)  // //启动一个goroutine add+1,里面有个计数器就down-1
		go increment(&w)
	}
	w.Wait()  // 一直阻塞在这里,直到1000个done,计数器减到0
	fmt.Println("final value of x", x)
}
-------------------------------------------------------------
final value of x 971

理想情况下应该是1000,但是由于并发安全问题,操作同一个数据,导致最后的结果不准确

所以我们需要给他加锁,定义在全局

使用 Mutex

在前面的程序里,我们创建了 1000 个 Go 协程。如果每个协程对 x 加 1,最终 x 期望的值应该是 1000。在本节,我们会在程序里使用 Mutex,修复竞态条件的问题。

通过mutex

package main
import (
   "fmt"
   "sync"
)
var x  = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
   m.Lock()
   x = x + 1
   m.Unlock()
   wg.Done()
}
func main() {
   var w sync.WaitGroup
   var m sync.Mutex
   for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, &m)
   }
   w.Wait()
   fmt.Println("final value of x", x)
}

通过信道实现

//package main
//import (
// "fmt"
// "sync"
//)
//var x  = 0
//func increment(wg *sync.WaitGroup, m *sync.Mutex) {
// m.Lock()
// x = x + 1
// m.Unlock()
// wg.Done()
//}
//func main() {
// var w sync.WaitGroup
// var m sync.Mutex
// for i := 0; i < 1000; i++ {
//    w.Add(1)
//    go increment(&w, &m)
// }
// w.Wait()
// fmt.Println("final value of x", x)
//}


package main
import (
   "fmt"
   "sync"
)
var x  = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
   ch <- true  // 一开始放一个true
   x = x + 1
   <- ch  // 执行完了把true取出来,如果没执行由于缓冲为1 就一直阻塞在这里
   wg.Done()
}
func main() {
   var w sync.WaitGroup // 等待协程完成
   ch := make(chan bool, 1)  // 定义了一个有缓冲大小唯一的信道
   for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, ch)
   }
   w.Wait()
   fmt.Println("final value of x", x)
}

利用的是缓冲信道放满了就会阻塞

Mutex vs 信道

共享内存更倾向于Mutex

传递数据倾向于信道,不涉及到数据操作,只涉及到通信

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值