欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

go语言系列-从Goroutine到Channel

程序员文章站 2022-09-26 23:43:27
Golang语言的核心特色 [TOC] Goroutine 基本介绍 进程和线程介绍 1) 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位 2) 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位 3) 一个进程可以创建和销毁多个线程,同 ......

golang语言的核心特色

goroutine

基本介绍

进程和线程介绍

  1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行

  4. 一个程序至少有一个进程,一个进程至少有一个线程

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

go语言系列-从Goroutine到Channel

并发和并行

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

  2. 多线程程序在多核上运行,就是并行
    go语言系列-从Goroutine到Channel
    并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

go协程和go主线程

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

go协程的特点

​ 1) 有独立的栈空间

​ 2) 共享程序堆空间

​ 3) 调度由用户控制

​ 4) 协程是轻量级的线程
go语言系列-从Goroutine到Channel

快速入门

案例说明

编写一个程序,完成如下功能:

​ 1) 在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出“hello,world”

​ 2) 在主线程中也每隔一秒输出“hello,world”,输出10次后,退出程序

​ 3) 要求主线程和goroutine同时执行

画出主线程和协程执行流程图
go语言系列-从Goroutine到Channel

import (
   "fmt"
   "strconv"
   "time"
)
//编写一个函数/每隔一秒输出"hello,world"
func test()  {
   for i := 1; i <= 10; i++ {
      fmt.println("test() hello,world" + strconv.itoa(i))
      time.sleep(time.second)
   }
}
func main()  {
   go test() //开启了一个协程
   for i := 1; i <= 10; i++ {
      fmt.println("main() hello,world" + strconv.itoa(i))
      time.sleep(time.second)
   }
}
//main() hello,world1        //main主线程和test协程同时执行
//test() hello,world1
//main() hello,world2
//test() hello,world2
//......

小结

  1. 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源

  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小

  3. go的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就突显了go在并发上的优势了

goroutine的调度模型

  1. m:操作系统的主线程(是物理线程)

  2. p:协程执行需要的上下文

  3. g:协程
    go语言系列-从Goroutine到Channel

mpg模式运行的状态 -1

go语言系列-从Goroutine到Channel

  1. 当前程序有三个m,如果三个m都在一个cpu上运行,就是并发,如果在不同的cpu上运行就是并行

  2. m1,m2,m3正在执行一个g,m1的协程队列有三个,m2的协程队列有三个,m3的协程队列有两个

  3. 从上图可以看到:go的协程是轻量级的线程,是逻辑态的,go可以容易的起上万个协程

  4. 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu

mpg模式运行的状态 - 2

go语言系列-从Goroutine到Channel

  1. 分成两个部分来看

  2. 原来的情况是mo主线程正在执行go协程,另外有三个协程在队列等待

  3. 如果go协程阻塞,比如读取文件或者数据库等

  4. 这时就会创建m1主线程(也可能是从已有的线程池中取出m1),并且将等待的3个协程挂到m1下开始执行,m0的主线程下的go仍然执行文件io的读写

  5. 这样的mpg调度模式,可以既让go执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行

  6. 等到go不阻塞了,m0会被放到空闲的主线程继续执行(从已有的线程池中取),同时go又会被唤醒

设置go运行的cpu数

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

import (
   "fmt"
   "runtime"
)

func main()  {
   //获取当前系统cpu的数目
   num := runtime.numcpu()
   //这里设置num - 1的cpu运行go程序
   runtime.gomaxprocs(num - 1)
   fmt.println("num = ", num)
}

go1.8后,默认让程序运行在多核上,可以不用设置
go1.8前,还是要设置一下,可以更高效的利用cpu

channel(管道)

看个需求

需求:现在要计算1 - 200 的各个数的阶乘,并且把各个数的阶乘放入到map中,最后显示出来

要求:使用goroutine

分析思路

​ 1) 使用goroutine来完成,效率高,但是会出现并发/并行安全问题

​ 2) 这里就提出了不同goroutine如何通信的问题

代码区

​ 1) 使用goroutine来完成(看看使用goroutine并发完成会出现什么问题?然后再去解决)

​ 2) 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数 - race 即可

示意图
go语言系列-从Goroutine到Channel

import (
	"fmt"
	"time"
)
//思路
//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\n", i, v)
	}
}
//fatal error: concurrent map writes
//
//goroutine 55 [running]:
//runtime.throw(0x4d6d6d, 0x15)
//	e:/go/go/src/runtime/panic.go:774 +0x79 fp=0xc0000eff60 sp=0xc0000eff30 pc=0x42d229
//runtime.mapassign_fast64(0x4b6240, 0xc00005c330, 0x31, 0x0)
//	e:/go/go/src/runtime/map_fast64.go:101 +0x357 fp=0xc0000effa0 sp=0xc0000eff60 pc=0x410167
//main.test(0x31)
//	e:/gostudent/src/2020-04-06/main.go:21 +0x6b fp=0xc0000effd8 sp=0xc0000effa0 pc=0x49c72b
//runtime.goexit()
//	e:/go/go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc0000effe0 sp=0xc0000effd8 pc=0x4556a1
//created by main.main
//	e:/gostudent/src/2020-04-06/main.go:26 +0x5f
//
//goroutine 1 [runnable]:
//time.sleep(0x2540be400)
//	e:/go/go/src/runtime/time.go:84 +0x248
//main.main()
//	e:/gostudent/src/2020-04-06/main.go:29 +0x82

不同goroutine之间如何通讯

  1. 全局变量的互斥锁

  2. 使用管道channel来解决

使用全局变量加锁同步改进程序

因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes

解决方案:加入互斥锁

数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)

代码区改进

package main

import (
	"fmt"
	"sync"
	"time"
)
//思路
//1. 编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的协程多个,统计的结果放入到map中
//3. map应该做出一个全局的
var (
	mymap = make(map[uint]uint,10)
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁
	//sync 是包:synchornized 同步
	//mutex :是互斥
	lock sync.mutex
)
//test函数就是计算n!,将这个结果放入到mymap
func test(n uint)  {
	var res uint = 1
	var i uint = 1
	for ; i <= n; i++ {
		res *= i
	}
	//这里将res 放入到mymap
	//加锁
	lock.lock()
	mymap[n] = res // concurrent map writes?
	//解锁
	lock.unlock()
}
func main()  {
	//这里开启多个协程完成这个任务[200个]
	var i uint = 1
	for ; i <= 200; i++ {
		go test(i)
	}
	//休眠10秒钟【第二个问题】
	time.sleep(time.second * 10)
	//这里输出结果,遍历这个结果
	lock.lock()
	for i, v := range mymap {
		fmt.printf("map[%d] = %d\n", i, v)
	}
	lock.unlock()
}

需求注意的是:uint64最大到20的阶乘,大整数可以使用math/big 来进行  实例:https://blog.csdn.net/hudmhacker/article/details/90081630

为什么需要channel

  1. 前面使用全局变量加锁同步来解决goroutine的通讯,但不完美

  2. 主线程在等待所有gorountine全部完成的时间很难确定,这里设置了10秒,仅仅是估算

  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁

  4. 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作

  5. 上面种种分析都在呼唤一个新的通讯机制 - channel

channel的基本介绍

  1. channel本质就是一个数据结构 - 队列

  2. 数据是先进先出【fifo :first int first out】

  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的

  4. channel有类型的,一个string的channel只能存放string类型数据
    go语言系列-从Goroutine到Channel

定义/声明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

​ ....

说明

​ 1) channel是引用类型

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

​ 3) 管道是有类型的,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\n", intchan, &intchan)
	//3. 向管道写入数据
	intchan <- 10
	num := 211
	intchan <- num
	intchan <- 50
	//intchan <- 99 //当给管道写入数据时,不能超过其容量
	//4. 看看管道的长度和cap(容量)
	fmt.printf("channel len = %v cap = %v \n", len(intchan), cap(intchan))
	//5. 从管道中读取数据
	var num2 int
	num2 = <- intchan
	fmt.println("num2 = ", num2)
	fmt.printf("channel len = %v cap = %v \n", len(intchan), cap(intchan))
	//6. 在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报告deadlock
	num3 := <- intchan
	num4 := <- intchan
	num5 := <- intchan
	fmt.printf("num3 = %v num4 = %v num5 = %v ", num3, num4, num5)
}
//fatal error: all goroutines are asleep - deadlock!
//intchan 的值 = 0xc000090000 intchan本身的地址 = 0xc00008a018
//channel len = 3 cap = 3 
//num2 =  10
//channel len = 2 cap = 3 
//
//goroutine 1 [chan receive]:
//main.main()
//	e:/gostudent/src/2020-04-06/main.go:28 +0x4d4

channel使用的注意事项

  1. channel 中只能存放指定的数据类型

  2. channel 的数据放满后,就不能再放入了

  3. 如果从channel取出数据后,可以继续放入

  4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock

读写channel案例演示

  1. 创建一个intchan,最多可以存放3个int,演示存3个数据到intchan,然后再取出这三个int
func main()  {
   var intchan chan int
   intchan = make(chan  int, 3)
   intchan <- 10
   intchan <- 20
   intchan <- 10
   //因为intchan 的容量为3,再存放会报告deadlock
   //intchan <- 50
   num1 := <- intchan
   num2 := <- intchan
   num3 := <- intchan
   //因为intchan 这时已经没有数据了,再取会报告deadlock
   //num4 := <- intchan
   fmt.printf("num1 = %v num2 = %v num3 = %v", num1, num2, num3)
}
//num1 = 10 num2 = 20 num3 = 10
  1. 创建一个mapchan,最多可以存放10个map[string]string的key-val,演示写入和读取
func main() {
   var mapchan chan map[string]string
   mapchan = make(chan map[string]string, 2)
   m1 := make(map[string]string, 2)
   m1["city1"] = "北京"
   m1["city2"] = "天津"
   m2 := make(map[string]string, 2)
   m2["hero1"] = "宋江"
   m2["hero2"] = "林冲"
   mapchan <- m1
   mapchan <- m2
   num1 := <- mapchan
   num2 := <- mapchan
   fmt.printf("num1 = %v num2 = %v", num1, num2)
}
//num1 = map[city1:北京 city2:天津] num2 = map[hero1:宋江 hero2:林冲]
  1. 创建一个catchan,最多可以存放10个cat结构体变量,演示写入和读取的用法
type cat struct{
   name string
   age int
}
func main() {
   var catchan chan cat
   catchan = make(chan cat, 10)
   cat1 := cat{name: "tom", age: 18,}
   cat2 := cat{name: "zise", age: 18,}
   catchan <- cat1
   catchan <- cat2
   //取出
   cat11 := <- catchan
   cat22 := <- catchan
   fmt.println(cat11, cat22)
}
//{tom 18} {zise 18}
  1. 创建一个catchan2,最多可以存放10个*cat变量,演示写入和读取的用法
type cat struct{
   name string
   age int
}
func main() {
   var catchan chan *cat
   catchan = make(chan *cat, 10)
   cat1 := cat{name: "tom", age: 18,}
   cat2 := cat{name: "zise", age: 18,}
   catchan <- &cat1
   catchan <- &cat2
   //取出
   cat11 := <- catchan
   cat22 := <- catchan
   fmt.println(*cat11, *cat22)
}
//{tom 18} {zise 18}
  1. 创建一个allchan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
type cat struct {
   name string
   age int
}

func main()  {
   var allchan chan interface{}
   allchan = make(chan interface{}, 10)
   cat1 := cat{name: "tom", age: 18}
   cat2 := cat{name: "zise", age: 18}
   allchan <- cat1
   allchan <- cat2
   allchan <- 10
   allchan <- "jack"
   //取出
   cat11 := <- allchan
   cat22 := <- allchan
   v1 := <- allchan
   v2 := <- allchan
   fmt.println(cat11, cat22, v1, v2)
}
//{tom 18} {zise 18} 10 jack
  1. 看下面的代码,会输出什么
type cat struct {
   name string
   age int
}

func main()  {
   var allchan chan interface{}
   allchan = make(chan interface{}, 10)
   cat1 := cat{name: "tom", age: 18}
   cat2 := cat{name: "zise", age: 18}
   allchan <- cat1
   allchan <- cat2
   allchan <- 10
   allchan <- "jack"
   //取出
   //cat11 := <- allchan
   //fmt.println(cat11.name)
   // # command-line-arguments
   //src\go_code\chapter15\exec03\test03.go:23:19: cat11.name undefined (type interface {} is interface with no methods)
   newcat := <- allchan //从管道中取出的cat是什么
   fmt.printf("newcat = %t newcat = %v \n", newcat, newcat)
   //下面写法是错误的,编译不通过
   //fmt.printf("newcat.name = %v", newcat.name)
   //使用类型断言
   a := newcat.(cat)
   fmt.printf("newcat.name = %v", a.name)
}
//newcat = main.cat newcat = {tom 18} 
//newcat.name = tom

