zoukankan      html  css  js  c++  java
  • Golang 并发编程

    前言

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

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

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

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

    image

    并行和并发

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

    image

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

    image

    • 宏观:用户体验上,程序在并行执行。
    • 微观:多个计划任务,顺序执行,在飞快的切换,轮换使用 cpu 时间轮片。

    大师曾以咖啡机的例子来解释并行和并发的区别:

    image

    • 并行是两个队列同时使用两台咖啡机 (真正 的多任务)
    • 并发是两个队列交替使用一台咖啡机 ( 的多任务)

    常见并发编程技术

    进程并发

    程序和进程

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

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

    程序 → 剧本(纸) 进程 → 戏(舞台、演员、灯光、道具 ...)

    同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)

    如:同时开两个终端。各自都有一个 bash 但彼此 ID 不同。

    在 windows 系统下,通过查看“任务管理器”,可以查看相应的进程。运行起来的程序就是一个进程。如下图所示:

    image

    进程状态

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

    image

    进程并发

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

    • 系统开销比较大,占用资源比较多,开启进程数量比较少。
    • 在 unix/linux 系统下,还会产生孤儿进程僵尸进程

    通过前面查看操作系统的进程信息,我们知道在操作系统中,可以产生很多的进程。

    在 unix/linux 系统中,正常情况下,子进程是通过父进程 fork 创建的,子进程再创建新的进程。

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

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

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

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

    线程并发

    什么是线程

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

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

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

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

    image

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

    线程:最小的执行单位

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

    线程同步

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

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

    举例1: 银行存款 5000。柜台:取3000;同时提款机:取 3000。剩余:2000。

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

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

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

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

    锁的应用

    互斥量 mutex

    Linux 中提供一把互斥锁 mutex(也称之为互斥量)。

    每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

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

    image

    但是应注意:同一时刻,只能有一个线程持有该锁。

    当 A 线程对某个全局变量加锁访问,B 在访问前尝试加锁,拿不到锁,B 阻塞。C 线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

    所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但是并没有强制限定。

    因此,即使有了 mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

    读写锁

    与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享

    读写锁状态:

    特别强调:读写锁只有一把,但其具备两种状态:

    • 读模式下加锁状态 (读锁)
    • 写模式下加锁状态 (写锁)

    读写锁特性:

    • 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
    • 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
    • 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

    读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

    读写锁非常适合于对数据结构读的次数远大于写的情况。

    协程并发

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

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

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

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

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

    协程的目的:提高程序执行的效率。

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

    进程、线程、协程 总结

    • 进程、线程、协程都可以完成并发。
    • 进程并发稳定性高、开销大。
    • 线程开销小(节省资源)。
    • 协程效率高。

    选择哪一种进行并发并没有一个统一的答案,需要根据具体情况具体分析。

    最后,举一个例子帮助大家更好的理解一下三者的区别:

    比如说我是一家生产手机的老板,要通过一条生产线(进程)去生产手机,光有生产线还不够,还需要工人(线程)来完成手机的生产。

    这时只有一条生产线,一个工人,我们可以看作是一个单进程、单线程的程序。

    接下来我发现这样效率太低了,我要多招工人,假设招了 50 个工人,也就是有了 50 个线程。

    这时有一条生产线,50 个工人,我们可以看作是一个单进程、多线程的程序。

    再接下来,还想提高效率,我弄 10 条生产线,每条生产线招 50 个工人,那么就需要 500 个工人。

    这时有 10 条生产线,500 个工人,我们可以看作是一个多进程、多线程的程序。

    但是如果上一个程序没有完成,接下来的工人都会在等待。那么就会有工人在这时刷刷微博、逛逛朋友圈什么的。我心想这不行,我得把这空闲时间利用起来啊。所以,老板又制定了一条规则:所有工人,如果有空闲时间,都要到我家来搬砖(协程)。

    这时有 10 条生产线,500 个工人,工作的同时还去我家帮忙搬砖,我们可以看作是一个多进程、多线程、多协程的程序。(我可真是个黑心老板 ...)

    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。

    李培冠博客

    欢迎访问我的个人网站:

    李培冠博客:lpgit.com

  • 相关阅读:
    STL源码剖析之_allocate函数
    PAT 1018. Public Bike Management
    PAT 1016. Phone Bills
    PAT 1012. The Best Rank
    PAT 1014. Waiting in Line
    PAT 1026. Table Tennis
    PAT 1017. Queueing at Bank
    STL源码剖析之list的sort函数实现
    吃到鸡蛋好吃,看看是哪只母鸡下的蛋:好用的Sqlite3
    cJSON
  • 原文地址:https://www.cnblogs.com/lpgit/p/13430849.html
Copyright © 2011-2022 走看看