6

Go 语言 协程和管道讲解

 4 years ago
source link: https://studygolang.com/articles/33340
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

参考链接: 角度6-管道

Go 语言 协程和管道讲解

一、进程和线程基本说明:

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

举个栗子:

使用的迅雷客户端,打开迅雷就是开启了一个进程,而下载多个视频,就是多个线程在工作;

二、并发、并行简单说明:

1.并发:

多线程程序在单核上运行,就是并发;

特点:

多个任务作用在一个cpu上;从微观的角度看,在一个时间点上,其实只有一个任务在执行,只是时间切片较块;

2.并行:

多线程程序在多核上运行,就是并行;

特点:

多个任务作用在多个cpu上;从微观的角度看,在一个时间点上,多个任务在同时执行;

并行的速度要快

三、协程基本介绍:

1.基本概念:

一个线程上,可以有多个协程,协程是轻量级的线程;

协程特点:

有独立的栈空间;共享程序堆空间;调度由用户控制;协程是轻量级的线程;

2.快速案例:

package main

import (

"fmt"

"strconv"

"time"

)

func test(){

for i:= 0; i < 10; i++{

fmt.Println("test() " +  strconv.Itoa(i))

time.Sleep(time.Second)

}

}

func main() {

go test()  // 开启一个协程

for i := 0; i < 10; i++{

fmt.Println("main()" + strconv.Itoa(i))

time.Sleep(time.Second)

}

}

主线程是一个物理线程,直接作用在CPU上,非常消耗CPU资源;协程从主线程开启的,是轻量级的线程,对资源消耗小;其它语言的并发机制一般是基于线程,开启过多的线程,资源消耗较大,这就体现出golang的优势;

3.MPG模式基本介绍:

M:操作系统的主线程(物理线程);P:协程执行需要的上下文;G:协程;

MPG模式介绍

4.设置cpu数:

package main

import (

"fmt"

"runtime"

)

func main () {

// 查看系统cpu个数

cpuNum := runtime.NumCPU()

// 可以自己设置使用多个cpu

runtime.GOMAXPROCS(cpuNum)

fmt.Println("cpuNum", cpuNum)

}

go 1.8版本以后,默认让程序运行在多核上,可不用设置;go 1.8版本前,需要设置,才可以更高效的利用cpu;

四、协程之间如何通讯?

1.全局变量加锁:

package main

import (

"fmt"

"sync"

"time"

)

var (

myMap = make(map[int]int, 10)

// 声明一个全局互斥锁

// lock 是一个全局互斥锁 sync 是包 同步的意思 Mutex:是互斥

lock sync.Mutex

)

// test函数计算 n的阶乘, 将结果放到map中

func testCount(n int) {

res := 1

for i := 1; i <= n; i++ {

res *=i

}

// 加锁

lock.Lock()

myMap[n] = res

// 解锁

lock.Unlock()

}

func main () {

// 开启多个协程完成20个任务

for i := 1; i <= 20; i++{

go testCount(i)

}

time.Sleep(time.Second * 5)

lock.Lock()

for k, v := range myMap{

fmt.Printf("map[%d]=%d\n", k, v)

}

lock.Unlock()

}

声明全局互斥锁;写的时候加索,写完释放锁;读的时候加索,读完释放锁;

否则会出现资源竞争的问题;报错信息:fatal error: concurrent map writes

全局变量加锁同步是低级程序操作:

主线程等待所有协程全部完成时间很难确定,因为主线程结束,不管协程是否执行完,程序就此结束;通过全局变量加锁同步实现通讯,也不利于多个协程对全局变量的读写操作;

2.使用管道channel解决:

2.1.channel的介绍:

声明方式:

var 变量名 chan 数据类型

channel 本质 就是一个数据结构(队列);  数据是先进先出(FIFO);  线程安全,多个协程访问,不需要加锁;  channel只能存放指定数据类型;

如:一个string的channel只能存放string类型数据

channel是引用类型;

必须初始化才能写入数据,即make后才能使用

channel数据放满后,就不能在放;  channel数据取完后,再取就会报错;

2.2.快速栗子:

package main

import "fmt"

func main() {

// 管道的使用

// 1.创建一个可以存放3个int类型的管道

var intChan chan int

intChan = make(chan int, 3)

// 2.查看intChan 是什么?

fmt.Printf("intchan:%v\n", intChan)  // 输出结果: intchan:0xc00008c080 可以看出是引用类型

// 3.向管道写入数据

intChan<- 10

num := 100

intChan<- num  // 也可以写入常量

// 4.看看管道的长度和cap(容量:定义的长度跟容量是相等的, 不同于map类型等)

fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

//  4输出结果:channel len=2 cap=3

//5.从管道中读取数据

//var num2 int

num2 := <-intChan

fmt.Println("取出的num2=", num2)

fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

// 5输出结果:channel len=1 cap=3

}

注意:

如果往管道中存入数据,管道已经满了,或者取数据,管道中已经没有值,会报错信息fatal error: all goroutines are asleep - deadlock!

2.3.channel关闭:

使用内置函数close可以关闭channel,当channel关闭后,就不能向channel写数据,但是仍然可以读数据;

举个栗子:

package main

import "fmt"

func main() {

// 创建一个管道,大小为3

intChan := make(chan int, 3)

intChan <- 3

intChan <- 5

// 将管道进行关闭

close(intChan)

// 此时会无法写入, 因为管道已经关闭: 报错信息 panic: send on closed channel

//intChan <-6

n1 := <- intChan

fmt.Println("可以从管道中读取值:", n1)

}

2.4.channel遍历:

channel 支持 for-range的方式进行遍历:

在遍历时,若channel没有关闭,出现deadlock错误;在遍历时,若channel已经关闭,会正常遍历数据,遍历完之后,就会退出遍历;

举个栗子:

package main

import "fmt"

func main () {

// 创建一个管道, 大小为200

intChan := make(chan int, 200)

for i := 0; i < 200; i++ {

intChan<- i * 2

}

// 在遍历取值时, 一定要关闭管道

close(intChan)

// 遍历, 取出管道所有的值

for value := range intChan{

fmt.Println("value:", value)

}

}

如果在遍历取值的时候,不关闭管道会报错:fatal error: all goroutines are asleep - deadlock!

2.5.协程与管道的使用:

package main

import "fmt"

// 往管道里写入50条数据

func writeDate(intChan chan int)  {

for i := 1; i <= 50; i++{

// 写入数据

intChan<- i

fmt.Println("管道中写入数据:", i)

}

// 写完后,关闭此管道

close(intChan)

}

// 从管道中读取数据

func readData(intChan chan int, exitChan chan bool) {

for {

v, ok := <-intChan

// 说明intChan管道已经取完了

if !ok{

break

}

fmt.Printf("intChan 管道取出数据:%v\n", v)

}

// readData 取完后表示任务已经完成

exitChan<- true

close(exitChan)

}

func main()  {

// 创建两个管道

intChan := make(chan int, 50)

// 退出管道, 主线程监控, 协程取完intChan后, 会写进此管道一条数据

exitChan := make(chan bool, 1)

// 开启写的协程、读的协程

go writeDate(intChan)

go readData(intChan, exitChan)

// 写一个for循环, 监听exitChan管道, 若exitChan管道的数据取完, 主线程可以结束

for {

_, ok := <- exitChan

if !ok{

break

}

}

}

切记,这里创建两个管道,是解决,主线程退出,协程还没有执行完,该程序就结束的问题;如果指向管道写入数据,而没有读取,就会出现阻塞dead lock,原因是超出了管道的容量;

2.6.管道使用细节:

声明管道为只写: var chan2 chan<- int

chan2 = make(chan int, 3)

chan2<-20

声明管道为只读: var chan3 <-chan int

num := <- chan3

只读或只写,可以应用到函数传参时,做严格校验;  select可以解决从管道取数据阻塞问题: package main

import (

"fmt"

"time"

)

func main() {

// 使用select 可以解决管道数据堵塞的问题

// 1.定义一个int类型管道, 大小为10

intChan := make(chan int, 10)

for i := 0; i < 10; i++{

intChan<- i

}

// 2.定义一个管道 5个数据string

strChan := make(chan string, 5)

for i := 0; i < 5; i++ {

strChan<- "hello" + fmt.Sprintf("%s", i)

}

// 传统方法在遍历管到时, 如果不关闭会阻塞 导致deadlock

// 实际开发中,有时不确定什么时候关闭该管道

for {

select {

// 注意:若intChan 一直没有关闭, 不会一直阻塞而导致deadlock

// 会自动到下一个case匹配

case v := <- intChan:

fmt.Printf("从intChan读取数据%d\n", v)

time.Sleep(time.Second)

case v := <- strChan:

fmt.Printf("从strChan读取数据%s\n", v)

time.Sleep(time.Second)

default:

fmt.Printf("取不到数据咯~~~\n")

return

}

}

}

协程中使用recover,解决协程中出现panic,导致程序崩溃问题: package main

import (

"fmt"

"time"

)

func sayGo() {

// 写一个正常运行的函数

for i := 0; i < 10; i++{

time.Sleep(time.Second)

fmt.Println("hello golang")

}

}

func testErr() {

// 使用defer + recover 进行对此函数的异常捕获

defer func() {

if err := recover(); err != nil{

fmt.Printf("test()函数发生错误:%v", err)

}

}()

// 写一个错误的函数

var myMap map[int]string

myMap[0] = "hello"

}

func main() {

go sayGo()

go testErr() // 若此函数错误, 不会影响到其余函数 所以在此函数里加上捕获异常

for i := 0; i < 10; i++{

fmt.Println("main() ok=", i)

time.Sleep(time.Second)

}

}

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK