Go中的并发编程和goroutine

并发编程对于任何语言来讲都不是一件简单的事情。Go在设计之初主打高并发,为使用者提供了goroutine,使用的方式虽然简单,可是用好却不是那么容易,咱们一块儿来学习Go中的并发编程。web

1. 并行和并发

并行(parallel): 指在同一时刻,有多条指令在多个处理器上同时执行。编程

并发(concurrency): 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具备多个进程同时执行的效果,但在微观上并非同时执行的,只是把时间分红若干段,经过cpu时间片轮转使多个进程快速交替的执行。数组

2. 进程和线程

进程: 咱们在操做系统中的每一次操做至关于触发了一个进程,打开一个浏览器,点开任务管理器,等等。浏览器

线程: 轻量级的进程,本质还是进程 。独立地址空间,拥有PCB,最小分配资源单位,可当作是只有一个线程的进程;而线程是程序的最小的执行单位,有独立的PCB,线程拥有本身的栈空间,但没有独立的地址空间 ,共享当前进程的地址空间。缓存

对操做系统来讲,线程是最小的执行单元,进程是最小的资源管理单元。安全

不管进程仍是线程,都是由操做系统所管理的。多线程

3.协程

协程: coroutine,协同式程序。协程不是轻量级的线程, 协程与线程的关系并不像是线程与进程的关系。并发

先了解一些概念:异步

CPU切换

在每一个任务运行前, CPU 都须要知道任务从哪里加载,又从哪里开始运行。也就是说,须要系统事先给他设置好 CPU 寄存器和程序计数器(Program Counter,PC)svg

  • CPU 寄存器:是 CPU 内置的容量小、但速度极快的内存

  • 程序计数器:是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置

它们都是 CPU 在运行任何任务前,必须依赖的环境,所以也被叫作 CPU 上下文。

上下文切换:就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,而后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

根据任务的不一样,又分为进程上下文切换线程上下文切换中断上下文切换

进程的上下文切换

进程的运行态:

Linux 按照特权等级,把进程的运行空间分为内核空间用户空间 。在这两种空间中运行的进程状态分别称为内核态用户态

  • 内核空间(Ring 0):具备最高权限,能够直接访问全部资源
  • 用户空间(Ring 3):只能访问受限资源,不能直接访问内存等硬件设备,必须经过系统调用进入到内核中,才能访问这些特权资源

系统调用:

从用户态到内核态的转变,须要经过系统调用来完成。好比查看文件时,须要执行屡次系统调用:open、read、write、close等。系统调用的过程以下:

  1. 把 CPU 寄存器里原来用户态的指令位置保存起来;
  2. 为了执行内核代码,CPU 寄存器须要更新为内核态指令的新位置,最后跳转到内核态运行内核任务;
  3. 系统调用结束后,CPU 寄存器须要恢复原来保存的用户态,而后再切换到用户空间,继续运行进程;

因此,一次系统调用的过程,实际上是发生了两次 CPU 上下文切换。

什么是进程上下文切换:

  • 进程执行终止,它以前顺颂的CPU就会被释放出来,这时就从就绪队列中取出下一个等待时间片的进程;
  • 当某个进程的时间片耗尽,它就会被系统挂起,切换到其余等待CPU的进程运行;
  • 某个进程由于须要的系统资源比较大(好比内存不足),这时候该进程会被挂起,系统会调度其余进程执行;
  • 当有优先级更高的进程(系统操做进程)须要时间片,为了保证优先级更高的进程可以执行,当前进程会被挂起;
  • 若是当前进程中有sleep函数,他也会被挂起;

进程上下文切换和系统调用的区别:

  • 进程上下文切换:进程之间的切换,从一个进程切换到另外一个;进程是由内核来管理和调度,进程的切换只发生在内核态;进程上下文切换过程当中,须要将该进程的虚拟内存、栈、全局变量保存起来,以供下次使用;
  • 系统调用:是在一个进程中的进程状态切换;切换过程当中无需改变进程的虚拟内存,栈,全局变量等相关信息,从内核态到用户态至关于新开辟了一块虚拟内存;
线程的上下文切换

