zoukankan      html  css  js  c++  java
  • Golang 关于 goroutine、channel

    Go--关于 goroutine、channel

    goroutine

    协程是一种轻量化的线程,由Go编译器进行优化。

    Go协程具有以下特点:

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

    如果主线程main函数(主 goroutine或者main goroutine)返回或者退出时,即使所有协程(goroutine)还没执行完毕,也会退出。当然,协程可以在主线程未退出之前自己执行完毕,并退出。


    • 主线程是一个物理线程,直接作用在cpu上。是重量级的,非常耗费cpu资源。
    • 协程从主线程开启的,是轻量级的线程,是逻辑态的。对资源要求相对较小。
    • Golang可以开启成千上万个协程。这是Golang的并发优势。

    MPG模式

    image-20201101201149692

    image-20201101201058123

    image-20201101201034227


    • Go1.8后,默认让程序运行在多个核上,可以不用设置了
    • Go1.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)
    }
    

    由于channelinterface{}类型,所以使用的时候,都需要先进行类型断言。

    	allChan <- myMap
    	<- allChan
    	<- allChan
    	stuMap := <- allChan
    	stus := stuMap.(map[int]Student)
    	fmt.Println(stus)
    	fmt.Println(stus[0])
    	fmt.Println(stus[1])
    

    image-20201101235011061

    	allChan <- stu1
    	allChan <- 10
    	allChan <- stu2
    	allChan <- 99.5
    	allChan <- stu3
    
    	<- allChan
    	n := <- allChan
    	n += 1  //报错
    

    image-20201102124504467

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


    channel的关闭

    channel关闭使用 close(chan),关闭channel

    关闭后不能再向channel发送数据,只能从channel读取数据。

    在上面的例子中的<-allChan加入以下代码:

    close(allChan)
    

    image-20201102130028586


    channel的遍历

    遍历channel之前需要关闭channel,否则会报错(deadlock)。

    image-20201102130608550

    关闭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
    		}
    	}
    }
    

    image-20201102144957098ddd


    可以使用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 循环
    		}
    	}
    }
    

    image-20201102144957098


    使用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++
    	}
    }
    

    image-20201102151030206

    关于recover:

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


  • 相关阅读:
    [算法]位运算问题之二
    [算法]位运算问题之一
    [算法]海量数据问题之二
    [算法]海量数据问题之一
    [算法]旋转词问题
    [算法]去掉字符串中连续出现的k个0子串
    [算法]字符串中数字子串的求和
    [算法]字符串之变形词问题
    Linux常用命令
    数据库中的事物
  • 原文地址:https://www.cnblogs.com/l1ng14/p/13914593.html
Copyright © 2011-2022 走看看