Golang学习+深入(十三)-goroutine/channal

114 阅读9分钟

一、goroutine(协程)

进程和线程说明

  1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
  4. 一个程序至少有一个进程,一个进程至少有一个线程

并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行
  3. 并发: 因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
  4. 并行: 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。

1、goroutine

  1. Go主线程(可理解为进程):一个Go线程上可以起多个协程。协程是轻量级的线程
  2. Go协程的特点
  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程
package main
import (
	"fmt"
	"strconv"
	"time"
)

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,golang"+strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
//=================================
D:\GO_WORKSPACE\src\go_code\project03\day06>go run main.go
main()..hello,golang1
test()..hello,world!1
test()..hello,world!2
main()..hello,golang2
main()..hello,golang3
test()..hello,world!3
test()..hello,world!4
main()..hello,golang4
main()..hello,golang5
test()..hello,world!5
test()..hello,world!6
main()..hello,golang6
main()..hello,golang7
test()..hello,world!7
test()..hello,world!8
main()..hello,golang8
main()..hello,golang9
test()..hello,world!9
  1. 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。

2、MPG模式(goroutine的调度模型)

  1. M:操作系统的主线程(是物理线程)
  2. P:协程执行需要的上下文
  3. G:协程

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

3、设置Golang运行的cpu数

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

package main
import (
	"fmt"
	"runtime"
)

func main(){
	//获取当前系统CPU的数量
	num := runtime.NumCPU()
	runtime.GOMAXPROCS(num)//设置cpu数量运行go程序
	fmt.Println("num=",num)
}

4、资源争抢

引出案例

package main
import (
	"fmt"
)
//需求:计算1-200的各个数的阶乘,并且把各个数的阶乘放到map中
//最后打印出来。要求使用goroutine

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

func test(n int){
	res := 1
	for i:=1;i<=n;i++{
		res *=i
	}
	mmap[n]=res
}

func main(){
	
	for i:=1;i<=200;i++{
		go test(i)
	}
	
	for i,v := range mmap{
		fmt.Printf("mmap[%d]=%d",i,v)
	}
	
}
===========================
D:\GO_WORKSPACE\src\go_code\project03\day07>go run main.go
fatal error: concurrent map writes
.....

4.1、全局互斥锁解决资源竞争

import "sync"
sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,
大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
package main
import (
	"fmt"
	"sync"
	"time"
)
//需求:计算1-200的各个数的阶乘,并且把各个数的阶乘放到map中
//最后打印出来。要求使用goroutine
var (
	mmap =make(map[int]int,10)
	//声明一个全局的互斥锁
	lock sync.Mutex  //Mutex:是互斥
)

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

func main(){
	for i:=1;i<=200;i++{
		go test(i)
	}
	time.Sleep(10*time.Second)
	lock.Lock()
	for i,v := range mmap{
		fmt.Printf("mmap[%d]=%d\n",i,v)
	}
	lock.Unlock()
}
==================
结果
....
mmap[107]=0
mmap[108]=0
mmap[77]=0
mmap[189]=0
mmap[64]=-9223372036854775808
mmap[74]=0
mmap[46]=1150331055211806720
mmap[152]=0
mmap[109]=0
mmap[149]=0
mmap[183]=0
mmap[197]=0
mmap[37]=1096907932701818880
mmap[105]=0
因为阶乘超过int存储的最大范围所以上面结果是0

还可以使用下面的channel(管道)来解决资源争抢的问题

二、channel(管道)

1、channel(管道)

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel是有类型的,一个string的channel只能存放string类型数据
基本语法
    var 变量名 chan 数据类型
例:
    var intChan chan int //(intChan用于存放int数据)
    intChan = make(chan int,10) //初始化
    num:=999
    intChan<-10  //向channel写入数据
    intChan<-num //向channel写入数据
    
    var perChan chan Person 
说明:
1.channel是引用类型
2.channel必须初始化才能写入数据,即make后才能使用
3.管道是有类型的,intChan只能写入整数int
package main
import (
	"fmt"
)