线程是调度的基本单位,而进程则是资源拥有的基本单位。 当进程只有一个线程时,能够认为进程就等于线程。 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不须要修改的。线程也有本身的私有数据,好比栈和寄存器等,这些在上下文切换时也是须要保存的。

线程上下文切换有两种状况:

  1. 先后两个线程属于不一样进程,由于资源不共享,因此切换过程就跟进程上下文切换是同样的;
  2. 先后两个线程属于同一个进程,由于虚拟内存是共享的,因此在切换时,虚拟内存这些资源就保持不动,只须要切换线程的私有数据、寄存器等不共享的数据。
中断上下文切换

中断处理会打断进程的正常调度和执行。在打断其余进程时,须要将进程当前的状态保存下来,中断结束后,进程仍然能够从原来的状态恢复运行。

中断上下文切换并不涉及到进程的用户态。因此,即使中断过程打断了一个正处在用户态的进程,也不须要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必须的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

在有线程的前提下,提出来协程,它到底解决了什么问题呢?

咱们知道线程的出现是为了减少进程的切换开销,提升多核的利用率。当程序运行到某个IO发送阻塞的时候,能够切换到其余线程去执行,这样不会浪费CPU时间。而线程的切换彻底是经过操做系统去完成的,切换的时候通常会经过系统从用户态切换到内核态。这段话的重点是,线程是内核态的。

咱们常见的代码逻辑都是被封装在一个个函数块里面。每次传递一个参数,这个函数就会从头至尾执行一遍,有对应的输出。若是在执行的过程当中,发生了线程的抢占切换,那么当前线程就会保存函数当前的上下文信息(放到寄存器里面),去执行其余线程的逻辑。当这个线程从新执行时会根据以前保存的上下文信息继续执行。这段话的重点是,线程的切换须要保存函数的上下文信息。

并且现代操做系统通常都是抢占式的,因此多个线程在执行的时候在何时切换咱们是没法控制的。因此,多线程编程时为了保证数据的准确性与安全性,咱们常常须要加锁。这段话的重点是,线程的执行顺序咱们没法控制,何时切换咱们也几乎没法控制。

因为线程在运行时常常会因为IO阻塞(或者时钟阻塞)而放弃CPU,会致使咱们的逻辑不能流畅的执行下去。因此,咱们通常采用异步+回调的方式去执行代码。当线程与到阻塞时直接返回,继续执行下面的逻辑。同时注册一个回调函数,当内核数据准备好了以后再通知咱们。这种写代码的方式其实不够直观,由于咱们通常都习惯顺序执行的逻辑,一段代码能从头跑到尾那是再理想不过了。这段话的重点是,涉及到IO阻塞的多线程编程时,咱们通常用异步+回调的方式来解决问题。

协程是用户态的,他是包含在线程里面的,简答来讲你能够认为一个线程能够按照你的规则把本身的时间片分给多个协程去执行。

由于一个线程里面可能有多个协程,因此协程的执行须要切换,切换就须要保存当前的上下文信息(一组寄存器和调用堆栈,保存在自身的用户空间内),这样才能在再次执行的时候继续前面的工做。相比线程,协程要保存的东西都不多。

相比线程,协程的切换时机是能够控制的。咱们能够告诉协程,代码执行到哪句的时候切换到哪一个协程,这样就能够避免线程执行不肯定性带来的安全问题,避免了各类锁机制带来的相关问题。

协程的代码看起来是同步的,不须要回调。好比说有两个协程,A协程执行到第3句就必定会切换到B协程的第4句,假如A与B里面都有循环,那展开来看其实就是A与B函数不断的顺序执行。这种感受有点像并发,一样在一个线程上的A与B不断的切换去执行逻辑。

协程不过是一种用户级别的实现手段,他并不像线程那样有明确的概念与实体,更像是一个语言技巧。他的切换开销很小。

协程拥有本身的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其余地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。所以:协程能保留上一次调用时的状态(即全部局部状态的一个特定组合),每次过程重入时,就至关于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。在并发编程中,协程与线程相似,每一个协程表示一个执行单元,有本身的本地数据,与其它协程共享全局数据和其它资源。目前主流语言基本上都选择了多线程做为并发设施,与线程相关的概念是抢占式多任务(Preemptive multitasking),而与协程相关的是协做式多任务

