Go 语言 协程和管道讲解

Go 语言 协程和管道讲解

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

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

举个栗子:

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

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

1.并发:

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

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

2.并行:

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

  • 特点:
    1. 多个任务作用在多个cpu上
    2. 从微观的角度看,在一个时间点上,多个任务在同时执行

并行的速度要快

三、协程基本介绍:

1.基本概念:

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

协程特点

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

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

3.MPG模式基本介绍:

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

MPG模式介绍

4.设置cpu数:

package main

import (
	"fmt"
	"runtime"
)

func main () {
	// 查看系统cpu个数
	cpuNum := runtime.NumCPU()
	// 可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum)
	fmt.Println("cpuNum", cpuNum)
}
  1. go 1.8版本以后,默认让程序运行在多核上,可不用设置;
  2. 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()
}
  1. 声明全局互斥锁;
  2. 写的时候加索,写完释放锁;
  3. 读的时候加索,读完释放锁;

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

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

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

2.使用管道channel解决:

2.1.channel的介绍:
  1. 声明方式:

    var 变量名 chan 数据类型

  2. channel 本质 就是一个数据结构(队列);

  3. 数据是先进先出(FIFO);

  4. 线程安全,多个协程访问,不需要加锁;

  5. channel只能存放指定数据类型

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

  6. channel是引用类型;

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

  7. channel数据放满后,就不能在放

  8. 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的方式进行遍历:

  1. 在遍历时,若channel没有关闭,出现deadlock错误
  2. 在遍历时,若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
		}
	}
}
  1. 切记,这里创建两个管道,是解决,主线程退出,协程还没有执行完,该程序就结束的问题
  2. 如果指向管道写入数据,而没有读取,就会出现阻塞dead lock,原因是超出了管道的容量
2.6.管道使用细节:
  1. 声明管道为只写:

    var chan2 chan<- int
    chan2 = make(chan int, 3)
    chan2<-20
    
  2. 声明管道为只读

    var chan3 <-chan int
    num := <- chan3
    
  3. 只读或只写,可以应用到函数传参时,做严格校验;

  4. 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
    		}
    	}
    }
    
  5. 协程中使用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)
    	}
    }
    
协程是轻量级的线程,可以在同一个程序中并发地执行多个任务。通过使用协程,我们可以更有效地利用计算资源并实现并发编程。而管道是用于在协程之间传递数据的通信机制。在Go语言中,我们可以使用管道来实现协程之间的同步和通信。 在Go语言中,我们可以通过以下步骤来使用协程管道: 1. 使用关键字"go"来创建一个协程,让其并发执行一个函数或方法。 2. 使用"make"函数来创建一个管道,并指定其元素类型和容量。管道可以是有缓冲的(指定了容量)或者无缓冲的(未指定容量)。 3. 在协程中,使用"<-"操作符将数据发送到管道中,或者从管道中接收数据。 4. 如果管道是无缓冲的,发送操作和接收操作会导致发送方和接收方都会阻塞,直到对应的操作完成。这种情况下,协程之间的通信是同步的。 5. 如果管道是有缓冲的,发送操作只有在管道已满时才会阻塞,接收操作只有在管道为空时才会阻塞。这种情况下,协程之间的通信是异步的。 下面是一个示例代码来演示协程管道的使用: ```go package main import ( "fmt" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "processing job", j) results <- j * 2 } } func main() { jobs := make(chan int, 5) results := make(chan int, 5) // 创建3个协程来并发执行任务 for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // 发送5个任务到管道中 for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // 从结果管道中接收并打印结果 for r := 1; r <= 5; r++ { fmt.Println(<-results) } } ``` 在这个示例中,我们创建了一个有缓冲的"jobs"管道和一个有缓冲的"results"管道。然后,我们创建了3个协程来并发执行任务。每个协程从"jobs"管道中接收任务,处理任务后将结果发送到"results"管道中。最后,主函数从"results"管道中接收并打印结果。 希望这个示例能够帮助你理解如何在Go语言中使用协程管道
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值