10 goroutine和channel

go并发编程概念

几乎所有的语言都支持并发编程。go也不例外,且轻量级并发编程(协程)是Golang的重要特性。
基本概念

进程:进程是程序在操作系统中的一次执行过程。是系统进行资源分配和调度的基本单位
线程:是进程的一个执行实例,是程序的最小执行单元,是比进程更小的能独立运行的基本单元。一个进程可以创建和销毁多个线程,一个程序至少有一个进程,一个进程至少包含一个线程
并发:多线程程序在单核上运行,就是并发,某一时刻只有一个线程在执行,多个线程轮流执行(每个线程轮流执行的时间很短,宏观上看好像是多个线程同时执行)。
并行:多线程程序也在多核上运行就是并行,某一时刻 有多个线程在执行。

Go线程和协程:一个GO线程上可以起多个协程(go特有的轻量级线程)
协程的特点:有独立的栈空间、共享程序堆空间、调度由用户控制
主线程是一个物理线程,直接作用在CPU上,是重量级的,非常消耗CPU资源。协程是从主线程开启的轻量级线程。一般语言不支持协程线程开多了资源消耗会很大,但是golang协程可以轻松开启数万个。
Golang可以设置程序运行的cpu数(1.8之后默认多核运行)

num := runtime.NumCPU()
	runtime.GOMAXPROCS(num)
	fmt.Println("程序运行cpu数量:",num)

goroutine

示例代码
启动一个协程 每秒输出一句话,主线程也每秒输出一句话

func test01(){
    
    
	go sayHello()//启动协程执行逻辑

	for i:=0;i<10;i++{
    
    
		fmt.Println("主线程 ====》",i)
		time.Sleep(time.Second)
	}
}

//定义一个协程方法
//每秒输出一个 hello world 输出 10次
func sayHello(){
    
    
	for i:=0;i<10;i++{
    
    
		fmt.Println("协程 hello world!",i)
		time.Sleep(time.Second)
	}
}

执行结果,可以看到两个逻辑同时在执行

主线程 ====0
协程 hello world! 0
协程 hello world! 1
主线程 ====1
主线程 ====2
协程 hello world! 2
协程 hello world! 3
主线程 ====3
协程 hello world! 4
主线程 ====4
主线程 ====5
协程 hello world! 5
协程 hello world! 6
主线程 ====6
主线程 ====7
协程 hello world! 7
协程 hello world! 8
主线程 ====8
主线程 ====9
协程 hello world! 9

计算 1-100 之间整数的阶加,并将结果输出到 一个map 中

//示例 多协程计算 0-200 每个数字的阶乘,把结果放到map 中
// 执行完成后主线程打印结果
//声明全局变量
//不加锁时 没有出现任何异常
var(
	resMap = make(map[int]int,10)
)
func test02(){
    
    
	for i:=1;i<=100;i++{
    
    
		go nmuls(i)
	}
	time.Sleep(time.Second*10)
	fmt.Println(resMap)//打印map
	//map[85:3656 48:1177 78:3082 35:631 41:862 55:1541 63:2017 66:221...
}
//阶加计算方法
func nmuls(num int)int{
    
    
	res:=1
	for i:=1;i<=num;i++{
    
    
		res += i
	}
	resMap[num]=res //将结果放入全局map中
	return res	
}

channel

不同的协程之间如何通讯
可以使用全局互斥锁 或 使用管道 (管道本身是线程安全的)

声明
var 变量名 chan 数据类型,如:var intChan chan int 声明只放int数据的channel

注意点

  1. channel 的本质就是一个数据结构-队列 ,先进先出。
  2. channel本身是线程安全的
  3. channel是有类型的,只能存放和声明类型相同的数据。
  4. channel 是引用类型,必须初始化(make)后才能写入数据
  5. channel中元素放满后 再放元素就会报dead lock 异常
  6. 没有使用协程的情况下,如果chnnel中数据已经取完了,再取就会报 deadlock
  7. 遍历时如果 channel 没有关闭 则会报 deadlock 异常. 原因应该同6
  8. channel 可以声明成 只读/只写。详见示例

示例代码

