Go语言的并发特性是一大亮点。今天我们就带大家看看如何使用Go更好的开发并发程序?众所周知,计算机的核心是CPU,它是计算机的计算和控制核心,承载着所有的计算任务。近半个世纪以来,由于半导体技术的飞速发展,集成电路中晶体管的数量也大幅度增加,大大提高了CPU的性能。著名的摩尔定律——“一块集成电路芯片上集成的电路数量每18个月翻一番”就描述了这种情况。过密的晶体管虽然提高了CPU的处理性能,但也带来了发热量大、单芯片成本高等问题。同时,受限于材料工艺的发展,芯片中晶体管数量密度的增长速度放缓。换句话说,程序已经不能单纯依靠硬件的改进来提高运行速度了。这时候,多核CPU的出现让我们看到了提高程序运行速度的另一个方向:把程序的执行过程分成多个可以并行或者并发执行的步骤,让它们同时执行在不同的CPU核中,最后将各部分的执行结果进行组合,得到最终的结果。并行性和并发性是计算机程序执行的常见概念。它们的区别是:并行是指两个或多个程序同时执行;并发是指两个或多个程序在同一时间段内执行。对于并行执行的程序,无论从宏观还是微观角度看,都是有多个程序同时在CPU中执行。这就需要CPU提供多核计算能力,将多个程序分配到CPU的不同核上同时执行。对于并发执行的程序,只需要从宏观上观察CPU中同时执行的多个程序即可。即使是单核CPU,也可以通过时分复用的方式为多个程序分配一定的执行时间片,让它们在CPU上快速轮换执行,从而在宏观上模拟多个程序同时执行的效果.但从微观上看,这些程序实际上是在CPU中串行执行的。Go的MPG线程模型Go被认为是一种高性能并发语言,这要归功于其对协程并发的原生支持。这里先了解一下进程、线程和协程之间的联系和区别。在多道程序系统中,进程是一个具有独立功能的程序在一定的数据集上动态执行的过程。它是操作系统进行资源分配和调度的基本单位,是应用程序的载体。线程是程序执行过程中单一的顺序控制流,是CPU调度调度的基本单元。线程是比进程更小的独立运行的基本单位。一个进程可以有一个或多个线程。这些线程共享进程持有的资源,在CPU中被调度执行,共同完成进程的执行任务。在Linux系统中,操作系统根据不同的资源访问权限,将内存空间分为内核空间和用户空间:内核空间中的代码可以直接访问计算机的底层资源,如CPU资源、I/O资源等,为用户空间的代码提供访问计算机底层资源的能力;用户空间是上层应用程序的活动空间,不能直接访问计算机底层资源,需要通过“系统调用”、“库函数”等方式调用内核空间提供的资源。同样,线程也可以分为内核线程和用户线程。内核线程由操作系统管理和调度。它们是内核调度实体。它们可以直接操作计算机的底层资源,可以充分利用CPU多核并行计算的优势。但是切换线程的时候需要CPU切换到内核态,有一定的开销。您可以创建的线程数也受到操作系统的限制。用户线程由用户空间代码创建、管理和调度,操作系统无法感知。用户线程的数据存放在用户空间。切换时不需要切换到内核态。切换开销小且高效。可以创建的线程数理论上只与内存大小有关。协程是一个用户线程,是一个轻量级的线程。协程的调度完全由用户空间的代码控制;协程有自己的寄存器上下文和栈,存放在用户空间;切换协程时无需切换到内核态即可访问内核空间,切换速度极快。但这也给开发者带来了很大的技术挑战:开发者在处理用户空间的协程切换时,需要保存和恢复上下文信息,管理栈空间大小。Go是为数不多的在语言层面实现协程并发的语言之一。它使用了一种特殊的二级线程模型:MPG线程模型(如下图)。MPG线程模型M,或者machine,相当于Go进程中内核线程的映射。它与内核线程一一对应,代表了真正执行计算的资源。在M的生命周期中,它只会与一个内核线程相关联。P,即处理器,代表Go代码片段执行所需的上下文。M和P的组合可以为G提供有效的运行环境,它们之间的组合关系不是固定的。P的最大数量决定了Go程序的并发规模,由runtime.GOMAXPROCS变量决定。G,即goroutine,是一个轻量级的用户线程,封装了代码片段,在执行过程中拥有堆栈、状态、代码片段等信息。在实际执行过程中,M和P共同为G提供一个有效的运行环境(如下图所示),多个可执行的G依次挂载在P的可执行G队列下,等待调度执行。当G中有一些I/O系统调用阻塞M时,P会断开与M的连接,从调度器的空闲M队列中获取一个M或者创建一个新的M组合执行,保证G可以在P中执行其他队列中的Gs被执行,由于程序中并行执行的Ms数量不变,保证了程序的高CPU利用率。M和P结合示意图当G中的系统调用执行返回时,M会为G捕获一个P上下文,如果捕获失败,G会被放入全局可执行G队列中等待其他P的执行被收购。新创建的G会被放入全局可执行G队列中,等待调度器将其分发到合适的P的可执行G队列中。M和P合并后,将从P的可执行G队列中无锁获取G执行。当P的可执行G队列为空时,P会加锁从全局可执行G队列中获取G。当全局可执行G队列中没有G时,P会尝试从其他P的可执行G队列中“窃取”G执行。goroutine和channel并发程序中的多个线程在CPU上同时执行。由于资源和竞争条件之间的相互依赖,需要一定的并发模型来协调不同线程之间任务的执行。Go提倡使用CSP并发模型来控制线程间的任务协作,CSP提倡使用通信来实现线程间的内存共享。Go通过goroutine和channel来实现CSP并发模型:goroutine,即协程,Go中的并发实体,是一个轻量级的用户线程,是消息的发送者和接收者;channel,即channel,goroutine使用Channels收发消息。CSP并发模型类似于常用的同步队列。它更注重消息的传输方式,将发送消息的goroutines和接收消息的goroutines解耦。Channel可以独立创建和访问,并在不同的goroutine中传递和使用。使用关键字go来使用goroutine并发执行代码片段。形式如下:goexpression和channel是引用类型。声明的时候需要指定传输的数据类型。声明形式如下:varnamechanT//双向通道varnamechan<-T//只能发送消息的channelvarnameT<-chan//只能接收消息的通道其中T为数据类型通道可以传输。channel作为一个队列,遵循消息的先进先出顺序,同时保证同一时间只有一个goroutine可以发送或接收消息。使用channel发送和接收消息的形式如下:channel<-val//发送消息val:=<-channel//接收消息val,ok:=<-channel//非阻塞接收消息goroutine发送给已填充信息的通道一条消息或从本身没有数据块的通道接收消息。goroutine在接收消息时可以使用非阻塞的方式,不管通道中是否有消息都会立即返回,通过ok布尔值判断是否接收成功。要创建通道,您需要使用make函数来初始化通道。形式如下:ch:=make(chanT,sizeOfChan)在初始化通道时,可以指定通道的长度,表示通道最多可以缓存多少条信息。下面我们通过一个简单的例子来演示goroutine和channel的使用:packagemainimport("fmt""time")//producerfuncProducer(begin,endint,queuechan<-int){fori:=begin;i
