天天看点

Go--关于 goroutine、channel

Go--关于 goroutine、channel

goroutine

协程是一种轻量化的线程,由

Go

编译器进行优化。

Go

协程具有以下特点:

  • 有独立的栈空间
  • 共享程序堆中的空间
  • 调度由用户控制

如果主线程

main

函数(主

goroutine

或者

main goroutine

)返回或者退出时,即使所有协程(

goroutine

)还没执行完毕,也会退出。当然,协程可以在主线程未退出之前自己执行完毕,并退出。

  • 主线程是一个物理线程,直接作用在

    cpu

    上。是重量级的,非常耗费

    cpu

    资源。
  • 协程从主线程开启的,是轻量级的线程,是逻辑态的。对资源要求相对较小。
  • Golang

    可以开启成千上万个协程。这是

    Golang

    的并发优势。

MPG模式

Go--关于 goroutine、channel
Go--关于 goroutine、channel
Go--关于 goroutine、channel
  • Go

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

    1.8前,还是要设置一下,可以更高效的利益

    cpu

numsCPU :=runtime.NumCPU()  //获取系统CPU数
	runtime.GOMAXPROCS(numsCPU)  //设置运行的CPU数目
           

channel

在此之前,先说明一种实现同步的方式:加锁(注意这里说的指互斥锁)

需求:计算

n!

var lock sync.Mutex //使用全局变量加锁

func testInput(n int)  {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	lock.Lock()
	myMap[n] = uint64(res)
	lock.Unlock()
}

func main() {
	for i := 1; i <= 50; i++ {
		go testInput(i)  
	}

	time.Sleep(time.Second *5)  //不等待会提前结束计算,未计算的线程将被退出
    
	lock.Lock()
	for i,v := range myMap {
		fmt.Println("map[",i,"]=",v)
	}
	lock.Unlock()
}
           

通过加互斥锁(同步锁)的方式,并发进行运算、添加,但是这种方式也有缺点:

  • 前面使用全局变量加锁同步来解决

    goroutine

    的通讯,但不完美
  • 主线程在等待所有

    goroutine

    全部完成的时间很难确定,我们这里设置 5 秒,仅仅是估算。
  • 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有

    goroutine

    处于工作状态,这时也会随主线程的退出而销毁。
  • 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。

还可以用

channel

来解决:

func add(s []int , c chan int)  {
	sum := 0
	for _, v := range s {
		sum += v
		fmt.Println(v)
	}
	c <- sum
}

func main() {
	c := make(chan int)
	s :=[]int{2,5,9,23,7,3,4}
	go add(s[:len(s)/2  ] ,c)  //写channel操作会阻塞,直到读channel操作执行

	go add( s[len(s)/2:] ,c)

	x ,y:= <-c ,<-c   //随机并发
	fmt.Println(x ,y ,x+y)
}
           

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。
           

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建:

ch := make(chan int)
           

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得

Go

程可以在没有显式的锁或竞态变量的情况下进行同步。

  • channel

    是线程安全的;
  • channel

    本质是队列,遵循先进先出;
  • channel

    中只能存放指定的数据类型;
  • channel

    的数据放满后,就不能再放入;
  • 如果从

    channel

    取出数据后,可以继续放入;
  • 在没有使用协程的情况下,如果

    channel

    数据取完了,再取,就会报

    deadlock

还可以放进任意类型(

interface{}

)的数据:

package main

import "fmt"

type Student struct {
	Name string	`json:"name"`  // 是 ` ` (tab键上的~按键) ,不是 ' '
	Sex string `json:"sex"`
}

func main() {
	allChan := make(chan interface{},5)

	stu1 := Student{Name: "lili",Sex: "f"}
	stu2 := Student{Name: "chang",Sex: "m"}
	stu3 := Student{Name: "ling",Sex: "m"}

	allChan <- stu1
	allChan <- 10
	allChan <- stu2
	allChan <- 99.5
	allChan <- stu3

	<- allChan
	<- allChan
	stuRes := <- allChan
	fmt.Println(stuRes)

	//读取结构体类型数据字段,需要先进行类型断言
	stu := stuRes.(Student)
	fmt.Println(stu.Name)
	fmt.Println(stu.Sex)
}
           