func test03(){
    
    
	var intChan chan int
	intChan = make(chan int,3)  //创建一个可以存放 3个int的channel
	fmt.Printf("intChan 值 =%v 地址是=%p \n",intChan,&intChan)
	//intChan 值 =0xc042076080 地址是=0xc04206e018

	//插入数据
	intChan<- 10
	intChan<- 2
	intChan<- 13
	//fatal error: all goroutines are asleep - deadlock! 
	//添加的元素超过 容量时 鲍deadlock 错误
	//intChan<- 4
	//intChan 长度 len= 3 容量= 3
	fmt.Printf("intChan 长度 len= %v 容量= %v \n",len(intChan),cap(intChan))

	//读取数据
	num2:=<-intChan
	fmt.Println("num2=",num2)
	fmt.Printf("intChan 长度 len= %v 容量= %v \n",len(intChan),cap(intChan))
	num3:=<-intChan
	num4:=<-intChan
	//没有元素的时候获取元素 会报异常 fatal error: all goroutines are asleep - deadlock!
	//num4=<-intChan
	fmt.Println(num3,num4)


	//创建可以放所有类型的 channel
	var aChan chan interface{
    
    }
	aChan = make(chan interface{
    
    },5)
	aChan<- 1
	aChan<- "123"
	aChan<- 12.12
	 o:=<-aChan
	fmt.Println("取出元素:",o)//取出元素: 1


	//channel 遍历 不能通过普通的for 循环遍历 (数量可能会变化)
	//
	// for i:=0;i<len(aChan);i++{
    
    

	// }
	//遍历时如果 channel 没有关闭 则会报 deadlock 异常,fatal error: all goroutines are asleep - deadlock!
	//如果channel 关闭了,则不会报异常,遍历完 程序退出
	fmt.Printf("aChan 长度 len= %v 容量= %v \n",len(aChan),cap(aChan))

	close(aChan)
	for v:=range aChan{
    
    
		fmt.Println("v=",v)
	}
}

互斥锁实现数据同步示例

//示例 多协程计算 0-200 每个数字的阶乘,把结果放到map 中
// 执行完成后主线程打印结果
//声明全局变量
//不加锁时 没有出现任何异常
var(
	resMap = make(map[int]int,10)
	lock sync.Mutex //定义全局排他锁
)
func test02(){
    
    
	for i:=1;i<=20;i++{
    
    
		go nmuls(i)
	}
	time.Sleep(time.Second*10)
	fmt.Println(resMap)//打印map
	//map[85:3656 48:1177 78:3082 35:631 41:862 55:1541 63:2017 66:221
}
//阶加计算方法
func nmuls(num int)int{
    
    
	res:=1
	for i:=1;i<=num;i++{
    
    
		res += i
	}
	lock.Lock()//写数据前枷锁
	resMap[num]=res //将结果放入全局map中
	lock.Unlock()//写数据完成进行解锁
	return res	
}

Channel示例
统计 1-20000 的数字中,哪些是素数

func test04(){
    
    
	intChan := make(chan int,1000)
	primeChan := make(chan int,2000)
	exitChan := make(chan bool,4)
	//开启协程 插入数据
	go putNum(intChan,100)

	//开启四个 计算协程
	for i:=1;i<=4;i++{
    
    
		go primeNum(intChan,primeChan,exitChan)
	}

	//当 exitchannel中取出四个元素后 证明任务执行完毕
	go func(){
    
    
		for i:=1;i<=4;i++{
    
    
			<-exitChan
		}
		close(primeChan)
	}()
	//遍历 primeChan
	fmt.Println("素数共",len(primeChan),"个如下:")
	for v:=range primeChan{
    
    
		fmt.Println(v)
	}

}
//放指定数量的数到channel中
func putNum(intChan chan int,num int){
    
    
	for i:=1;i<=num ;i++{
    
    
		intChan<-i
	}
	//添加完了 关闭 channel
	close(intChan)
}
//从 channel中取数据并判断是否为素数,如果是 则添加到 素数 channel中
//如果取不到数据了(数据一处理完)则 exitChannel中添加元素并 退出程序
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
    
    
	var flag bool
	for{
    
    
		time.Sleep(time.Millisecond*10)//睡10毫秒
		num,ok:=<-intChan
		if !ok{
    
    
			break; //取不到数据 则 退出
		}
		flag =true
		for i:=2;i<num;i++{
    
    
			if num%i ==0{
    
    //证明是不是素数
				flag = false
				break
			}
		}
		if flag{
    
    //如果是素数则加到channel中
			primeChan<-num
		}

	}
	fmt.Println("一个协程由于取不到数据而退出")
	exitChan<-true//添加标识 到 退出 channel
}

只读只写协程示例

func test05(){
    
    
	//默认情况下 channel是可读可写
	//声明为只写
	var chan2 chan<- int
	chan2 = make(chan int,3)
	chan2<- 1
	chan2<- 2
	//num:=<-chan2 //error invalid operation: 
	//声明只读channel
	//var chan3 <-chan int
	//chan3 = make(chan int,3)
	//chan3<- 1 //invalid operation: 
	//num:=<-chan3 
	//fmt.Println("num=",num)  // deadlock!

	//协程测试 只读 和只写
	 var ch chan int
	 ch  = make(chan int,10)
	 exitChan := make(chan bool,2)
	 go send(ch,exitChan)
	 go recv(ch,exitChan)
	//  go func(){
    
    
	// 	for i:=1;i<=2;i++ {
    
    
	// 		val :=<-exitChan
	// 		fmt.Println("执行结束")
	// 	 }
	// 	 close(exitChan)
	//  }()
	total:=0
	for v := range exitChan{
    
    
		fmt.Println("读取内容v=",v)
		total ++
		if(total>= 2){
    
    
			break
		}
	}
	
	 fmt.Println("执行结束")

}
//该方法只写数据到 channel
func send(ch chan<- int,exitChan chan bool){
    
    
	fmt.Println("send 开始")
	for i:=1;i<=10;i++{
    
    
		ch<-i
	}
	close(ch)
	fmt.Println("send 结束")
	exitChan<- true
}
//该方法只读数据
func recv(ch <-chan int,exitChan chan bool){
    
    
	fmt.Println("recv 开始")
	for {
    
    
		v,ok:=<-ch
		if !ok{
    
    
			fmt.Println("读取值错误",ok)
			break
		}
		fmt.Println("读取到值==》:",v)
	}
	fmt.Println("recv 结束")
	exitChan<-true
}

select解决deadlock问题

func test06(){
    
    
   //select 解决从 管道 取数据的阻塞问题
   intChan :=make(chan int,10)
   for i:=0;i<10;i++{
    
    
	   intChan<-i
   }

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

   //传统方式 遍历管道时 如果 没有关闭会导致阻塞 而报错 deadlock
   //实际开发过程中,可能不确实什么时候该关闭管道
   //可以使用select 解决报错的问题
   for{
    
    
	   select {
    
    
	   //这里 虽然intChan一直没有关闭,但是不会一直阻塞而deadlock,而是自动到写一个case匹配
	   		case v:=<-intChan:
				fmt.Printf("从intChan取值 %d \n",v)
				time.Sleep(time.Second)
			case v:=<-strChan:
				fmt.Printf("从strChan取值 %s \n",v)
				time.Sleep(time.Second)
			default:
				fmt.Printf("都没有取到值\n")
				return
	   }
   }

}

协程异常处理

func test07(){
    
    
	go sayHello2()
	go testPanic()
	for i:=0;i<11;i++{
    
    
		fmt.Println("main ok~",i)
		time.Sleep(time.Second)
	}

}
func sayHello2(){
    
    
	for i:=0;i<10;i++{
    
    
		time.Sleep(time.Second)
		fmt.Println("helloWorld~")
	}
}
func testPanic(){
    
    
	//捕获异常
	//如果不捕获这个异常,那么主线程一起其他协程都会因异常而退出执行
	//捕获之后,该协程的异常不会影响其他协程和主线程
	defer func(){
    
    
		if err:=recover();err!=nil{
    
    
			fmt.Println("方法发生错误,",err)
		}
	}()
	var mapp map[int]string
	mapp[1]="golang"//没有执行make所以肯定报错
}

猜你喜欢

转载自blog.csdn.net/zhangxm_qz/article/details/115280572