一般程序能够串行的执行一系列的任务最好,也很好写,但是往往对于网络相关的应用,往往需要多线程独立的处理能力

Go因此支持了并行的能力

为一个函数创建goroutine的时候,Go会将其放在一个独立的逻辑处理器上执行,Go语言的调度器是一个复杂的软件,为每个goroutin分配执行时间,调度器会在任何给定的时间,全面控制哪个goroutine在哪个逻辑处理器上运行

Go的并发同步模型来自一个叫做通信顺序的进程 CSP是一种消息传递的模型,用于在go的协程之间传递数据来传递消息,而不是对数据进行加锁来实现访问,用于在goroutine之间同步和传递数据的关键通道即channel

那么我们从系统的进程和线程调用说起

一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码,每个进程至少包含一个线程,每个进程的初始线程称为主线程

图片

上面是一个进程的运行状态,操作系统的调度决定哪个线程会获得给定的CPU的运行时间

而在Go中,我们利用逻辑处理器,调度所有的Goroutine

比如,我们创建了一个goroutine并准备运行,这个goroutine就被放在调度器的全局队列中,然后等待运行

图片

阻塞就替换并生成新的线程,所以go中的并发是一种协程

比如,我们运行的时候的goroutine需要打开一个文件的时候,线程和绑定的goroutine就会从逻辑处理器上分离,等待返回,返回后会再次进入本地队列

并发和并行是不同的,并行是在不同的代码片段在同时做很多事情

并发则是同时在管理很多事情

两者的区别可以简化的看做如下的图示

图片

我们先Pass逻辑处理器和物理处理器,也就是CPU核之间的关系

我们先看一下调度器本身的行为,以及Goroutine本身的使用方式

图片 图片

图片

我们在main函数中,调用了runtime包中的GOMAXPROCS函数

这个函数可以设置调度器可以使用的逻辑处理器的数量

环境变量也可以更改这个逻辑处理器的数量,我们设置了只能使用一个逻辑处理器

然后我们声明了两个匿名函数,显示英文字母表,根据代码清单给出的输出可以看到,每个goroutine的代码执行

在go函数运行之后,main函数继续往前执行,main函数通过WaitGroup,等待两个go函数执行完成

waitGroup是一个技术信号量,记录并维护执行的goroutine,如果WaitGroup的值大于0,Wait就会阻塞,为了减少WaitGroup的值,我们需要使用defer在函数退出的时候调用Done方法

关键字defer会在函数放回的时候真正调用声明的操作.

而且在调度器的内部,一个运行的goroutine并不会一直运行直到结束,而是可能被暂停,将其交给其他可运行的goroutine,给其运行的机会

切换的操作可以如下

图片

可以通过创建一个需要长时间完成的go函数来看这个行为

更深一步的操作,还有着控制多个逻辑处理器,方便分配到不同的物理CPU中,设置多个逻辑处理器的代码如下

图片

如上,我们就设置了逻辑处理器的数量,我们在创建完成这个数量之后,再次运行打印字母表的实例代码

这会发现代码是并行运行的,goroutine基本上是同时运行的

但是只有有多个逻辑处理器并且可以同时让每个goroutine运行在一个可用的物理处理器上的时候,goroutine才会并行运行

然后goroutine的竞争状态

如果多个goroutine试图同时读和写某个共享的资源,那么就是竞争的一个状态

竞争状态使得程序复杂

一个共享资源的读和写操作必须是原子化的,同一时刻会智能有一个goroutine对共享资源进行读和写操作

我们来看一个具有竞争状态的示例

图片

图片

图片

这个代码执行后,最终得到的Counter是2

这就是goroutine操作时候来回覆盖的问题

goroutine来会切换切换,但是会共享一个本地变量

在其他语言中,这也是很常见的

Go中还提供了一个特别的工具,可以在代码中检测竞争的状态

go build -race

图片

会之处这个例子中的代码有问题

对于这种竞争状态,最好的解决方案就是锁住共享资源

发表评论

邮箱地址不会被公开。 必填项已用*标注