Go基础10:协程和管道

概述

进程/线程

进程:是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程:是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

并发/并行

多线程程序在单核心的 cpu 上运行,称为并发;

多线程程序在多核心的 cpu 上运行,称为并行。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

协程/线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。使用Go语言开发服务器程序时,就需要对它的并发机制有深入的了解。

goroutine

goroutine(协程)是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。

说到底 goroutine 其实就是线程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,而且Go语言内部也实现了 goroutine 之间的内存共享。

goroutine 的特点

  1. 有独立的栈空间

  2. 共享程序堆空间

  3. 调度由用户控制

  4. 协程是轻量级的线程

goroutine 的用法

//go 关键字放在方法调用前新建一个 goroutine 并执行方法体
go GetThingDone(param1, param2);

//新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)

//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
    //do someting...
}

测试案例:

package main

import (
	"fmt"
	"time"
)

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test()", i)
		time.Sleep(time.Second)
	}
}

func main() {

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

	for i := 0; i < 10; i++ {
		fmt.Println("main()", i)
		time.Sleep(time.Second)
	}
}

输出结果:

main() 0
test() 0
main() 1
test() 1
test() 2
main() 2
main() 3
test() 3
test() 4
main() 4
main() 5
test() 5
test() 6
main() 6
main() 7
test() 7
main() 8
test() 8
test() 9
main() 9

主线程和协程执行流程图:

在这里插入图片描述

使用小结:

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

goroutine 的调度模型

MPG 模式基本介绍:

在这里插入图片描述

M:操作系统的主协程(物理线程)

P:协程执行需要的上下文

G:协程

MPG 模式运行的状态1

在这里插入图片描述

  1. 当前程序有三个M,如果三个M都是在一个CPU运行,就是并发,如果在不同的CPU运行就是并行。
  2. M1,M2,M3正在执行一个G,M1的协程队列有3个,M2的协程队列有3个,M3的协程队列有2个。
  3. 从上图可以看到,Go 的协程是轻量级的线程,是逻辑态的, Go 可以容易的起上万个协程。
  4. 其他程序 C/Java 的多线程,往往是内核态的,比较重量级,几千个线程可能耗光CPU。

MPG 模式运行的状态2

在这里插入图片描述

  1. 分成两个部分来看。
  2. 原来的情况是 M0 主线程正在执行 Go线程,另外有3个协程在队列等待。
  3. 如果 Go 协程阻塞,比如读取文件获取数据库等。
  4. 这时就会创建 M1 主线程(也可能是从已有的线程池中取出 M1),并且将等待的3个协程挂到 M1 下开始执行,M0 的主线程下的 Go 仍然执行文件 io 的读写。
  5. 这样的MPG调度模式,可以既让 Go 执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。
  6. 等到 Go 不阻塞了,M0 会被放到空闲的主线程继续执行(从已有的线程池中取),同时 Go 又会被唤醒。

设置运行的 CPU 数

Go 1.8后,默认让程序运行在多个核上,可以不用设置。

// 获取当前系统CPU的数量
num := runtime.NumCPU()
// 设置num个CPU运行Go程序
runtime.GOMAXPROCS(num)

channel

channel(通道)是Go语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或多个 goroutine 之间传递消息。

channel 是进程内的通信方式,因此通过 channel 传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用分布式系统的方法来解决,比如使用 Socket 或者 HTTP 等通信协议。Go语言对于网络方面也有非常完善的支持。

channel 是类型相关的,也就是说,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。如果对 Unix 管道有所了解的话,就不难理解 channel,可以将其认为是一种类型安全的管道。

channel 的声明

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

测试案例:

package main

import (
	"fmt"
)

func main() {
	// 1. 创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)

	// 2. 查看intChan是什么
	fmt.Printf("intChan的值=%v  intChan本身的地址=%p\n", intChan, &intChan) // intChan的值=0xc00013e000  intChan本身的地址=0xc000136018

	// 3. 向管道写入数据
	intChan <- 10
	num := 211
	intChan <- num
	intChan <- 50
	// intChan<- 98// 注意点:当我们给管写入数据时,不能超过其容量

	// 4. 查看管道的长度和cap(容量)
	fmt.Println(len(intChan), cap(intChan)) // 3, 3

	// 5. 从管道中读取数据
	var num2 int
	num2 = <-intChan
	fmt.Println("num2=", num2)
	fmt.Println(len(intChan), cap(intChan)) // 2, 3

	// 6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
	num3 := <-intChan
	num4 := <-intChan
	num5 := <-intChan
	fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}

使用小结:

  1. channel 中只能存放指定的数据类型。
  2. channle 的数据放满后,就不能再放入了。
  3. 如果从 channel 取出数据后,可以继续放入。
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取就会报 deadlock。

channel 的关闭

使用内置函数 close 可以关闭 channel,当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据。

package main

import "fmt"

