计算机并发过程举例子,并发性Concurrency

并发性Concurrency

1.1 什么是并发

Go是并发语言,而不是并行语言。在讨论如何在Go中进行并发处理之前,我们首先必须了解什么是并发,以及它与并行性有什么不同。(Go is a concurrent language and not a parallel one. )

并发性Concurrency是同时处理许多事情的能力。

举个例子,假设一个人在晨跑。在晨跑时,他的鞋带松了。现在这个人停止跑步,系鞋带,然后又开始跑步。这是一个典型的并发性示例。这个人能够同时处理跑步和系鞋带,这是一个人能够同时处理很多事情。

什么是并行性parallelism,它与并发concurrency有什么不同?

并行就是同时做很多事情。这听起来可能与并发类似,但实际上是不同的。

让我们用同样的慢跑例子更好地理解它。在这种情况下,我们假设这个人正在慢跑,并且使用它的手机听音乐。在这种情况下,一个人一边慢跑一边听音乐,那就是他同时在做很多事情。这就是所谓的并行性(parallelism)。

并发性和并行性——一种技术上的观点。

假设我们正在编写一个web浏览器。web浏览器有各种组件。其中两个是web页面呈现区域和下载文件从internet下载的下载器。假设我们以这样的方式构建了浏览器的代码,这样每个组件都可以独立地执行(这是在Java和Go中使用线程来完成的,我们可以在稍后使用Goroutines来实现这一点)。当这个浏览器运行在单个核处理器中时,处理器将在浏览器的两个组件之间进行上下文切换。它可能会下载一个文件一段时间,然后它可能会切换到呈现用户请求的网页的html。这就是所谓的并发性。并发进程从不同的时间点开始,它们的执行周期重叠。在这种情况下,下载和呈现从不同的时间点开始,它们的执行重叠。假设同一浏览器运行在多核处理器上。在这种情况下,文件下载组件和HTML呈现组件可能同时在不同的内核中运行。这就是所谓的并行性。

8f606e29da20373c77639b20070ade86.png

并行性Parallelism不会总是导致更快的执行时间。这是因为并行运行的组件可能需要相互通信。例如,在我们的浏览器中,当文件下载完成时,应该将其传递给用户,比如使用弹出窗口。这种通信发生在负责下载的组件和负责呈现用户界面的组件之间。这种通信开销在并发concurrent 系统中很低。当组件在多个内核中并行concurrent 运行时,这种通信开销很高。因此,并行程序并不总是导致更快的执行时间!

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

进程

进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。

进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。

线程

线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

协程

协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。

子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

协程的特点在于是一个线程执行,与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

1.2 Goroutines

1.2.1 什么是Goroutines

go中使用Goroutines来实现并发concurrently。Goroutines是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

Goroutines在线程上的优势。

与线程相比,Goroutines非常便宜。它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且是固定的

Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。如果线程中的任何Goroutine都表示等待用户输入,则会创建另一个OS线程,剩下的Goroutines被转移到新的OS线程。所有这些都由运行时进行处理,我们作为程序员从这些复杂的细节中抽象出来,并得到了一个与并发工作相关的干净的API。

当使用Goroutines访问共享内存时,通过设计的通道可以防止竞态条件发生。通道可以被认为是Goroutines通信的管道。

1.2.2 主goroutine

封装main函数的goroutine称为主goroutine。

主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随机,这个go程序的运行也会终止。

此后,主goroutine会进行一系列的初始化工作,涉及的工作内容大致如下:

创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束

启动专用于在后台清扫内存垃圾的goroutine,bin个设置GC可用的标识

执行mian包中的init函数

执行main函数

执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。最后主goroutine会结束自己以及当前进程的运行。

1.2.3 如何使用Goroutines

在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。

实例代码:

package main

import (

"fmt"

)

func hello() {

fmt.Println("Hello world goroutine")

}

func main() {

go hello()

fmt.Println("main function")

}

运行结果:可能会值输出“main function”。

e3d3e5f81ed3c91c4d8b89e7d28c6105.bmp

我们开始的Goroutine怎么样了?我们需要了解Goroutine的规则

当新的Goroutine开始时,Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束。当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码。

main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。

修改以上代码:

package main

import (

"fmt"

"time"

)

func hello() {

fmt.Println("Hello world goroutine")

}

func main() {

go hello()

time.Sleep(1 * time.Second)

fmt.Println("main function")

}

运行结果:

13ac3362dbd247810a923bc1e1bdc94e.bmp

在上面的程序中,我们已经调用了时间包的Sleep方法,它会在执行过程中睡觉。在这种情况下,main的goroutine被用来睡觉1秒。现在调用go hello()有足够的时间在main Goroutine终止之前执行。这个程序首先打印Hello world goroutine,等待1秒,然后打印main函数。

1.2.4 启动多个Goroutines

示例代码:

package main

import (

"fmt"

"time"

)

func numbers() {

for i := 1; i <= 5; i++ {

time.Sleep(250 * time.Millisecond)

fmt.Printf("%d ", i)

}

}

func alphabets() {

for i := 'a'; i <= 'e'; i++ {

time.Sleep(400 * time.Millisecond)

fmt.Printf("%c ", i)

}

}

func main() {

go numbers()

go alphabets()

time.Sleep(3000 * time.Millisecond)

fmt.Println("main terminated")

}

运行结果:

1 a 2 3 b 4 c 5 d e main terminated

时间轴分析:

ce04dc6214d9d1a078daa9e264396ded.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值