进阶篇二:并发编程

一、认识并发

在并发编程之前,让我们先理清两个概念——「并发」与「并行」

并发和并行都有两件以上的事情一起进行,并发是指两个或多个事件在同一时间间隔发生,二并行是指两个或者多个事件在同一时刻发生。Erlang 之父 Joe Armstrong 曾经用一张非常简单易懂的图解释了「并发」与「并行」的区别:

并发与并行

在操作系统原理中,对并发和并行有如下的介绍:

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并行

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

并发

并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。

二、关键字goroutine

2.1进程、线程和协程之间的联系

在面对对象编程中,线程实现了并行的操作。线程一般是由进程在运行时创建无状态任务,其生命周期受到进程的控制。

进程、线程是操作系统的基本概念,协程是计算机高级语言的一个新特性。这些概念比较抽象,进入详细介绍前,先用一个简单的例子进行介绍,看完之后能对进程和线程有个非常直观的印象,这样也方便理解后文。

在有限的市场资源下,企业是资源分配的基本单位。企业中,一个员工等同于一个线程,员工成为企业独立调度的基本单位。企业为了降低用人成本,用机器取代可替换的工作,这些机器成为开销更小的线程(协程)。

进程(Process)是一个程序在一个数据中的一次动态执行过程,可以理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。进程由程序控制块(PCB)、程序段、数据段组成。我们编写的程序用来描述进程要完成的什么功能(what)以及如何完成(how);程序控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用PCB来控制和管理进程,PCB是系统感知进程存在的唯一标志;数据段则是程序在执行过程中所需要使用的资源。进程的局限是在创建、撤销和切换时的开销比较大。

线程(Thread)是操作系统能够进行运算调度的最小单位,它是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。线程又称为迷你进程,但是比进程更容易创建,也更容易撤销。在引入线程后,线程成为了独立调度的基本单位,进程仅是资源分配的基本单位。

协程(Goroutine,也称轻量线程)是Go语言特有的名词。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

进程(Process)线程(Thread)协程(Goroutine)
概念是一个程序在一个数据中的一次动态执行过程,它是CPU资源分配和调度的独立单位。是操作系统能够进行运算调度的最小单位,它是进程中的一个执行任务(控制单元),负责当前进程中程序的执行是轻量级的线程,相比线程,占用的内存更小,切换消耗的时间更短
单位资源分配的基本单位独立调度的基本单位独立调度的基本单位
特点动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行。
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
原子性:指的是一个操作是不可中断,即使有多个线程执行,一个操作开始也不会受其他线程影响,即可以理解为线程的最小执行单元,不可被分割。
可见性:当某个线程修改了其内存中共享变量的值时,其他线程能立刻感知到其值的变化,这是可见性,对于串行线程来说,这个可见性现象是不存在的。
有序性:有序性即是程序按一定规则进行顺序的执行,期间会进行编译器优化重排、指令重排、内存重排等,执行规则遵循as-if-serial语义规则和happens-before原则。
轻量性:占用内存小,以很小的栈空间启动(2KB左右)。
动态性:
高效性:工作在用户态,切换消耗的时间更短。
实时性:能够实时地伸缩栈的大小,最大可以支持到GB级别。
线程、进程和协程的比较

2.2协程的目的

在传统的J2EE系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如遇到耗时的I/O行为,整个系统的吞吐能力会立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底。

最常见的例子就是JDBC(它是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的上下文切换(ContextSwitch)开销。

对于上述问题,现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是node.js以及Java里的新秀Vert.x。而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除上下文切换的开销。

2.3协程的调度原理

相比其他语言,golang采用了MPG模型管理协程,更加高效,但是管理非常复杂。

  • M:内核级线程
  • G:代表一个goroutine
  • P:Processor,处理器,用来管理和执行goroutine的。

G-M-P三者的关系与特点:
P的个数取决于设置的GOMAXPROCS,go新版本默认使用最大内核数,比如你有8核处理器,那么P的数量就是8。
M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。
P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列。
除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。

2.4协程和线程的比较

比较项线程协程
占用资源初始单位为1MB,固定不可变初始一般为 2KB,可随需要而增大
调度所属由 OS 的内核完成由用户完成
切换开销涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP等寄存器的刷新等只有三个寄存器的值修改 – PC / SP / DX.
性能问题资源占用太高,频繁创建销毁会带来严重的性能问题资源占用小,不会带来严重的性能问题
数据同步需要用锁等机制确保数据的一直性和可见性不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
线程和协程的比较

三、关键字channel

看完了以上内容,我们知道协程是独立执行的,他们之间没有通信。协程间必须通过通信协调/同步他们的工作。协程可以使用共享变量来通信,但是在Go中不提倡这样做,因为这种方式给所有的共享内存的多线程都带来了困难。

在Go中通过一种特殊的类型管道(channel),一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱。这种通信的方式保证了同步性。数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争,数据的所有权(读写数据的能力)也因此被传递。

3.1管道的介绍

管道的本质是一种只能存储相同数据结构的队列,遵循先进先出原则;线程安全,多协程访问时,不需要加锁。

管道的声明:

var intChan chan int
var stringChan chan string
var mapChan chan map[int]string
var objectChan chan interface{}

var pChan chan *int

管道初始化:

intChan = make(chan int)
stringChan = make(chan string)
mapChan = make(chan map[int]string)
objectChan = make(chan interface{})
pChan = make(chan *int)

3.2通信操作符

通信操作符的是使用:

//流向通道(发送)
intChan <- 0
//表示:用通道 intChan 发送变量 0
//从通道流出(接收)
var temp = <- intChan 
//表示:变量 temp 从通道 intChan 接收数据

3.3代码用例

3.3.1判断指定范围内数字的奇偶性并顺序打印

package main

import (
	"fmt"
	"time"
)

func main() {
	A := make(chan bool, 1)
	B := make(chan bool)
	Exit := make(chan bool)
	max := 100

	go func() {

		for i := 1; i <= max; i++ {
			if ok := <-A; ok {
				//time.Sleep(time.Second)
				if i%2 == 0 {
					fmt.Println(i, "是偶数")
				}
				B <- true
			}
		}
	}()
	go func() {
		defer func() {
			close(Exit)
		}()

		for i := 1; i <= max; i++ {
			if ok := <-B; ok {
				if i%2 != 0 {
					fmt.Println(i, "是奇数")
				}
				A <- true
			}
		}
	}()

	A <- true
	<-Exit
}
发表回复 0

Your email address will not be published. Required fields are marked *