Go 并发编程技术

Jackey Golang 1,995 次浏览 , , , 没有评论

概述

简而言之,所谓并发编程是指在一台处理器上“同时”处理多个任务。

随着硬件技术的发展,并发程序变得越来越重要。web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的额批处理问题(读取数据、计算、写输出),现在也会并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。

宏观的并发是指在一段时间内,有多个程序在同时运行。

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

并行和并发

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

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

程序和进程

程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(内存、打开的文件、设备、锁。。。)

进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)

在Windows系统下,通过查看“任务管理器”,可以查看相应的进程。

进程状态

进程的基本状态有5种,分别为:初始态、就绪态、运行态、挂起(阻塞)态、终止(停止)态。其中初始态为进程准备阶段,常与就绪态结合来看。

进程并发

在使用进程实现并发时会出现什么问题呢?

  1. 系统开销比较大,占用资源比较多,开启进程数量比较少。
  2. 在unix/Linux系统下,还会产生“孤儿进程”和“僵尸进程”。

在操作系统运行过程中,可以产生很多的进程。在unix/Linux系统中,正常情况下,子进程是通过父进程fork创建的,子进程再创建新的进程。

并且父进程永远无法预测子进程到底什么时候结束。当一个进程完成它的工作终止后,它的父进程需要调用系统取得子进程的终止状态。

孤儿进程:父进程先于子进程技术,则子进程成为孤儿进程,子进程的父进程为init进程,称为init进程领养孤儿进程。

僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

Windows下的进程和Linux下的进程是不一样的,它比较懒惰,从来不执行任何东西,只是为线程提供执行环境。然后由线程负责执行包含在进程的地址空间中的代码。当创建一个进程的时候,操作系统会自动创建这个进程的第一个线程,称为主线程。

什么是线程

LWP:light weight process 轻量级的进程,本质仍是进程(Linux下)

进程:独立地址空间,拥有PCB

线程:有独立的PCB,但没有独立的地址空间(共享)

区别:在于是否共享地址空间。独居(进程);和居(线程)

线程:最小的执行单位

进程:最小分配资源单位,可看成是只有一个线程的进程。

Windows系统下,可以直接忽略进程的概念,只谈线程。因为线程是最小的执行单位,是被系统独立调度和分派的基本单位。而进程只是给线程提供执行环境。

线程同步

同步即协同步调,按预定的先后次序运行。

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。

举例:内存中100字节,线程T1欲填入全1,线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续,从失去cpu的位置向后写入1,当执行结束,内存中的100字节,即不全是1,也不全是0.

产生的现象叫做”与时间有关的错误“(time related)。为了避免这种数据混乱,线程需要同步。

同步的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间都需要同步机制。

因此,所有”多个控制流,共同操作一个共享资源“的情况,都需要同步。

互斥量mutex

Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

资源还是共享的,线程间也还是竞争的,但通过锁就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

协程并发

协程:coroutine。也叫轻量级线程。

与传统的系统级线程和进程相比,协程最大的优势在于”轻量级“。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通畅很难超过1万个。这也是协程别称”轻量级线程“的原因。

一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源

多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步IO操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。

在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少,但能达到进程、线程并发相同的效果。

在一次并发任务中,进程,线程,协程均可以实现。从系统资源消耗的角度来看,进程相当多,线程次之,协程最少。

Go并发

Go在语言级别支持协程,叫goroutine。Go语言标准库提供的所有系统调用操作(包括所有同步IO操作),都会出让CPU给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于cpu的核心数量。

有人把Go比作21世纪的C语言。第一是因为Go语言设计简单,第二,21世纪最重要的额就是并行程序设计,而Go从语言层面就支持并行。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。。

Go语言为并发变成而内置的上层API基于顺序通讯进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接收数据以实现同步,这大大地简化了并发程序的编写。

Go语言中的并发程序主要使用两种手段来实现。goroutine和channel.

什么是Goroutine

goroutine是Go语言并行设计的核心,有人称之为go程。goroutine所到底其实就是协程,它比线程更小,十几个goroutine可能提现在底层就是五六个线程,Go语言内部帮实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概4-5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

壁板情况下,一个普通计算机跑几十个线程就有点负载过大了,但是通用的机器却可以轻松的让成百上千个goroutine进行资源竞争。

Goroutine的创建

只需要在函数调用语句前添加 go 关键字,就可以创建并发执行单元。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。

在并发编程中,我们通常想将一个过程切分成几块,然后将每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它 main goroutine。新的 goroutine 会用 go 语句来创建。而 go 语言的并发设计,让我们很轻松就可以达成这一目的。

示例参考:Golang goroutine

Gosched

runtime.Gosched() 用于让出cpu时间片,让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次再获得cpu时间片的时候,从该让出cpu的位置恢复执行。

代码示例:

func main() {
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")

    for i := 0; i < 2; i++ {
        runtime.Gosched()
        fmt.Println("hello")
    }
}

Goexit

调用 runtime.Goexist() 将立即终止当前goroutine 执行,调度器确保所有已注册 defer 延迟调用被执行。

代码示例:

func test() {
    defer fmt.Println("cccccccccc")
    runtime.Goexit()
    fmt.Println("dddddddddd")
}

func main() {
    go func() {
        fmt.Println("aaaaaaaaa")
        test()
        defer fmt.Println("bbbbbbbbbb")
    }()
    for {

    }
}

GOMAXPROCS

调用 runtime.GOMAXPROCS()  用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

代码示例:

func main() {
    runtime.GOMAXPROCS(1)
    for {
        go fmt.Println(0)
        fmt.Println(1)
    }
}

channel

详情参考:Golang channel 和  Go语言并发编程

发表回复

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

Go