func main() {
	var intChan chan int //(intChan用于存放int数据)
    intChan = make(chan int,3) //初始化
	fmt.Printf("intChan 的值=%v intChan本身的地址=%p \n",intChan,&intChan)
    num:=999
    intChan <- 10  //向channel写入数据
    intChan <- num //向channel写入数据,注意:往管道中写入数据,不能超过其容量
	fmt.Printf("intChan len=%v cap= %v \n",len(intChan),cap(intChan))
	var num2 int
	num2 = <- intChan//从管道中取出数据,注意,管道中数据全取完,再取就是报错deadlock!
	fmt.Println(num2)
	num2 = <- intChan
	fmt.Println(num2)
	num2 = <- intChan
	fmt.Println(num2)
	fmt.Printf("intChan len=%v cap= %v \n",len(intChan),cap(intChan))
}
  1. channel中只能存放指定的数据类型
  2. channel的数据存放满后,就不能再放入了
  3. 如果从channel取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报deadlock

2、channel的遍历和关闭

channel的关闭

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

func close(c chan<- Type):内建函数close关闭信道,该通道必须为双向的或只发送的。
========================
package main
import (
	"fmt"
)

func main(){
    intChan := make(chan int,3)
    intChan <- 100  
    intChan <- 200 
    close(intChan)
    n1:=<-intChan
    fmt.Println("n1=",n1)
    intChan <- 300 
    fmt.Println("ok")
}
========================
n1= 100
panic: send on closed channel

channel的遍历

channel支持for-range的方式进行遍历

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

func main(){
    intChan := make(chan int,100)
    for i:=0;i<100;i++ {
	intChan<- i*2
    }
    //遍历时,如果channel没有关闭,会出现deadlock的错误
    //close(intChan)//关闭后,遍历数据是正常的
    for v := range intChan{
	fmt.Println("v=",v)
    }
}
========================
....
v= 192
v= 194
v= 196
v= 198
fatal error: all goroutines are asleep - deadlock!
....

goroutine/channal结合

package main
import (
	"fmt"
)

func writeData(intChan chan int){
	for i :=1;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
            }
	}
}

channel阻塞

如果编译器(运行),发现一个管道,只有写,而没有读,则该管道,会阻塞。 写管道和读管道的频率不一致,不影响,无所谓。

3、管道的使用细节

  1. channel可以声明为只读,或者只写性质,默认情况下,管道是双向。
package main
import (
	"fmt"
)

func main(){
	//声明为只写
	var chan1 chan<- int
	chan1 =make(chan int,3)
	chan1<-20
	fmt.Println("chan1=",chan1)
	//num := <-chan1 //.\main.go:13:11: invalid operation: cannot receive from send-only channel chan1 (variable of type chan<- int)
	//fmt.Println("num=",num)

	//声明为只读
	var chan2 <-chan int
	num1 := <-chan2
	fmt.Println("num1=",num1)
	
}
  1. channel只读只写案例
package main
import (
	"fmt"
)

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("结束...")
}

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
}
  1. 使用select可以解决从管道取数据的阻塞问题 实际开发中,可能不好确定什么时候关闭该管道
package main
import (
	"fmt"
)

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

	stringChan := make(chan string,5)
	for i :=0;i<5;i++{
		stringChan<-"h" + fmt.Sprintf("%d",i)
	}

	label:
	for {
		select {
			//管道intChan一直不关闭,不会导致阻塞而deadlock
			//会自动到下一个case匹配
			case v:=<-intChan: 
				fmt.Printf("从intChan读取数据%d\n",v)
			case v:=<-stringChan: 
				fmt.Printf("从stringChan读取数据%s\n",v)
			default:
				fmt.Println("退出")
				break label
		}
	}
}
  1. goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

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

package main
import (
	"fmt"
	"time"
)

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


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

func test2(){
	defer func(){
		if err := recover();err!=nil {
			fmt.Println("test2() 发生错误",err)
		}
	}()
	var mmap map[int]string 
	mmap[0]="go lang"
}

干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!