Golang学习之---goroutine协程和channel管道(八)

一、协程goroutine

  1. exe称为文件或者源码,只有执行起来的时候才叫进程。进程是跑起来了的。把文件加载到内存中执行,我们就把这个叫做进程。进程程序在操作系统中的一次执行过程。
  2. 分为后台进程和带页面的进程。
    打开电脑的任何一个应用,这里都会加入对应的进程。
  3. 如:打开一个迅雷应用,就是起了一个进程。迅雷中同时进行5个文件的下载,就是5个线程。从宏观看,有5个文件在同时下载,从微观看,有一个时间片段,来回切换,一个时间点,只有一个在运作。这就叫并发。
  4. 一个进程下面有很多的线程,这样可以最大的发挥CPU的性能。线程是进程的一个执行实例。一个进程至少有一个线程
  5. 一个程序能不能同时起多个进程呢?看程序的设定或者操作系统。
    比如暴风影音,原来可以允许多个暴风,同时播放多个视频,但是现在不行了,再打开第二个默认进入第一个。这就是原来可以同时起多个进程,现在只能单线程运行了。
  6. 一个进程能不能同时起多个线程呢?看程序的设定或者操作系统。
    比如腾讯视频。一个进程是否可以起多个线程,是可以设置的。如:视频下载的时候,可以设置为允许多个视频同时下载,也可以设置为只下载一个。
  7. 进程和线程是操作系统的概念,是由操作系统调度的。不由用户控制。
  8. 多线程在单核上运行,为并发。时间片的切换速度非常快,以毫秒(?)计算,人眼反应不过来,看起来像是在同时进行。如:在网页上同时打开两个视频,看起来是同时在播放,其实是以非常快的速度切换的。从宏观看,貌似在同时进行。
  9. 进程--线程--协程  发展史:
    ①原来就是一个进程走天下,后来发现效率太低了(摩尔定律,硬件发展总比软件快)。为了跟上硬件,提高使用效率,开发了线程。线程是一个轻量级的进程,可以在一个进程上开好几个线程。
    ②后来发现线程还是吃资源的,毕竟线程还是作用在进程上的,还是要占用CPU和内存的。有些地方还是物理态的,比较笨拙。
    ③于是乎,对编译器等做了算法优化,变成逻辑态,把线程变得更轻量级,做到资源共享,就叫做协程。协程是轻量级的线程。在一个主线程(有的程序员直接称为线程/也可以理解成进程。这个还没定下来)中,可以轻轻松松地写上万个协程。

1.1 进程和线程

①进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
②线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
③一个进程可以创建和销毁多个线程,同一进程中的多个线程可以并发执行。
④一个程序至少有一个进程,一个进程至少有一个线程

1.2 程序、进程和线程的关系示意图

1.3 并发和并行

①多线程在单核上运行,为并发
②多线程在多核上运行,为并行
③示意图:


小结:

  • 并发:多个任务作用在一个cpu上。比如有10个线程,每个线程执行10毫秒(进行轮询操作)。从人的角度看,好像这10个线程同时在运行,但是从微观上看,在某一个时间点只有一个线程在执行,这就是并发。
  • 并行:多个任务作用在多个cpu上(比如有10个cpu)。比如有10个线程,每个线程执行10毫秒(各自运行在不同的cpu上)。从人的角度看,这10个线程都在运行,从微观上看,在某一个时间点,也同时有10个线程在执行,这就是并行。

