Golang-goroutine/channel

Posted essaycode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang-goroutine/channel相关的知识,希望对你有一定的参考价值。

goroutine-基本介绍

进程和线程介绍

    技术图片

 

 

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

    技术图片

并发和并行

  1)多线程程序在单核上运行,就是并发
  2)多线程程序在多核上运行,就是并行
  3)示意图:

    技术图片 

    小结:

    技术图片

 

 

 

Go 协程和Go 主线程

    Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。

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

    示意图

      技术图片

 

 

   

goroutine-快速入门
   案例说明
    ?请编写一个程序,完成如下功能:
    1)在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
    2)在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
    3)要求主线程和 goroutine 同时执行.
    4)画出主线程和协程执行流程图

    技术图片

 

 

     输出的效果说明, main 这个主线程和 test 协程同时执行.

    技术图片

 

 

主线程和协程执行流程图

  技术图片

 

 

 

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


goroutine 的调度模型
  MPG 模式基本介绍

  技术图片

 

   MPG 模式运行的状态 1

  技术图片

 

   MPG 模式运行的状态 2

  技术图片

   

设置 Golang 运行的cpu 数
  介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目

  技术图片

 

 

   

  channel(管道)-看个需求
    需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。要求使用 goroutine 完成

  分析思路:
    1)使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
    2)这里就提出了不同 goroutine 如何通信的问题

  代码实现
    1)使用 goroutine 来完成(看看使用 gorotine 并发完成会出现什么问题? 然后我们会去解决)
    2)在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数 -race 即可 [示意图]

package main 
import (
	"fmt"
	"time"
)
	
	// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。
	// 最后显示出来。要求使用 goroutine 完成
	
	// 思 路
	// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.
	// 2. 我们启动的协程多个,统计的将结果放入到 map 中
	// 3. map  应该做出一个全局的.

var (
	myMap = make(map[int]int, 10)
	)
	
	// test 函数就是计算 n!, 让将这个结果放入到 myMap 
func test(n int) {
	
	res := 1
	for i := 1; i <= n; i++ {
		 res *= i
	}
	
	//这里我们将 res 放入到 myMap
	myMap[n] = res //concurrent map writes?
}
func main() {

	// 我们这里开启多个协程完成这个任务[200 个] 
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	//休眠 10 秒钟【第二个问题 】
	time.Sleep(time.Second * 10)

	//这里我们输出结果,变量这个结果
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d
", i, v)
	}

}

  示意图

 技术图片

 

 

 

不同goroutine 之间如何通讯
  1)全局变量的互斥锁
  
  2)使用管道 channel 来解决

  使用全局变量加锁同步改进程序
  因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
解决方案:加入互斥锁
  我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)
代码改进
技术图片 技术图片

 

技术图片

 

 为什么需要channel

  1)前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2)主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3)如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
  4)通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5)上面种种分析都在呼唤一个新的通讯机制-channel

channel 的基本介绍

  1)channle 本质就是一个数据结构-队列【示意图】
  2)数据是先进先出【FIFO : first in first out】
  3)线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4)channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
  5)示意图:

   技术图片

 

定义/声明 channel

  var 变量名 chan 数据类型
  举例:
  var intChan chan    int (intChan 用于存放 int 数据)
  var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
  var perChan chan    Person var  perChan2        chan        *Person
  ...
  说明
  channel 是引用类型
  channel 必须初始化才能写入数据, 即 make 后才能使用管道是有类型的,intChan 只能写入 整数 int


  管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
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
", intChan, &intChan)
	
	//3. 向管道写入数据
	intChan<- 10
	num := 211 intChan<- num intChan<- 50
	// intChan<- 98//注意点,  当我们给管写入数据时,不能超过其容量
	
	
	//4. 看看管道的长度和 cap(容量)
	fmt.Printf("channel len= %v cap=%v 
", len(intChan), cap(intChan)) // 3, 3
	
	//5. 从管道中读取数据
	var num2 int num2 = <-intChan
	fmt.Println("num2=", num2)
	fmt.Printf("channel len= %v cap=%v 
", len(intChan), cap(intChan))	// 2, 3
	//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock num3 := <-intChan
	num4 := <-intChan num5 := <-intChan

	fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}

  

channel 使用的注意事项

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

 

  读写channel 案例演示
    技术图片

 

     技术图片

 

     技术图片

 

     技术图片

 

     技术图片

 

     技术图片 技术图片

 

管道的练习题
  技术图片

 

channel 的遍历和关闭
 
  channel 的关闭
    使用内置函数close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
  案例演示:

     技术图片

 

 

  channel 的遍历

  channel 支持 for--range 的方式进行遍历,请注意两个细节
  1)在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
  2)在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

  channel 遍历和关闭的案例演示

  看代码演示:
    技术图片

 

   应用实例

    技术图片

 

   思路分析

    技术图片

 

   代码实现

    

package main 
import (
	"fmt"
	_ "time"
)
	
