• Golang并发编程-Go程(Goroutine)实战篇


             Golang并发编程-Go程(Goroutine)实战篇

                                   作者:尹正杰

    版权声明:原创作品,谢绝转载!否则将追究法律责任。

     

     

     

    一.并行和并发概述

    1>.什么是并行(parallel)

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

    2>.什么是并发(concurrency)

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

    3>.并行和并发的区别

      如下图所示:
        并行是两个队列同时使用两台咖啡机(真正的多任务)
        并发是两个队列交替使用一台咖啡机(假的多任务)

    二.常见的并发编程技术

    1>.进程并发

      程序:
        是指编译好的二进制文件,在磁盘上,不占用系统资源
    
      进程:
        是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源,在内存中执行。换句话说,程序运行起来,产生一个进程。
    
      进程状态:
        进程基本的状态有5种。分别为初始态,就绪态(等待CPU分配时间片),运行态(占用CPU),挂起态(等待除CPU以外的其它资源主动放弃CPU)与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
    
      在使用进程 实现并发时会出现什么问题呢?
        1>.系统开销比较大,占用资源比较多,开启进程数量比较少;
        2>.在unix/linux系统下,还会产生"孤儿进程""僵尸进程”
          孤儿进程:
            父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
          僵尸进程:
            进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。  
    
      温馨提示:
        在操作系统运行过程中,可以产生很多的进程。在unix/linux系统中,正常情况下,子进程是通过父进程fork创建的,子进程再创建新的进程。并且父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用系统调用取得子进程的终止状态。  
        Windows下的进程和Linux下的进程是不一样的,它比较懒惰,从来不执行任何东西,只是为线程提供执行环境。然后由线程负责执行包含在进程的地址空间中的代码。当创建一个进程的时候,操作系统会自动创建这个进程的第一个线程,成为主线程。

    2>.线程并发

      线程概念:
        线程是轻量级的进程(light weight process),本质仍是进程(Linux下)
    
      进程:
        独立地址空间,拥有PCB(进程控制块) 
    
      线程:
        有独立的PCB(进程控制块),但没有独立的地址空间(即和其所在的进程共享用户空间)
    
      线程同步:
        指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
        "同步"的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。因此,所有"多个控制流,共同操作一个共享资源"的情况,都需要同步。
    
      常见锁的应用如下所示:
        互斥量(mutex):
          Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
          资源还是共享的,线程间也还是竞争的,但通过""就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。但应注意"同一时刻,只能有一个线程持有该锁"。
          举个例子:
            当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
            综上所述,互斥锁实质上是操作系统提供的一把"建议锁"(又称"协同锁"),建议程序中有多线程访问共享资源的时候使用该机制。但并没有强制限定。因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
    
        读写锁
          与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
          读写锁状态:
            特别强调:读写锁只有一把,但其具备两种状态,即读模式下加锁状态(读锁),写模式下加锁状态(写锁)。
          读写锁特性:
            读写锁是"写模式加锁"时,解锁前,所有对该锁加锁的线程都会被阻塞。
            读写锁是"读模式加锁"时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
            读写锁是"读模式加锁"时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
          温馨提示:
            读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
            读写锁非常适合于对数据结构读的次数远大于写的情况。

    3>.进程和线程的区别

      进程:
        是并发执行的程序在执行过程中分配和管理资源的基本单位。
    
      线程:
        是进程的一个执行单元,是比进程还要小的独立运行的基本单位。一个程序至少有一个进程,一个进程至少有一个线程。
    
      进程和线程的主要区别如下:
        根本区别:
          进程是资源分配最小单位,线程是程序执行的最小单位。 计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。
     
        地址空间:
          进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;线程没有独立的地址空间,同一进程的线程共享本进程的地址空间。
    
        资源拥有:
          进程之间的资源是独立的;同一进程内的线程共享本进程的资源。
    
        执行过程:
          每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    
        调度单位:  
          线程是处理器调度的基本单位,但是进程不是。由于程序执行的过程其实是执行具体的线程,那么处理器处理的也是程序相应的线程,所以处理器调度的基本单位是线程。  
          Windows系统下,可以直接忽略进程的概念,只谈线程。因为线程是最小的执行单位,是被系统独立调度和分派的基本单位。而进程只是给线程提供执行环境。
        系统开销:             进程执行开销大,线程执行开销小。

    4>.协程并发(coroutine)

      协程:
        coroutine。也叫轻量级线程。
        与传统的系统级线程和进程相比,协程最大的优势在于"轻量级"。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程别称"轻量级线程"的原因。
        一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。
        协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
        综上所述,我们可以总结说协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
    
      子程序调用:
        或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
        协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。 
     
      多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。关于协程调度的实现理论上分为以下三类模型:
        一对多:
          即用户态中的多个协程对应内核态的一个线程。
          如果在这样的轻量级线程中调用一个同步IO操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。     一对一:
          即用户态中的一个协程对应内核态的一个线程。
          虽然解决一对多的阻塞问题,但是本质上还是线程之间的切换。
        多对多:
          即用户态中的多个协程对应内核态的多个线程。
          相比一对多方案解决了阻塞问题,现在的协程调度器都是使用类似的模型。
      在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少!但能达到进程、线程并发相同的效果。   在一次并发任务中,进程、线程、协程均可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。

    5>.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调度存在会频繁的加锁解锁,最好的情况就是那个线程创建的协程就由哪个线程执行;
          早期的协程调度存在资源拷贝的弊端,频繁的在线程间切换会增加系统开销。
        Goroutine新版调度器算法(MPG):
          M:
            os线程(即操作系统内核提供的线程)       G:
            goroutine,其包含了调度一个协程所需要的堆栈以及instruction pointer(IP指令指针),以及其他一些重要的调度信息。       P:
            M与P的中介,实现m:n 调度模型的关键,M必须拿到P才能对G进行调度,P其实限定了golang调度其的最大并发度。         P默认和CPU核数相等,可按需设置。       M要去抢占P,如果抢到了P然后去领取G,如果没有任务会从其它的P或者全局的任务队列获取G。 

    三.Goroutine实战案例

    1>.什么是Goroutine

      Goroutine是Go语言并行设计的核心,有人称之为go程。 Goroutine从量级上看很像协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
    
      一般情况下,一个普通计算机跑几十个线程就有点负载过大了,但是同样的机器却可以轻松地让成百上千个goroutine进行资源竞争。

    2>.创建Goroutine

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func Task(start int, end int, desc string) {
        for index := start; index <= end; index += 2 {
            fmt.Printf("%s %d
    ", desc, index)
            time.Sleep(1 * time.Second)
        }
    }
    
    func main() {
        /**
        创建Goroutine:
            只需在函数调⽤语句前添加Go关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。
            在并发编程中,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。而go语言的并发设计,让我们很轻松就可以达成这一目的。
        
        Goroutine特性:
            为了避免类似孤儿进程的存在,如果main协程挂掉,所有协程都挂掉。
            换句话说,主goroutine退出后,其它的工作goroutine也会自动退出。
        */
        go Task(10, 30, "Task Func Say: index =")
    
        Task(11, 30, "Main Say: index =")
    }

    3>.Goexit函数

    package main
    
    import (
        "fmt"
        "runtime"
        "time"
    )
    
    func main() {
        go func() {
            defer fmt.Println("Goroutine 666666")
    
            func() {
                defer fmt.Println("Goroutine 88888888")
    
                /**
                return、Goexit() 和 os.Exit()的区别:
                    return:
                        一般用于函数的返回,只能结束当前所在的函数.
                    Goexit():
                        一般用于协程的退出
                        具有击穿特性,能结束掉当前所在的Goroutine,无论存在几层函数调用
                    os.Exit():
                        主动退出主Goroutine,换句话说,直接终止整个程序的运行。
                */
                runtime.Goexit() //终止当前Goroutine
                //return
                //os.Exit(100)
                fmt.Println("AAAA")
            }()
    
            fmt.Println("CCCCC")
        }()
    
        //我们的主Goroutine会运行15s,有充足时间使得上面的子Go程代码执行完毕哟~
        for index := 1; index <= 30; index += 2 {
            fmt.Printf("Main Say: index = %d
    ", index)
            time.Sleep(1 * time.Second)
        }
    }

    4>.Go程的安全机制

      博主推荐阅读:
        https://www.cnblogs.com/yinzhengjie2020/p/12657206.html
  • 相关阅读:
    Docker实用技巧之更改软件包源提升构建速度
    Jenkins 集群搭建
    Jenkins 无法捕获构建脚本错误问题
    CentOS 7 安装 Jenkins
    CentOS 7 安装 JAVA环境(JDK 1.8)
    CentOS 7 源码编译安装 Nginx
    CentOS 7 源码编译安装 Redis
    CentOS 7 源码编译安装 NodeJS
    Chrome 谷歌浏览器清除HTTPS证书缓存
    IdentityServer4实战
  • 原文地址:https://www.cnblogs.com/yinzhengjie2020/p/12556606.html
Copyright © 2020-2023  润新知