func main() {
	var intChan chan int
	intChan = make(chan int, 3)

	intChan <- 100
	intChan <- 200
	close(intChan) // 关闭管道

	// 当管道关闭后,不能够再写入数据
	// intChan <- 300 // error

	// 当管道关闭后,读取数据是可以的
	n1 := <-intChan
	fmt.Println(n1) // 100
}

channel 的遍历

channel 支持 for–range 的方式进行遍历,注意两个细节:

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main

import "fmt"

func main() {
	var intChan chan int
	intChan = make(chan int, 100)

	for i := 0; i < 100; i++ {
		intChan <- i
	}

	close(intChan)

	// 1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
	// 2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
	for v := range intChan {
		fmt.Println(v)
	}
}

应用实例

实例1

goroutine 和 channel 协同工作的案例,具体要求:

  1. 开启一个 writeData 协程,向管道 intChan 中写入 50 个整数。
  2. 开启一个 readData 协程,从管道 intChan 中读取 writeData 写入的数据。
  3. 注意:writeData 和 readData 操作的是同一个管道。
  4. 主线程需要等待 writeData 和 readData 协程都完成工作才能退出。

思路分析:

在这里插入图片描述

代码实现:

package main

import "fmt"

// 写入数据
func writeData(intChan chan int) {
	for i := 0; i < 50; i++ {
		intChan <- i
		fmt.Println("writeData", i)
	}

	// 写入完数据后,关闭管道
	close(intChan)
}

// 读取数据
func readData(intChan chan int, exitChan chan bool) {
	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		fmt.Println("readData", v)
	}

	// 读取完数据后,即任务完成并关闭管道
	exitChan <- true
	close(exitChan)
}

func main() {
    
	// 创建两个管道
	intChan := make(chan int, 50)
	exitChan := make(chan bool, 1)

	go writeData(intChan)
	go readData(intChan, exitChan)

	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}
}

问题:如果注销掉 go readData(intChan, exitChan),程序会怎么样?

答:如果只是向管道写入数据,而没有读取,就会出现阻塞报错 fatal error: all goroutines are asleep - deadlock!,原因是 intChan 容量是 10,而代码 writeData 会写入 50 个数据,因此会阻塞再 writeData 的 intChan <- i

实例2

统计 1-8000 的数字中,哪些是素数?

思路分析:

传统的方法,就是使用一个循环,循环的判断各个数是不是素数,完成任务时间长。

使用并发/并行的方式,将统计素数的任务分配给多个(4个) goroutine 去完成,完成任务时间短。

代码实现:

package main

import (
	"fmt"
)

func putNum(intChan chan int) {
	for i := 0; i < 8000; i++ {
		intChan <- i
	}

	close(intChan)
}

func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	var flag bool
	for {
		num, ok := <-intChan
		if !ok {
			break
		}

		// 假设是素数
		flag = true

		// 判断 num 是不是素数
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}

		if flag {
			// 将这个数放入到 primeChan
			primeChan <- num
		}
	}

	exitChan <- true
}

func main() {
    
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 2000)
	exitChan := make(chan bool, 4)

	go putNum(intChan)
	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}

	go func() {
		for i := 0; i < 4; i++ {
			<-exitChan
		}
		close(primeChan)
	}()

	for {
		res, ok := <-primeChan
		if !ok {
			break
		}
		fmt.Println(res)
	}
}

结论:使用 Go 协程后,执行的速度,比普通方法提高至少 4 倍。

实例3

channel 可以声明为只读,或者只写性质。

package main

import "fmt"

// ch chan<- int 声明只写操作
func send(ch chan<- int, exitChan chan struct{}) {
	for i := 0; i < 10; i++ {
		ch <- i
	}
	close(ch)
	var a struct{}
	exitChan <- a
}

func recv(ch <-chan int, exitChan chan struct{}) {
	for {
		v, ok := <-ch
		if !ok {
			break
		}
		fmt.Println(v)
	}
	var a struct{}
	exitChan <- a
}

func main() {
	var ch chan int
	ch = make(chan int, 10)
	exitChan := make(chan struct{}, 2)

	go send(ch, exitChan)
	go recv(ch, exitChan)

	var total = 0
	for range exitChan {
		total++
		if total == 2 {
			break
		}
	}
}

实例4

goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题。

说明:如果我们起了一个协程,但是这个协程出现了 panic,如果我们没有捕获这个 panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover 来捕获 panic 进行处理,这样即使这个协程发生了问题,但是主线程仍然不受影响,可以继续执行。

package main

import (
	"fmt"
	"time"
)

func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello,world!")
	}
}

func test() {
	defer func() {
		// 捕获 test 抛出的 panic
		if err := recover(); err != nil {
			fmt.Println("test() 发生错误", err)
		}
	}()

	var myMap map[int]string
	myMap[0] = "golang" // error
}

func main() {
	go sayHello()
	go test()

	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值