无论是进程仍是线程,每次阻塞、切换都须要陷入系统调用(system call),先让CPU跑操做系统的调度程序,而后再由调度程序决定该跑哪个进程(线程)。并且因为抢占式调度执行顺序没法肯定的特色,使用线程时须要很是当心地处理同步问题,而协程彻底不存在这个问题(事件驱动和异步程序也有一样的优势)。

协做式的任务,是要用户本身来负责任务的让出的。若是一个任务不主动让出,其余任务就不会获得调度。这是协程的一个弱点,可是好好的规划,这实际上是一个能够变得很强大的优势。

总结一下上面的重点:

1.多线程处理,叫作抢占式多任务处理;多协程处理,叫作协做式多任务处理。

2.历史上是先有协程,可是由于它是非抢占式的,致使多任务时间片不能公平分享,因此后来所有废弃了协程改为抢占式的线程。

3.协程是用户态的,是包含在一个线程里面的多个执行单元。意味他是单线程处理的过程。(好比你的main函数比协程修饰的函数先中止,那么协程是没有执行完的)协程都没有参与多核CPU的并行处理。而线程是在多核 CPU上是受操做系统调度并行执行的。

4.因为协程能够在用户空间内切换上下文,再也不须要陷入内核来作线程切换,避免了大量的用户空间和内核空间之间的数据拷贝,下降了CPU的消耗,从而避免了追求高并发时CPU早早到达瓶颈的窘境 。

5.协程本质仍是单线程下处理多任务,单线程的瓶颈也是协程的瓶颈。我以为协程最大的意义就是能够用同步方式编写异步代码 。

4. Go并发

goroutine是Go并行设计的核心。 通常会使用goroutine来处理并发任务 。goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心。它是处于异步方式运行,你不须要等它运行完成之后在执行之后的代码。

Goroutine是创建在线程之上的轻量级的抽象。它容许咱们以很是低的代价在同一个地址空间中并行地执行多个函数或者方法。相比于线程,它的建立和销毁的代价要小不少,而且它的调度是独立于线程的。在Go中建立一个goroutine很是简单,使用“go”关键字便可:

go hello(str)

先来看一个简单的例子:

package main

import (
	"time"
)

func Print() {
	for i := 1; i <= 5; i++ {
		time.Sleep(100 * time.Millisecond)
		println(i)
	}
}

func HelloWorld() {
	println("Hello world")
}

func main() {
	go Print()    // 开启第一个goroutine
	go HelloWorld()    // 开启第二个goroutine
	time.Sleep(2*time.Second)
	println("end")
}

打印:
Hello world
1
2
3
4
5
end
4.1 Go中的CountDownLatch

CountDownLatch是Java中的一个同步辅助类,在完成一组正在其余线程中执行的操做以前,它容许一个或多个线程一直等待。

在Go中可使用sync包中的WaitGroup来实现同样的功能,WaitGroup 等待一组goroutinue执行完毕,主程序调用 Add 添加等待的goroutinue数量,每一个goroutinue在执行结束时调用 Done ,此时等待队列数量减1,主程序经过Wait阻塞,直到等待队列为0。

package main

import (
	"fmt"
	"sync"
)

func cal(a int , b int ,n *sync.WaitGroup)  {
	c := a+b
	fmt.Printf("%d + %d = %d\n",a,b,c)
	defer n.Done() //goroutinue完成后, WaitGroup的计数-1

}

func main() {
	var go_sync sync.WaitGroup //声明一个WaitGroup变量
	for i :=0 ; i<10 ;i++{
		go_sync.Add(1) // WaitGroup的计数加1
		go cal(i,i+1,&go_sync)
	}
	go_sync.Wait()  //等待全部goroutine执行完毕
	println("主程序执行完毕")
}

结果:
0 + 1 = 1
1 + 2 = 3
9 + 10 = 19
3 + 4 = 7
4 + 5 = 9
2 + 3 = 5
5 + 6 = 11
6 + 7 = 13
7 + 8 = 15
8 + 9 = 17
主程序执行完毕
4.2 goroutine之间的通信–channel

