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有两端

:= 只能用在函数内部,在函数外部使用则会无法编译通过,所以要使用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  {
        ;
    }
}

猜你喜欢

转载自www.cnblogs.com/aidata/p/12757921.html