channel的遍历和关闭

channel的关闭

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

func main()  {
   intchan := make(chan int, 3)
   intchan <- 100
   intchan <- 200
   close(intchan) //close
   //这时不能够再写入数到channel
   //intchan <- 300
   fmt.println("oko")
   //当管道关闭后,读取数据是可以的
   n1 := <- intchan
   fmt.println("n1 = ", n1)
}
//oko
//n1 =  100

channel的遍历

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

  1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误

  2. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

channel遍历和关闭的案例演示

func main()  {
     //遍历管道
   intchan2 := make(chan int, 100)
   for i := 0; i < 100; i++ {
      intchan2 <- i *2  //放入100个数据到管道
   }
   //遍历管道不能使用普通的for循环
   //for i := 0; i < len(intchan2); i++ {
   //
   //}
   //1)在遍历时,如果channel没有关闭,则会出现deadlock的错误
   //2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
   close(intchan2)
   for v := range intchan2 {
      fmt.println("v = ", v)
   }
}

应用案例

应用案例-利于管道实现边写边读

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

  1. 开启一个writedata协程,向管道intchan中写入50个整数

  2. 开启一个readdata协程,从管道intchan中读取writedata写入的数据

  3. 注意:writedata和readdata操作的是同一个管道

  4. 主线程需要等待writedata和readdata协程都完成工作才能退出【管道】

思路分析
go语言系列-从Goroutine到Channel
代码区

import (
   "fmt"
   "time"
)
//writedata
func writedata(intchan chan int)  {
   for i := 1; i <= 50; i++ {
      //放入数据
      intchan <- i
      fmt.println("writedata", i)
      time.sleep(time.second)
   }
   close(intchan) //关闭
}
//readdata
func readdata(intchan chan int, exitchan chan bool)  {
   for {
      v, ok := <- intchan
      if !ok {
         break
      }
      time.sleep(time.second)
      fmt.printf("readdata 读到数据 = %v\n", 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
      }
   }
}

go语言系列-从Goroutine到Channel

var (
    mymap = make(map[int]int, 10)
)

func cal(n int) map[int]int {
    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }
    mymap[n] = res
    return mymap
}

func write(mychan chan map[int]int) {
    for i := 0; i <= 15; i++ {
        mychan <- cal(i)
        fmt.println("writer data:", cal(i))
    }
    close(mychan)
}

func read(mychan chan map[int]int, exitchan chan bool) {
    for {
        v, ok := <-mychan
        if !ok {
            break
        }
        fmt.println("read data:", v)
    }
    exitchan <- true
    close(exitchan)
}

func main() {
    var mychan chan map[int]int
    mychan = make(chan map[int]int, 20)
    var exitchan chan bool
    exitchan = make(chan bool, 1)
    go write(mychan)
    go read(mychan, exitchan)
    for {
        _, ok := <-exitchan
        if !ok {
            break
        }
    }
}

应用案例 - 阻塞

go语言系列-从Goroutine到Channel
思考:假设我们注销掉go read(mychan,exitchan)会发生什么呢?

也就是说,只有写入mychan而没有读取mychan,当存入mychan里面的数据达到了mychan的容量,再继续存入就会报deadlock错误。同时,由于exitchan需要写入一个true,而exitchan需要读取完mychan中的数据后才写入一个true,但是现在不能进行读取,也就是说,true不会写入exitchan,就形成了阻塞。假设我们打开go read(mychan,exitchan),我们设置其每隔1秒才读取一条数据,而写入则让其正常运行,也就是说,写入很快,读取很慢,这样会导致deadlock吗?答案是不会,只要有读取,golang会有个机制,不会让mychan存储的值超过mychan的容量。

应用案例-求素数

需求

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

​ 现在具备了goroutine和channel的知识后,就可以完成了

分析思路

​ 传统的方法:使用一个循环,循环的判断各个数是不是素数

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

画出分析思路
go语言系列-从Goroutine到Channel

说明:有五个协程,三个管道。其中一个协程用于写入数字到intchan管道中,另外四个用于取出intchan管道中的数字并判断是否是素数,然后将素数写入到primechan管道中,最后如果后面四个协程哪一个工作完了,就写入一个true到exit管道中,最后利用循环判断这四个协程是否都完成任务,并退出

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 num int
	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, 200000)
	primechan := make(chan int, 200000) //放入结果
	//标识退出的管道
	exitchan := make(chan bool, 4) // 4个
	//开启一个协程,向intchan放入1 - 200000个数
	go putnum(intchan)
	//开启四个协程,从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\n", res)
	}
	fmt.println("main线程退出")
}