channel用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通信简单高效,同时也线程安全,多个goroutine可同时修改一个channel,不须要加锁。

channel可分为三种类型:

  • 只读channel:只能读channel里面数据,不可写入
  • 只写channel:只能写数据,不可读
  • 通常channel:可读可写

注意,必须使用make 建立channel:

c1 := make(chan int)
c2 := make(chan string)

channel经过操做符<-来接收和发送数据 :

c1 <- str //发送数据str到c1
newStr := <- c1 //从str中接受数据并赋值给newStr

默认的,信道的存消息和取消息都是阻塞的 , 叫作无缓冲的信道。也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另外一端已经准备好。

那么有缓存的channel是指在声明的时候指定该channel缓存的容量:

ch := make(chan int, 10)

有缓存的 channel 相似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操做将被阻塞,直到有其余 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操做会形成阻塞,直到有 goroutine 向 channel 中写入消息。

ch := make(chan int, 3)

// 读消息阻塞,由于channel为空
<- ch

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3

// 存入消息阻塞,channel已满,未被读取
ch <- 4

channel的使用:

1.使用channel阻塞主线程,直到子goroutine完成才继续往下走。

c := make(chan int)
go func(){
    //do something
    c <- 1
}()
doAnnotherThing()
<- c

2.消息传递

func test(){
	intChan := make(chan int)

	go func() {
		intChan <- 1
	}()

	value := <- intChan
	fmt.Println("value : ", value)
}

匿名函数中的操做产生一个值,将该值传递到主函数中去。

3.合并多个channel的输出

package main
   
   import (
   	"fmt"
   	"time"
   )
   
   
   func testMergeInput() {
   	input1 := make(chan int)
   	input2 := make(chan int)
   	output := make(chan int)
   
   	//将 channel 1 和 2 中的数据输出到output中
   	go func(in1, in2 <-chan int, out chan<- int) {
   		for {
   			select {
   			case v := <-in1:
   				out <- v
   			case v := <-in2:
   				out <- v
   			}
   		}
   	}(input1, input2, output)
   
   	go func() {
   		for i := 0; i < 10; i++ {
   			input1 <- i
   			time.Sleep(time.Millisecond * 100)
   		}
   	}()
   
   	go func() {
   		for i := 20; i < 30; i++ {
   			input2 <- i
   			time.Sleep(time.Millisecond * 100)
   		}
   	}()
   
   	go func() {
   		for {
   			select {
   			case value := <-output:
   				fmt.Println("输出:", value)
   			}
   		}
   	}()
   
   	time.Sleep(time.Second * 5)
   	fmt.Println("主线程退出")
   }
   
   
   
   func main(){
   	testMergeInput()
   }

4.模拟生产者和消费者模式

package main

import (
	"fmt"
	"math/rand"
	"time"
)

var(
	lockChan = make(chan int, 1)
	remainMoney = 1000
)

func testSynchronize()  {
	quit := make(chan bool, 2)

	go func() {
		for i:=0; i<10;i++{
			money := (rand.Intn(12) + 1) * 100
			go testSynchronize_expense(money)
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
		}
		quit <- true
	}()

	go func() {
		for i:=0; i<10; i++{
			money := (rand.Intn(12) + 1) * 100
			go testSynchronize_gain(money)
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
		}
		quit <- true
	}()
	<- quit
	<- quit
	fmt.Println("主程序退出")
}

func testSynchronize_expense(money int)  {
	lockChan <- 0
	if(remainMoney >= money){
		srcRemainMoney := remainMoney
		remainMoney -= money
		fmt.Printf("原来有%d, 花了%d,剩余%d\n", srcRemainMoney, money, remainMoney)
	}else{
		fmt.Printf("想消费%d钱不够了, 只剩%d\n", money, remainMoney)
	}

	<- lockChan
}

func testSynchronize_gain(money int)  {
	lockChan <- 0
	srcRemainMoney := remainMoney
	remainMoney += money
	fmt.Printf("原来有%d, 赚了%d,剩余%d\n", srcRemainMoney, money, remainMoney)
	<- lockChan
}



func main(){
	testSynchronize()
}
相关文章
相关标签/搜索