• Go(四) 并发编程


    一、基本概念

    并行和并发

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

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

    进程并发

        程序:编译成功得到的二进制文件。 占用磁盘空间。

        进程:运行起来的程序。占用系统资源(内存)。

        进程和程序是N比1的关系,也就是说同一个程序可以加载为不同进程。如同时开两个终端,各自都有一个bash当彼此ID不同。

    进程状态

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

     进程并发:

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

            系统开销比较大,占用资源比较多,开启进程数量比较少

        在Unix/Linux系统下,可以产生很多进程。正常情况下,子进程通过父进程fork创建,子进程再创建新的进程。并且父进程永远无法预测子进程到底什么时候结束。当一个子进程完成它的工作终止之后,它的父进程需要调用系统取得子进程的终止状态。

        孤儿进程

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

        僵尸进程

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

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

    线程并发

    线程

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

        进程:独立地址控件,拥有PCB

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

        区别:在于是否共享地址空间。进程相当于独居,线程则是合租。

                   进程是最小的执行单位,线程是最小分配资源单位,可以看出只有一个线程的进程。

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

    线程同步

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

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

       同步是为了避免数据混乱,解决与时间有关的错误。所有多个控制流,共同操作一个共享资源的情况都需要同步。

       线程同步机制:

            锁的应用

             互斥锁(互斥量):建议锁。拿到锁以后,才能访问数据,没有拿到锁的线程,阻塞等待。等到拿到锁的线程释放锁。

              读写锁:一把锁(读属性、写属性)。写独占,读共享。写锁优先级高。    

    协程并发

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

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

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

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

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

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

    进程:稳定资源消耗大

    线程:资源消耗小

    协程:程序执行效率高 

    比如生成手机

    生成线----------进程

    工人--------线程

    50个工人---------50个线程

    10条生产线----------500个工人------------多进程、多线程

    上个工序没做完,下个工序的工人只能等待

    老板让等待的工人去搬砖-----------协程-------多进程、多线程、多协程

    二、Goroutine

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

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

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

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

    Goroutine创建和使用

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

    创建于进程中

    调用方法时,直接使用go关键字,防止于调用方法的前方即可产生go程,并发执行。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func sing()  {
        for i:=0; i<50; i++{
            fmt.Println("---正在唱:隔壁泰山---")
            time.Sleep(100 * time.Millisecond)
        }
    }
    
    func dance(){
        for i:=0; i<50; i++{
            fmt.Println("---正在跳舞:赵四街舞---")
            time.Sleep(100 * time.Millisecond)
        }
    }
    
    func main() {
        go sing()
        go dance()
    
        for{
            ;
        }
    }

    结果,两个方法交替执行

        主go程结束,其他的工作go程也会自动退出。

    runtime包

    Gosched

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

    func main() {
    
        go func() {
            for  {
                fmt.Println("this is goroutine")
            }
    
        }()
    
        for  {
            fmt.Println("this is main")
        }
    }

     this is main 和 this is goroutine交替出现,且都成片出现

    加入Gosched后

    func main() {
    
        go func() {
            for  {
                fmt.Println("this is goroutine")
            }
    
        }()
    
        for  {
            runtime.Gosched() // 让出当前CPU时间片
            fmt.Println("this is main")
        }
    }

    this is main打印一次就会让出CPU时间片

    ...
    this is goroutine
    this is goroutine
    this is goroutine
    this is goroutine
    this is goroutine
    this is main
    this is goroutine
    this is goroutine
    this is goroutine
    this is goroutine
    ...

    Goexit

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

        return:返回当前函数调用 return之前的defer才有效。

        Goexit:结束调用该函数的当前go程。Goexit之前注册的defer都生效。

    GOMAXPROCS

    设置可以并行计算的CPU核数的最大值,并返回上一次调用成功的设置值,首次调用返回默认值。

    三、Channel

    Go语言的一个核心类型,可以看出管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。channel是一个数据类型,主要用于解决协程的同步问题及协程之间数据共享的问题。

    goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine奉行通过通信来共享内存,而不是共享内存来通信。

    引用类型channel可用于多个goroutine通讯。其内部实现了同步,并确保并发安全。

     定义channel变量

    和map类似,channel也是一个对应make创建的底层数据结构的引用。

    当我们复制一个channel或用于函数参数传递时,只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其他的引用类型一样,channel的零值也是nil。

    定义一个channel时,需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建。

    chan是创建channel所需要使用的关键字。Type代表指定channel收发数据的类型。

    make(chan Type) // 等价于make(chan Type, 0)
    make(chan Type, capacity)

    当我们复制一个channel或用于函数参数传递时,我们只拷贝一个channel引用,因此调用者和被调用这将引用一个channel对象。和其他的引用类型一样,channel的零值也是nil。

    当参数capacity=0时,channel是无缓冲阻塞读写的,当capacity>0时,channel有缓冲、是非阻塞的,直到写满capacity个元素才阻塞写入。

    通过<-来接收和收发数据

    channel <- value // 发送value到channel
    <- channel // 接收并将其丢弃
    x := <- channel // 从channel中接收数据,并赋值给x
    x, ok := <- channel // 功能同上,同时检查通道是否已关闭或是否为空

    channel用于协程通信

    每当一个进程启动时,系统会自动打开三个文件:标准输入、标准输出、标准错误,对应三个文件:stdin、stdout、stderr

     channel有两端

    写端: ch <- ,如果没有读端在读,写端阻塞

    读端: <- ch, 读端读数据,同时写端不在写,读端阻塞

    := 只能用在函数内部,在函数外部使用则会无法编译通过,所以要使用var方式来定义全局变量

    var channel = make(chan int)
    
    // 定义一台打印机
    func printer(s string)  {
        for _, ch := range s{
            fmt.Printf("%c", ch) // 屏幕:stdout
            time.Sleep(300 * time.Millisecond,)
        }
    }
    
    
    // 定义两个人使用打印机
    func person1(){ // 先执行
        printer("hello")
        channel <- 8
    }
    
    func person2()  { // 后执行
        <-channel
        printer("world")
    }
    
    func main() {
       go person1()
       go person2()
        for  {
            ;
        }
    }

    上面的是无缓冲channel

    无缓冲channel

    无缓冲(unbuffered)的channel是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的goroutine阻塞等待。

    这种通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

    阻塞:由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足,才接触阻塞。

    同步:在两个或多个协程(线程)间,保持数据内容一致性的机制。

    通道容量为0,len=0,不能存储数据

    应用于两个及以上go程,一个读,一个写

    func main() {
    
        ch := make(chan int)
    
        go func() {
            for i:=0; i<5; i++ {
                fmt.Println("子go程,i= ", i)
                ch <- i
            }
        }()
    
        for i:=0; i<5; i++{
            num := <- ch
            fmt.Println("主go程读: ", num)
        }
    }

    结果

    子go程,i=  0
    子go程,i=  1
    主go程读:  0
    主go程读:  1
    子go程,i=  2
    子go程,i=  3
    主go程读:  2
    主go程读:  3
    子go程,i=  4
    主go程读:  4

    为什么不是打印一个子go程然后打印一个主go程呢?

    1.主go程开始执行,通道里没有数据,阻塞

    2.子go程开始执行打印 ”子go程,i= 0“,将0写入channel

    3.channel有数据后,主go程被唤醒,从channel中读取数据

    4.读取完发生什么?因为打印是IO操作,要访问硬件,比较耗时,因此读完数据后,还没来得及打印,子go程又被唤醒,主go程阻塞。

    5.子go程开始执行打印 ”子go程,i= 1“,将1写入channel

    6.主go程被唤醒,这次才有时间执行打印“主go程读: 0”

    有缓冲channel

    通道容量为非0,len(ch)指channel中剩余未读取数据个数,cap(ch)指通道的容量。

    channel应用于两个go程中,一个读,一个写。

    缓冲区可以进行数据存储,存储至容量上限,阻塞。具备异步能力,不需同时操作channel缓冲区。

    func main() {
    
        ch := make(chan int, 3) // 存满3个元素之前不会阻塞
        fmt.Println("len = ", len(ch), "cap = ", cap(ch))
    
        go func() {
            for i:=0; i<8; i++{
                ch <- i
                fmt.Println("子go程:i ", i, "len = ", len(ch), "cap = ", cap(ch))
            }
        }()
    
        time.Sleep(time.Second * 3)
        for i:=0; i<8; i++ {
            num := <- ch
            fmt.Println("主go程读到: ", num, "len = ", len(ch), "cap = ", cap(ch))
        }
    
    }
    len =  0 cap =  3
    子go程:i  0 len =  1 cap =  3
    子go程:i  1 len =  2 cap =  3
    子go程:i  2 len =  3 cap =  3
    
    主go程读到:  0 len =  3 cap =  3
    主go程读到:  1 len =  2 cap =  3
    主go程读到:  2 len =  1 cap =  3
    主go程读到:  3 len =  0 cap =  3
    子go程:i  3 len =  3 cap =  3
    子go程:i  4 len =  0 cap =  3
    
    
    
    
    子go程:i  5 len =  1 cap =  3
    子go程:i  6 len =  2 cap =  3
    子go程:i  7 len =  3 cap =  3
    主go程读到:  4 len =  0 cap =  3
    主go程读到:  5 len =  2 cap =  3
    主go程读到:  6 len =  1 cap =  3
    主go程读到:  7 len =  0 cap =  3

    IO阻塞导致打印结果顺序不符预期

    关闭channel

    如果发送者知道,没有更多需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。

    对端可以判断channel是否关闭

    if num, ok := <-ch; ok == true{
    ...

    如果已经关闭,ok返回false,num无数据,没有关闭则ok返回true,num返回保存的数据

    func main() {
    
        ch := make(chan int)
    
        go func() {
            for i:=0; i<8; i++{
                ch <- i
            }
            close(ch) // 写端,写完数据主动关闭channel
            // ch <- 6 不可向关闭的channel写数据
        }()
    
        for {
            if num, ok := <- ch; ok == true {
                fmt.Println("读到数据: ", num)
            }else {
                n := <- ch // 可从关闭的channel读数据,读到是0,说明关闭
                fmt.Println("关闭的channel", n)
                break
            }
        }
        for  {
            ;
        }
    }

    结果

    读到数据:  0
    读到数据:  1
    读到数据:  2
    读到数据:  3
    读到数据:  4
    读到数据:  5
    读到数据:  6
    读到数据:  7
    关闭的channel 0

    有缓冲和无缓冲channel对比

    有缓冲channel是写满缓存后才开始读,channel关闭后还能读到,读完缓存区的数据后才读到0

    func main() {
    
        ch := make(chan int, 8)
    
        go func() {
            for i:=0; i<8; i++{
                ch <- i
            }
            close(ch) // 写端,写完数据主动关闭channel
            // ch <- 6 不可向关闭的channel写数据
            fmt.Println("channel已经关闭")
        }()
    
        time.Sleep(time.Second * 5)
    
        for {
            if num, ok := <- ch; ok == true {
                fmt.Println("读到数据: ", num)
            }else {
                n := <- ch // 可从关闭的channel读数据
                fmt.Println("缓冲区里没有数据了", n)
                break
            }
        }
        for  {
            ;
        }
    }

    结果

    channel已经关闭
    
    读到数据:  0
    读到数据:  1
    读到数据:  2
    读到数据:  3
    读到数据:  4
    读到数据:  5
    
    
    
    
    
    读到数据:  6
    读到数据:  7
    缓冲区里没有数据了 0

    可遍历有缓冲channel,用range替代ok,注意ch左边没有 <-

    func main() {
    
        ch := make(chan int, 8)
    
        go func() {
            for i:=0; i<8; i++{
                ch <- i
            }
            close(ch) // 写端,写完数据主动关闭channel
            // ch <- 6 不可向关闭的channel写数据
            fmt.Println("channel已经关闭")
        }()
    
        for i := range ch{
            fmt.Println("读到的数据:", i)
        }
    
        for  {
            ;
        }
    }

    结果

    channel已经关闭
    读到的数据: 0
    读到的数据: 1
    读到的数据: 2
    读到的数据: 3
    读到的数据: 4
    读到的数据: 5
    读到的数据: 6
    读到的数据: 7

    单向channel

    默认的channel是双向的  var ch chan int    ch = make(chan int)

    单向写channel  var sendCh chan <- int    sendCh = make(chan <- int)

    单向读channel var recvCh <- Chan int    recvCh = make(<-chan int)

    转换:

        双向channel可以隐式转换为任意一种channel    sendCh = ch

        单向channel不能转换为双向channel    ch = sendCh/recvCh是错误的

        ch := make(chan int) // 双向channel
    
        var sendCh chan <- int = ch // 写channel
        sendCh <- 789
        num := <- sendCh // 错,写channel不能读
    
        var recvCh <- chan int = ch // 读channel
        num := <- recvCh // 错,会死锁,没有写端
        fmt.Println("num=", num)
    
        // 反向赋值
        /var ch2 chan int = recvCh // 错,单向channel不能分配给双向channel

    传参,传递的是引用:

    func send(out chan <- int)  {
        out <- 88
        fmt.Println("写入88")
        close(out)
    }
    
    func recv(in <- chan int)  {
        n := <- in
        fmt.Println("读到", n)
    }
    
    func main() {
    
        ch := make(chan int) // 双向channel
    
        go send(ch) // 双向channel转为写channel
    
        recv(ch)
    }

    结果

    写入88
    读到 88

    生产消费者模型

    单向channel最典型的应用是“生产消费者模型”。所谓生产消费者模型是指某个模块(函数等)负责生产数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、协程、线程、进程等)。产生数据的模块,就形象地称为生产者,而处理数据的模块就称为消费者。

    单单抽象出生产者和消费者,还够不上生产者/消费者模型。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图:

    缓冲区的好处:

    •     解耦
    •     并发(生产者和消费者数量不对等时,能保持正常的通信)
    •     缓存 (生产者和消费者数据处理速度不一致时,暂存数据)

    无缓冲channel:同步通信

    func producer(out chan <- int)  {
    
        for i:=0; i<10; i++{
            fmt.Println("生产:", i*i)
            out <- i * i
        }
        close(out)
    
    }
    
    func consumer(in <- chan int)  {
        for num := range in{
            fmt.Println("消费者拿到:", num)
            time.Sleep(time.Second)
        }
    }
    
    func main() {
    
        ch := make(chan int) // 双向channel
    
        go producer(ch)
        consumer(ch)
    }

    有缓冲channel:异步通信

    func producer(out chan <- int)  {
    
        for i:=0; i<10; i++{
            fmt.Println("生产:", i*i)
            out <- i * i
        }
        close(out)
    
    }
    
    func consumer(in <- chan int)  {
        for num := range in{
            fmt.Println("消费者拿到:", num)
            time.Sleep(time.Second)
        }
    }
    
    func main() {
    
        ch := make(chan int, 6) // 双向channel
    
        go producer(ch)
        consumer(ch)
    }

    模拟订单

    电商网站的订单处理就是非常典型的生产消费者模式。当很多用户单击下单按钮后,订单生产的数据全部放到缓冲区(队列)中,然后消费者将队列中的数据取出来发送给仓库等系统。用户可以随时下单,不必等待仓库系统的返回结果。

    type OrderInfo struct {
        id int
    }
    
    func producer(out chan <- OrderInfo)  { // 生产订单--生产者
    
        for i:=0; i<10; i++{ // 循环生成10个订单
            order := OrderInfo{id: i+1}
            out <- order // 写入channel
        }
        close(out) // 关闭channel
    
    }
    
    func consumer(in <- chan OrderInfo)  { // 处理订单--消费者
        for order := range in{ // 从channel取出订单
            fmt.Println("订单ID为:", order.id) // 模拟处理订单
        }
    }
    
    func main() {
    
        ch := make(chan OrderInfo, 6) // 双向channel
    
        go producer(ch)
        consumer(ch)
    }

    定时器

    time.Timer

    Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer等待多长时间。

    type Timer struct {
        C <-chan Time
        r runtimeTimer
    }

    它提供一个channel,在定时时间到达之前,没有数据写入timer.C会一直阻塞。直到定时时间到,系统会自动向timer.C这个channel中写入当前时间,阻塞即被解除。

    time.After()定时:

      指定定时时长,定时到达后,系统会自动向定时器的成员写入 系统当前时间,返回可读 chan 。读取,可获得系统写入时间。

    3种定时方法

    func main() {
        // 1.sleep
        time.Sleep(time.Second)
    
        // 2.Timer.C
        myTimer := time.NewTimer(time.Second * 2) // 创建定时器,指定定时时长
        nowTime := <- myTimer.C  // 定时满,系统会自动写入系统时间,系统是写端,我们这里进行读取
        fmt.Println("现下时间: ", nowTime)
    
        // 3.time.After
        fmt.Println("当前时间:", time.Now())
        nowTime2 := <- time.After(time.Second * 2)
        fmt.Println("现下时间:",nowTime2)
    
    }

    定时器的停止和重置

    func main() {
        myTimer := time.NewTimer(time.Second * 3)
        myTimer.Reset(1 * time.Second) // 重置定时器时长为1
        go func() {
            <- myTimer.C
            fmt.Println("子go程定时完毕")
        }()
        // myTimer.Stop() // 设置定时器停止,定时器归零,<-myTimer.C会阻塞
        for  {
            ;
        }
    }

    周期定时

    // A Ticker holds a channel that delivers `ticks' of a clock
    // at intervals.
    type Ticker struct {
        C <-chan Time // The channel on which the ticks are delivered.
        r runtimeTimer
    }

    每隔一个周期时长,系统会自动向Ticker的C中写入系统当前时间,可以看成是有循环功能的Timer

    例子

    func main() {
        quit := make(chan bool) // 创建一个判断是否终止的channel
        fmt.Println("now: ", time.Now())
        myTicker := time.NewTicker(time.Second) // 设置1秒
    
        i := 0
        go func() {
            for{
                nowTime := <- myTicker.C // 每秒系统会写入系统时间到C,这里是读取,每秒会获取到一次
                i ++
                fmt.Println("nowTimer: ", nowTime)
                if i == 2{
                    quit <- true // 解除主go程阻塞
                    break // return // runtime.Goexit
                }
            }
        }()
    
        <- quit // 子go程循环获取 <- myTicker.C期间,一直阻塞
    }

    四、select

    select是一个关键字,通过它可以监听channel上的数据流动。select的用法于switch语言非常类似,由select开始一个新的选择块,每个选择块条件由case语句来描述。

    与switch相比,select有较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致结构如下:

    select{
        case <- chan1:
            // 如果chan1成功读到数据,则进行该case处理语句
        case chan2 <- 1:
            // 如果chan2成功写入数据,则进行该case处理语句
        default:
            // 如果上面都没有成功,则进入default处理流程
    }

    在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

    如果有多条语句可执行即没阻塞,那么就可以从那些可以执行的语句中任意选择一条来使用,如果没有语句可执行1)如果有default,执行default,同时从select语句后的语句中恢复 。default通常不会用,因为可能产生忙轮询 2)如果没有default,那么select语句被阻塞,直到至少一个语句可以执行

    select自身不带循环机制,借助外层的for来循环监听,break跳出case选项。

    func main() {
        ch := make(chan int) // 用来进行数据通信的channel
        quit := make(chan bool) // 创建一个判断是否终止的channel
    
        go func() {
            for i:=0; i<5; i++ {
                ch <- i
                time.Sleep(time.Second)
            }
            close(ch)
            quit <- true // 通知主go程退出
            runtime.Goexit()
        }()
    
        for  { // 主go程度数据
            select {
            case num := <-ch:
                fmt.Println("读到: ", num)
            case <- quit:
                // break // 跳出select
                return
    
            }
            fmt.Println("=============")
        }
    }

    实现斐波那契数列

    func fibonacci(ch <- chan int, quit <- chan bool)  {
        for {
            select {
            case num := <- ch:
                fmt.Println(num, " ")
            case <- quit:
                runtime.Goexit()  // return
            }
        }
    }
    
    func main() {
        ch := make(chan int) // 用来进行数据通信的channel
        quit := make(chan bool) // 创建一个判断是否终止的channel
    
       go fibonacci(ch, quit) // 打印fibonacci数列
    
       x, y := 1, 1
        for i:=0; i<20; i++ {
            ch <- x
            x, y = y, x +y
        }
    
        quit <- true
    }

    超时处理

    有时goroutine会出现阻塞的情况,可以利用select来设置超时。

    func main() {
        ch := make(chan int) // 用来进行数据通信的channel
        quit := make(chan bool) // 创建一个判断是否终止的channel
    
         go func() {
             for {
                 select {
                 case num := <-ch:
                     fmt.Println(num)
                 case <-time.After(5 * time.Second):
                     fmt.Println("timeout")
                     quit <- true
                     break
                 }
             }
         }()
        // c <- 666 // 注释掉引发timeout
        <- quit
    }

    五、锁和条件变量

    死锁

    不是锁的一种,是一种错误使用锁导致的现象。

    1. 单go程自己死锁

    channel 应该在 至少 2 个以上的 go程中进行通信。否则死锁!!!

    2. go程间channel访问顺序导致死锁

    使用channel一端读(写), 要保证另一端写(读)操作,同时有机会执行。否则死锁。

    3. 多go程,多channel 交叉死锁

    Ago程,掌握M的同时,尝试拿N; Bgo程,掌握N的同时尝试拿M。

    4. 在go语言中,尽量不要将 互斥锁、读写锁 与 channel 混用。 —— 隐性死锁。

    package main
    
    // 死锁1
    /*func main()  {
        ch :=make(chan int)
        ch <- 789
        num := <-ch
        fmt.Println("num = ", num)
    }*/
    
    // 死锁2
    /*func main()  {
        ch := make(chan int)
        go func() {
            ch <- 789
        }()
        num := <- ch
        fmt.Println("num = ", num)
    }*/
    
    // 死锁 3
    func main()  {
        ch1 := make(chan int)
        ch2 := make(chan int)
        go func() {                //
            for {
                select {
                case num := <-ch1:
                    ch2 <- num
                }
            }
        }()
        for {
            select {
                case num := <- ch2:
                    ch1 <- num
            }
        }
    }

    互斥锁(互斥量)

    A 、B go程 共同访问共享数据。 由于cpu调度随机,需要对 共享数据访问顺序加以限定(同步)。

    创建 mutex(互斥锁),访问共享数据之前,加锁,访问结束,解锁。 在Ago程加锁期间,B go程加锁会失败——阻塞。

    直至 A go程 解说mutex,B 从阻塞处。恢复执行。

    package main
    
    import (
        "fmt"
        "time"
        "sync"
    )
    
    // 使用channel 完成同步
    /*var ch = make(chan int)
    
    func printer(str string)  {
        for _, ch := range str {
            fmt.Printf("%c", ch)
            time.Sleep(time.Millisecond * 300)
        }
    }
    
    func person1()  {                // 先
        printer("hello")
        ch <- 98
    }
    
    func person2()  {                // 后
        <- ch
        printer("world")
    }
    
    func main()  {
        go person1()
        go person2()
        for {
            ;
        }
    }
    */
    
    // 使用 “锁” 完成同步 —— 互斥锁
    var mutex sync.Mutex        // 创建一个互斥量, 新建的互斥锁状态为 0. 未加锁。 锁只有一把。
    
    func printer(str string)  {
        mutex.Lock()            // 访问共享数据之前,加锁
        for _, ch := range str {
            fmt.Printf("%c", ch)
            time.Sleep(time.Millisecond * 300)
        }
        mutex.Unlock()            // 共享数据访问结束,解锁
    }
    
    func person1()  {                //
        printer("hello")
    }
    
    func person2()  {                //
        printer("world")
    }
    
    func main()  {
        go person1()
        go person2()
        for {
            ;
        }
    }

    读写锁

    读时共享,写时独占。写锁优先级比读锁高。

    package main
    
    import (
        "math/rand"
        "time"
        "fmt"
        "sync"
    )
    
    var rwMutex sync.RWMutex        // 锁只有一把, 2 个属性 r w
    
    func readGo(in <-chan int, idx int)  {
        for {
            rwMutex.RLock()            // 以读模式加锁
            num := <-in
            fmt.Printf("----%dth 读 go程,读出:%d
    ", idx, num)
            rwMutex.RUnlock()        // 以读模式解锁
        }
    }
    
    func writeGo(out chan<- int, idx int)  {
        for {
            // 生成随机数
            num := rand.Intn(1000)
            rwMutex.Lock()            // 以写模式加锁
            out <- num
            fmt.Printf("%dth 写go程,写入:%d
    ", idx, num)
            time.Sleep(time.Millisecond * 300)        // 放大实验现象
            rwMutex.Unlock()
        }
    }
    
    func main()  {
        // 播种随机数种子
        rand.Seed(time.Now().UnixNano())
    
        // quit := make(chan bool)            // 用于 关闭主go程的channel
        ch := make(chan int)            // 用于 数据传递的 channel
    
        for i:=0; i<5; i++ {
            go readGo(ch, i+1)
        }
        for i:=0; i<5; i++ {
            go writeGo(ch,i+1)
        }
        for{
            ;
        }
    }
    package main
    
    import (
        "math/rand"
        "time"
        "fmt"
        "sync"
    )
    
    var rwMutex sync.RWMutex        // 锁只有一把, 2 个属性 r w
    
    var value int        // 定义全局变量,模拟共享数据
    
    func readGo05(idx int)  {
        for {
            rwMutex.RLock()            // 以读模式加锁
            num := value
            fmt.Printf("----%dth 读 go程,读出:%d
    ", idx, num)
            rwMutex.RUnlock()        // 以读模式解锁
            time.Sleep(time.Second)
        }
    }
    func writeGo05(idx int)  {
        for {
            // 生成随机数
            num := rand.Intn(1000)
            rwMutex.Lock()            // 以写模式加锁
            value = num
            fmt.Printf("%dth 写go程,写入:%d
    ", idx, num)
            time.Sleep(time.Millisecond * 300)        // 放大实验现象
            rwMutex.Unlock()
        }
    }
    func main()  {
        // 播种随机数种子
        rand.Seed(time.Now().UnixNano())
    
        for i:=0; i<5; i++ {            // 5 个 读 go 程
            go readGo05(i+1)
        }
        for i:=0; i<5; i++ {            //
            go writeGo05(i+1)
        }
        for{
            ;
        }
    }
    package main
    
    import (
        "math/rand"
        "time"
        "fmt"
    )
    
    //var value06 int        // 定义全局变量,模拟共享数据
    func readGo06(in <-chan int, idx int)  {
        for {
            num := <-in                // 从 channel 中读取数据
            fmt.Printf("----%dth 读 go程,读出:%d
    ", idx, num)
            time.Sleep(time.Second)
        }
    }
    func writeGo06(out chan<- int, idx int)  {
        for {
            // 生成随机数
            num := rand.Intn(1000)
            out <- num                    // 写入channel
            fmt.Printf("%dth 写go程,写入:%d
    ", idx, num)
            time.Sleep(time.Millisecond * 300)        // 放大实验现象
        }
    }
    func main()  {
        // 播种随机数种子
        rand.Seed(time.Now().UnixNano())
        ch := make(chan int)
    
        for i:=0; i<5; i++ {            // 5 个 读 go 程
            go readGo06(ch, i+1)
        }
        for i:=0; i<5; i++ {            //
            go writeGo06(ch, i+1)
        }
        for{
            ;
        }
    }

    条件变量

    本身不是锁,但经常与锁结合使用。

    使用流程:

    • 1. 创建 条件变量: var cond sync.Cond
    • 2. 指定条件变量用的 锁: cond.L = new(sync.Mutex)
    • 3. cond.L.Lock() 给公共区加锁(互斥量)
    • 4. 判断是否到达 阻塞条件(缓冲区满/空) —— for 循环判断
    • for len(ch) == cap(ch) { cond.Wait() —— 1) 阻塞 2) 解锁 3) 加锁
    • 5. 访问公共区 —— 读、写数据、打印
    • 6. 解锁条件变量用的 锁 cond.L.Unlock()
    • 7. 唤醒阻塞在条件变量上的 对端。 signal() Broadcast()

    生成消费者模型

    package main
    
    import (
        "fmt"
        "time"
        "math/rand"
    )
    
    func producer(out chan<- int, idx int)  {
        for i:=0; i<50; i++ {
            num := rand.Intn(800)
            fmt.Printf("生产者%dth,生产:%d
    ", idx, num)
            out <- num
        }
        close(out)
    }
    
    func consumer(in <-chan int, idx int)  {
        for num := range in {
            fmt.Printf("-----消费者%dth,消费:%d
    ",idx,  num)
        }
    }
    
    func main()  {
        product := make(chan int)
        rand.Seed(time.Now().UnixNano())
    
        for i:=0; i<5; i++ {
            go producer(product, i+1)            // 1 生产者
        }
        for i:=0; i<5; i++ {
            go consumer(product, i+1)            // 3 个消费者
        }
        for {
            ;
        }
    }
    package main
    
    import (
        "fmt"
        "time"
        "math/rand"
        "sync"
    )
    var cond sync.Cond            // 定义全局条件变量
    
    func producer08(out chan<- int, idx int)  {
        for {
            // 先加锁
            cond.L.Lock()
            // 判断缓冲区是否满
            for len(out) == 5 {
                cond.Wait()            // 1. 2. 3.
            }
            num := rand.Intn(800)
            out <- num
            fmt.Printf("生产者%dth,生产:%d
    ", idx, num)
            // 访问公共区结束,并且打印结束,解锁
            cond.L.Unlock()
            // 唤醒阻塞在条件变量上的 消费者
            cond.Signal()
            time.Sleep(time.Millisecond * 200)
        }
    }
    
    func consumer08(in <-chan int, idx int)  {
        for {
            // 先加锁
            cond.L.Lock()
            // 判断 缓冲区是否为空
            for len(in) == 0 {
                cond.Wait()
            }
            num := <-in
            fmt.Printf("-----消费者%dth,消费:%d
    ",idx,  num)
            // 访问公共区结束后,解锁
            cond.L.Unlock()
            // 唤醒 阻塞在条件变量上的 生产者
            cond.Signal()
            time.Sleep(time.Millisecond * 200)
        }
    }
    
    func main()  {
        product := make(chan int, 5)
        rand.Seed(time.Now().UnixNano())
    
        quit := make(chan bool)
    
        // 指定条件变量 使用的锁
        cond.L = new(sync.Mutex)                // 互斥锁 初值 0 , 未加锁状态
    
        for i:=0; i<5; i++ {
            go producer08(product, i+1)            // 1 生产者
        }
        for i:=0; i<5; i++ {
            go consumer08(product, i+1)            // 3 个消费者
        }
    /*    for {
            runtime.GC()
        }*/
        <-quit
    
    }
  • 相关阅读:
    OpenCV中 常用 函数 的作用
    OpenCV中Mat的使用
    awk --- 常用技巧
    Specify 的含义 ------ 转载
    关于CPU CACHE工作机制的学习
    关于CPU Cache -- 程序猿需要知道的那些事
    ARM920T的Cache
    Learn Git and GitHub
    朴素贝叶斯分类器(MNIST数据集)
    k-近邻算法(KNN)识别手写数字
  • 原文地址:https://www.cnblogs.com/aidata/p/12757921.html
Copyright © 2020-2023  润新知