由于

channel

interface{}

类型,所以使用的时候,都需要先进行类型断言。

allChan <- myMap
	<- allChan
	<- allChan
	stuMap := <- allChan
	stus := stuMap.(map[int]Student)
	fmt.Println(stus)
	fmt.Println(stus[0])
	fmt.Println(stus[1])
           
Go--关于 goroutine、channel
allChan <- stu1
	allChan <- 10
	allChan <- stu2
	allChan <- 99.5
	allChan <- stu3

	<- allChan
	n := <- allChan
	n += 1  //报错
           
Go--关于 goroutine、channel

不使用类型断言,直接使用将会报错。因为编译器并不认识此类型,需要经过类型断言进行确认。

channel的关闭

channel

关闭使用

close(chan)

,关闭

channel

关闭后不能再向

channel

发送数据,只能从

channel

读取数据。

在上面的例子中的

<-allChan

前加入以下代码:

close(allChan)
           
Go--关于 goroutine、channel

channel的遍历

遍历

channel

之前需要关闭

channel

,否则会报错(

deadlock

)。

Go--关于 goroutine、channel

关闭

channel

后,即可正常进行遍历

channel

,知道遍历完成,退出遍历。

close(allChan)
	for v := range allChan {
		fmt.Println(v)
	}
           

只读、只写 channel

  • 只读

    channel

    :(例如)
var chan1 <-chan int
           
  • 只写

    channel

    :(例如)
var chan2 chan<- int
           

案例:

func send(ch chan<- int,exit chan  struct{})  {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("输入",i)
	}
	close(ch)
	var  a struct{}
	exit <- a
}

func get(ch <-chan int,exit chan struct{})  {
	for i := 0; i < 10; i++ {
		v,ok := <-ch
		if !ok {
			break
		}
		fmt.Println("输出",v)
	}
	var  a struct{}
	exit <- a
}

func main() {
	ch := make(chan int ,10)  //双向通道
	exitChan := make(chan struct{} ,2)
	go send(ch,exitChan)
	go get(ch,exitChan)
	for {
		if len(exitChan) == 2 {
			break
		}
	}
}
           
Go--关于 goroutine、channel

可以使用

for

+

select

语句防止阻塞:

func main() {
	ch := make(chan int ,10)
	for i := 0; i < 5; i++ {
		ch <- i
	}

	ch2 := make(chan float64 ,10)
	for i := 0; i < 5; i++ {
		ch2 <- rand.Float64()
	}
	label:
	for  {
		select {
		case n:= <-ch:
			fmt.Println(n)
			time.Sleep(time.Second)
		case m:=<-ch2:
			fmt.Println(m)
			time.Sleep(time.Second)
		default:
			fmt.Println("没了")
			time.Sleep(time.Second)
			//return  直接结束退出程序运行
			break label   //中断指定的 for 循环
		}
	}
}
           
Go--关于 goroutine、channel

使用

defer

+

recover

解决运行时协程中抛出的

panic

,保证程序继续运行:

func wrong()  {
	defer func(){
		if e := recover() ; e != nil {
			fmt.Print("func wrong()计算错误,")
			fmt.Println("异常",e)
		}
	}()

	num := 0
	num1 := 100
	num = num1 /num
	fmt.Println("func wrong()计算正确",num)
}

func right()  {
	defer func(){
		if e := recover() ; e != nil {
			fmt.Println(e)
		}
	}()

	num := 10
	num1 := 100
	num = num1 /num
	fmt.Println("func right() 计算正确",num)
}

func main() {
	go wrong()
	go right()
	for i := 0; i < 5; i++ {
		time.Sleep(time.Second)
		i++
	}
}
           
Go--关于 goroutine、channel

关于

recover

:

内建函数recover允许程序管理恐慌过程中的Go程。在defer的函数中,执行recover调用会取回传至panic调用的错误值,恢复正常执行,停止恐慌过程。若recover在defer的函数之外被调用,它将不会停止恐慌过程序列。在此情况下,或当该Go程不在恐慌过程中时,或提供给panic的实参为nil时,recover就会返回nil。