1.4 Go协程和Go主线程

  • ①Go主线程(有的程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程。可以这样理解:协程是轻量级的线程[编译器做优化]
  • ②Go协程的特点:
    1、有独立的栈空间
    2、共享程序堆空间
    3、调度由用户控制
    4、协程是轻量级的线程
  • ③示意图:

1.5 快速入门及示意图

小结:
①主线程是一个物理线程,直接作用在cpu上的。是重要级的,非常耗费cpu资源。
②协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。
③Golang的协程机制是非常重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这就突出Golang在并发上的优势了。

1.6 协程的调度模型

1.6.1 MPG 模式基本介绍

1.6.2 MPG模式运行的状态1 -- 静态调用

1.6.3 MPG模式运行的状态2 -- 动态调用

  1. 新的协程,是从线程池中拿到,还是新创建,取决于当前的环境。
  2. 读写的时候阻塞,我们看来是1~2秒的事情,但在CPU中1S已经算是很长时间了
  3. 当多个协程出现时,就会以队列的方式
  4. 操作系统+编译器共同完成MPG

1.7 设置Golang运行的CPU数

为了充分利用多CPU的优势,在Go程序中,设置运行的CPU数量
go1.8 后,Go默认运行在多核上,提高效率,可以不用设置了;
go1.8 前,还是要设置一下的,可以更高效的利用cpu

1.8 通过一个案例看出单独使用协程存在的问题,引出管道

问题1、使用协程来完成,效率高,但是会出现并发/并行的安全问题:同一个时间点向同一个地址写入数据,报错【concurrent map writes :并发的map写入】
这就好比:一本书可以多人同时看一个地方,但是多个人不能同时往同一个地方写。写是应该有保护的
产生原因:多个协程同时往同一个map空间写入数据
在运行某个程序时,如何知道是否存在资源竞争问题?答:在编译程序时,增加一个参数 -race 即可。 go build -race xxx.go race:竞争
问题2、不同协程之间通信问题

1.8.1 执行结果


1.8.2 示意图

1.8.3  解决 “协程带来的并发/并行的安全问题” 的方法:加入 互斥锁
没有对全局变量加锁,因此出现资源争夺问题,代码出现错误【concurrent map writes】。我们尝试着加入锁

为什么读的时候也需要加锁呢?主线程不知道什么时候结束,可能中间尝试着去看一看,可能也会造成资源竞争。

1.8.4 加锁的弊端

  • ①前面使用全局变量加锁同步来解决goroutine的通讯,并不完美
  • ②加锁的地方必须每一个都想到,否则还会报错。比如:读取的数据的地方,我们认为不用加锁了,但是还是要的。要把每一处都想到,太麻烦了;
  • ③再者,问题二通讯问题还没有解决,我们如何设置主线程的等待时间呢
  • ④综上,我们需要一种其他方法来解决。即管道channel

二、管道channel

2.1 基本介绍

  • 管道本质就是一个数据结构-队列
  • ②遵循先进先出FIFO【first in first out】
  • ③本身就是线程安全的,不需要加锁。多协程访问时不受影响。编译器在底层维护的,管道线程安全。
  • ④管道是有类型的,一个string类型的channel只能存放string数据类型的数据
  • ⑤示意图:

2.2 声明/定义

  • var 变量名 chan 数据类型
  • 举例:
    var intChan chan int   存放int类型的数据
    var mapChan chan map[int]string  存放map[int]string类型的数据
    var perChan chan Person 
    var perChan chan *Person 
  • 引用类型,必须初始化后才能使用。即必须make后才能使用
  • 是引用类型,指向一个地址,该地址指向的才是真正要操作的管道数据
  • 管道是有数据类型的,intChan只能写入整数int类型的数据
  • 使用:

2.3 使用的注意事项

  1. channel只能存放指定的数据类型(interface{}也是指定的数据类型,后面如果使用要进行类型断言)
  2. channel的数据放满后,就不能再放了,否则报错deadlock
  3. 从channel中取出数据后,可以继续放入。是一种流动的模型。
  4. 管道的容量不能自动增长,长度随数据的写入读出增加或减少。即使写入的数据>容量也没关系,只要有读有写是一种流动的状态,读的频率大于写的频率也没有关系,这是一种有效的阻塞,也是可以的。管道的价值在于一边放一边取。
  5. channel中的数据取完了,再取就会报错deadlock
  6. 把一个管道类型声明为空接口类型,后面如果使用要进行类型断言因为,在编译层面上还是空接口类型(虽然打印出来是main.结构体类型,但是这是在运行时才知道的),所以会报错【 cat55.Name undefined (type interface {} is interface with no methods)】
  7. 使用示例...

2.4 管道的关闭

  1. 使用内置函数close可以关闭管道,当管道关闭后就不能再向channel写入数据了,但是仍然可以读取数据。
  2. close后,管道内的数据都被取出(读取/遍历)完毕后,自动退出。如果没有close,就会认为管道内一直还有数据,无法退出,最后发生死锁。【fatal error: all goroutines are asleep - deadlock!】

2.5 管道的遍历

  1. 管道的遍历只能用for-range形式。且没有key-value, 只有value一个值。由于没有key,所以不能跳过第一个值直接取第三个,必须一个一个的挨着取出来才行。管道中没有下标值,所以不能跳过几个值直接取第几个值
  2. 管道的cap不能自动增长,len随数据的写入读出增加或减少。所以不能使用普通for循环来遍历。

三、协程和管道的应用实例

3.1 应用实例一

3.1.1 需求:

3.1.2 示意图:

3.1.3 代码实现

3.1.4 代码效果

3.1.5 怎么判断读取数据结束的?
x是取出来的数据,ok默认为true,当管道中的数据都被取走后,将ok设置为false

3.2 应用实例二 -- 阻塞

  • 一、问题:如果注销 go readData(intChan, exitChan) 会发生什么情况?
  • 情况①:如果cap(intChan)=10,写入的数据为50个:会阻塞在写入数据的地方
  • 情况②:如果cap(intChan)=50,写入的数据为50个:正常。就当成一个存数据的管道就行
  • 二、问题:如果不注销 go readData(intChan, exitChan) 会发生什么情况?
  • 情况①:如果cap(intChan)=10,写入的数据为50个:正常。这是因为:cap小于要写入的数据个数也没啥,只要有读取,就是一个流动状态,就不会发生死锁。
  • 情况②:如果cap(intChan)=10,写入的数据为50个,使用了time.Sleep(time.Second),且写的频率远低于读的频率:正常。这是因为:如果读的频率和写的频率不一样,无所谓。最多就是读的时候等一等,等写入了再读取。这是一种有效堵塞只要管道有读有写,就有流动性,就不会发生死锁。死锁是无效阻塞,是要报错的。

3.3 应用实例三

3.3.1 需求:

3.3.2 思路分析图:

3.3.3 代码实现

3.3.4 代码效果
说明:
管道是一边写一边取的,不是我们平常理解的,都写完了才进行取值。。。也是Go快的一个原因

3.3.5 和传统方法(只用一个for循环)耗时对比
当开启多个协程的时候,可以明显的看到CPU的使用升高,执行完毕后,CPU的使用就会降下来。

3.4 channel的使用细节和注意事项

  • (1)管道可以声明为只读或者只写性质。数据类型还是管道chan int。只读send、只写receive是性质。默认管道是可读可写的。作用:设置为自读/只写,可以防止误操作。
  • (2)管道只读、只写的最佳案例
  • (3)使用select可以解决从管道读取数据的堵塞问题。用法与switch非常类似。如果管道没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配。可以在default语句中使用return语句来终止select语句的执行。尽量不用使用label标签,甚至在以后的版本里会剔除这种处理方式。

    如果不close就想去读取或遍历管道数据,就会deadlock。但是我们此时还不想关闭(或者不知道什么时候关闭,一些复杂的需求)该怎么操作?用select,可以解决从管道读取数据的阻塞问题。

    程序的输出顺序是由操作系统或者编译器控制的,和当前的上下文环境有关了。
  • (4)goroutine中使用recover,解决协程中出现panic导致的程序崩溃问题。
    如果说我们起了一个协程,但是这个协程出现了panic,如果未作处理,就会造成整个程序的崩溃。这是我们可以在goroutine中使用defer+recover来捕获panic进行处理。这样即使这个协程发生了问题,但是主线程和其他协程仍然不受影响,可以继续执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值