升级

package main

import (
	"fmt"
	"time"
)

func isprime(n int) bool {
	for i := 2; i <= n; i++ {
		if n%i == 0 {
			return false
		}
	}
	return true
}

//传统方法耗时
func test() {
	start := time.now()
	for i := 1; i < 80000; i++ {
		isprime(i)
	}
	cost := time.since(start)
	fmt.printf("传统方法消耗时间为:%s", cost)
}

//向intchan放入1 - 80000个数
func putnum(intchan chan int)  {
	for i:= 1; i <= 80000; i++ {
		intchan <- i
	}
	//关闭intchan
	close(intchan)
}
//从intchan取出数据,并判断是否为素数,如果是,就放入到primechan
func primenum(intchan chan int, primechan chan int, exitchan chan bool)  {
	//使用for循环
	//var num int
	//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
	//	}
	//}
		isp := isprime(num)
		if !isp {
			continue
		} else {
			primechan <- num
		}
	}
	fmt.println("有一个primenum协程因为取不到数据,退出")
	//这里还不能关闭primechan
	//向exitchan 写入true
	exitchan <- true
}
func main()  {
	intchan := make(chan int, 200000)
	primechan := make(chan int, 200000) //放入结果
	//标识退出的管道
	exitchan := make(chan bool, 4) // 4个
	//记录当前时间
	start := time.now()
	//开启一个协程,向intchan放入1 - 200000个数
	go putnum(intchan)
	//开启四个协程,从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
		//计算耗时时间
		cost := time.since(start)
		fmt.printf("使用协程耗费时间:%s\n", cost)
		close(primechan)
	}()
	//遍历primechan,把结果取出
	for {
		_, ok := <- primechan
		if !ok {
			break
		}
		//将结果输出
		//fmt.printf("素数 = %d\n", res)
	}
	fmt.println("main线程退出")
	test()
}
//有一个primenum协程因为取不到数据,退出
//有一个primenum协程因为取不到数据,退出
//有一个primenum协程因为取不到数据,退出
//有一个primenum协程因为取不到数据,退出
//使用协程耗费时间:876.6558ms
//main线程退出
//传统方法消耗时间为:3.3300976s

channel使用细节和注意事项

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

func main()  {
   //管道可以声明为只读或者只写
   //1. 在默认情况下,管道是双向
   //var chan1 chan int //可读可写
   //2. 声明为只写
   var chan2 chan <- int
   chan2 = make(chan int, 3)
   chan2 <- 20
   //num := <- chan2 //error
   fmt.println("chan2 = ", chan2)
   //3. 声明为只读
   var chan3 <- chan  int
   num2 := <- chan3
   //chan3 <- 30 //err
   fmt.println("num2", num2)
}

channel只读和只写的最佳实践案例

//ch chan <- int 这样ch就只能写操作了
func send(ch chan <- int, exitchan chan struct{})  {
   for i := 0; i < 10; i++ {
      ch <- i
   }
   close(ch)
   var a struct{}
   exitchan <- a
}
//ch <- chan int ,这样ch 就只能读操作了
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
      }
   }
   fmt.println("结束...")
}

使用select可以解决从管道取数据的阻塞问题

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\n", v)
         time.sleep(time.second)
      case v := <-stringchan:
         fmt.printf("从stringchan读取的数据%s\n", v)
         time.sleep(time.second)
      default:
         fmt.printf("都取不到了,不玩了,程序员可以加入逻辑\n")
         time.sleep(time.second)
         return
         //break label
      }
   }
}

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

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

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)
   }
}
//main() ok= 0
//test() 发生错误 assignment to entry in nil map
//hello,world
//main() ok= 1
//hello,world
//main() ok= 2
//hello,world
//main() ok= 3
//hello,world
//main() ok= 4
//hello,world
//main() ok= 5
//hello,world
//main() ok= 6
//hello,world
//main() ok= 7
//hello,world
//main() ok= 8
//hello,world
//main() ok= 9
//hello,world

管道的练习题

说明:

  1. 创建一个person结构体[name,age,address]

  2. 使用rand方法配合随机创建10个person实例,并放入到channel中

  3. 遍历channel,将各个person实例的信息显示在终端...