//write Data
func writeData(intChan chan int) { 
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan<- i
		fmt.Println("writeData ", i)
		//time.Sleep(time.Second)
	}
	close(intChan) //关闭
}
//read data
func readData(intChan chan int, exitChan chan bool) {


	for {
		v, ok := <-intChan 
		if !ok {
			break
		}
	//time.Sleep(time.Second) fmt.Printf("readData 读到数据=%v
", v)
	}
	
	//readData 读取完数据后,即任务完成
	exitChan<- true 
	close(exitChan)
	
}
func main() {

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

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

	//time.Sleep(time.Second * 10) 
	for {
		_, ok := <-exitChan 
		if !ok {
			break
		}
	}
}
	

  

应用实例 2-阻塞
  技术图片

 

 应用实例 3

  需求:
  要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine和 channel 的知识后,就可以完成了 [测试数据: 80000]
 
  分析思路:
  传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
  使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。

  画出分析思路
    技术图片

 

   代码实现

    

package main import (
	"fmt"
	"time"
	
	)
	
//向 intChan 放入 1-8000 个数
func putNum(intChan chan int) {
	
	for i := 1; i <= 8000; i++ { 
		intChan<- i
	}
	
	//关闭 intChan
	close(intChan)
}
	
	
// 从 intChan 取出数据,并判断是否为素数,如果是,就
//	//放入到 primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	
	//使用 for 循环
	var flag bool 
	for {
		time.Sleep(time.Millisecond * 10) 
		num, ok := <-intChan
		
		if !ok { //intChan 取不到.. 
			break
		}
		flag = true //假设是素数
		//判断 num 是不是素数
		for i := 2; i < num; i++ {
			if num % i == 0 {//说明该 num 不是素数
				flag = false break
			}
		}
		
		if flag {
			//将这个数就放入到 primeChan 
			primeChan<- num
		}
	}
		
	fmt.Println("有一个 primeNum  协程因为取不到数据,退出")
	//这里我们还不能关闭 primeChan
	//向 exitChan  写入 true
	exitChan<- true
}

func main() {

	intChan := make(chan int , 1000)
	primeChan := make(chan int, 2000)//放入结果
	//标识退出的管道
	exitChan := make(chan bool, 4) // 4 个
	
	//开启一个协程,向 intChan 放入 1-8000 个数
	go putNum(intChan)
	//开启 4 个协程,从 intChan 取出数据,并判断是否为素数,如果是,就
	//放入到 primeChan
	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	
	}
	
	//这里我们主线程,进行处理
	//直接
	go func(){
		for i := 0; i < 4; i++ {
			<-exitChan
		}
		//当我们从 exitChan 取出了 4 个结果,就可以放心的关闭 prprimeChan 
		close(primeChan)
	}()

	//遍历我们的 primeChan ,把结果取出
	for {
		res, ok := <-primeChan 
		if !ok{
			break
		}
		//将结果输出
		fmt.Printf("素数=%d
", res)
	}
	
	fmt.Println("main 线程退出")
}
 
结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍
 
channel 使用细节和注意事项
  1)channel 可以声明为只读,或者只写性质 【案例演示】
    技术图片
  
  2)channel 只读和只写的最佳实践案例
    技术图片
  
  3)使用 select 可以解决从管道取数据的阻塞问题
package main 
import (
	"fmt"
	"time"
)
	
func main() {
	
	//使用 select 可以解决从管道取数据的阻塞问题
	
	//1.定义一个管道 10 个数据 int 
	intChan := make(chan int, 10) 
	for i := 0; i < 10; i++ {
		intChan<- i
	}
	//2.定义一个管道 5 个数据 string 
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}
		
	//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
	
	//问题,在实际开发中,可能我们不好确定什么关闭该管道.
	//可以使用 select 方式可以解决
	//label:
	for {
		select {
		//注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock
		//,会自动到下一个 case 匹配
		case v := <-intChan :
			fmt.Printf("从 intChan 读取的数据%d
", v) time.Sleep(time.Second)
		case v := <-stringChan :
			fmt.Printf("从 stringChan 读取的数据%s
", v) time.Sleep(time.Second)
		default :
			fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑
") time.Sleep(time.Second)
			return
			//break label
		}
	}
}

  

  4)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 + recover 
	defer func() {
	//捕获 test 抛出的 panic
		if err := recover(); err != nil { 
			fmt.Println("test() 发生错误", err)
		}
	}()
	//定义了一个 map
	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)
	}
	
}

  

以上是关于Golang-goroutine/channel的主要内容,如果未能解决你的问题,请参考以下文章

R留学生作业代码代写代编程代编程代编程

IPEX-1代/3代/4代/5代,PCB天线底座,公头,样式及封装尺寸图

JVM 年轻代 老年代 持久代 gc

深圳本地网店代运营公司

C线程代业代写代调试POSIX Threads代编码

jvm中的年轻代 老年代 持久代 gc