golang中的goroutine与channel对线程的支持 - Go语言中文社区

golang中的goroutine与channel对线程的支持


1、goroutine基本介绍与应用

1.1、线程和进程介绍
在这里插入图片描述
1.2、程序、进程和线程的关系示意图
在这里插入图片描述
1.3、并发和并行
在这里插入图片描述
在这里插入图片描述
1.4、go协程的主线程
在这里插入图片描述
在这里插入图片描述

1.5、goroutine(协程)的简单应用
编写程序完成以下功能:
在这里插入图片描述

import (
	"fmt"
	"time"
)

/**
 *  @Description:goroutine 简单应用
 *  @Author: guai
 *  @Date:2020/2/25 22:28
**/

//编写一个函数,每隔一秒输出”hello,world
func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() hello,world", i)
		time.Sleep(time.Second)
	}
}
func main() {
	//开启一个协程 
	go test()

	for i := 0; i < 10; i++ {
		fmt.Println("main() hello,golang", i)
		time.Sleep(time.Second)

	}
}

结果:
在这里插入图片描述
在这里插入图片描述
1.5.1、小结
在这里插入图片描述
1.6、goroutine的调度模型
1.6.1、MPG模式基本介绍
在这里插入图片描述
1.6.2、MPG模式运行的状态1
在这里插入图片描述
1.6.3、
在这里插入图片描述
1.6.4、设置golang程序运行的cpu数
在这里插入图片描述

2、channel(管道)介绍

2.1、我们通过一个需求去发现问题引入channel
在这里插入图片描述

 package main

import (
	"fmt"
	"time"
)

/**
 *  @Description:channel管道 用于多协称之间的通信
 *  @Author: guai
 *  @Date:2020/2/25 23:49
**/
func main() {
	//创建一个map保存阶乘的结果
	var myMap = make(map[int]int, 10)
	
	cal := func(n int) {
		res := 1
		for i := 1; i < n; i++ {
			res *= i
		}
		//将结果放到map中
		myMap[n] = res
	}

	//开启多协程 完成任务
	for i := 0; i < 130; i++ {
		go cal(i)
	}
	//输出结果
	//防止主线程在协程运行完之前结束
	time.Sleep(time.Second * 3)
	for index, val := range myMap {
		fmt.Printf("map[%d]=%dn", index, val)
	}
}

结果:出现了一个致命错误:并发的向map中写
在这里插入图片描述
示意图:
在这里插入图片描述

2.2、不同goroutine之间如何通讯
1)全局互斥锁
2)使用管道channel来解决

2.2.1、全局变量锁
在这里插入图片描述

package main

import (
	"fmt"
	"sync"
	"time"
)

/**
 *  @Description:channel管道 用于多协称之间的通信
 *  @Author: guai
 *  @Date:2020/2/25 23:49
**/
func main() {
	//创建一个map保存阶乘的结果
	var myMap = make(map[int]int, 10)
	//声明一个全局互斥锁
	//sync是包 Mutex(互斥)结构体
	var lock sync.Mutex
	cal := func(n int) {
		res := 1
		for i := 1; i < n; i++ {
			res *= i
		}
		//为变量myMap加锁,是同一时间只能有一个协程访问
		lock.Lock()
		//将结果放到map中
		myMap[n] = res
		//解锁
		lock.Unlock()
	}

	//开启多协程 完成任务
	for i := 0; i < 130; i++ {
		go cal(i)
	}
	 //防止主线程在协程运行完之前结束
	time.Sleep(time.Second * 2)
	//输出结果
	for index, val := range myMap {
		fmt.Printf("map[%d]=%dn", index, val)
	}
}

结果:
在这里插入图片描述

2.3、使用channel解决上述问题(将在channel应用实例三中解决上述问题)
2.3.1、为什么使用channel
在这里插入图片描述
2.3.2、channel基本介绍
在这里插入图片描述
在这里插入图片描述
2.3.3、定义/声明channel
在这里插入图片描述
channel的使用:

package main
import "fmt"
/**
 *  @Description:channel 管道的使用
 *  @Author: guai
 *  @Date:2020/2/26 16:16
**/
func main() {
	//1、创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)
	fmt.Printf("intChan的值=%v intChan本身的地址=%pn", intChan, &intChan)

	// 为管道写入数据,注意写入数据的个数不能超过其容量否则报错
	intChan <- 10
	num := 122
	intChan <- num
	intChan <- 50

	fmt.Printf("channel len%v cap=%vn", len(intChan), cap(intChan))
	//从管道中取出数据 注意是取出 ,取出后管道中将不再有该值
	var num2 int
	num2 = <-intChan
	fmt.Println("num2:", num2)
	//通过观察结果可知 len为2
	fmt.Printf("channel len=%v cap=%vn", len(intChan), cap(intChan))
	//继续取出管道中的值   注意当管道中的值被全部取出后继续取就会报错 deadlock
	num3 := <-intChan
	num4 := <-intChan
	//num5:=<-intChan
	fmt.Println("num3=", num3, "num4=", num4)
	fmt.Printf("channel len=%v cap=%v", len(intChan), cap(intChan))

}

结果:
在这里插入图片描述

channel使用的注意事项
在这里插入图片描述
2.3.4、channel练习:

import "fmt"

/**
 *  @Description:使用管道保存任意数据类型
 *  @Author: guai
 *  @Date:2020/2/26 20:55
**/

type cate struct {
	name string
	age  int
}

func main() {
	//定义一个可以保存任意数据类型的管道
	allChan := make(chan interface{}, 10)
	//向管道中保存一个cate结构体
	allChan <- cate{"guai", 12}

	map1 := make(map[int]int)
	map1[1] = 1
	map1[2] = 2
	//相管道中保存一个map
	allChan <- map1

	//当从次此类管道中取出数据时 需使用类型断言
	//当类型与断言类型匹配时ok为true否则反之
	cate1, ok := (<-allChan).(cate)
	fmt.Println("name", cate1.name, "ok", ok)

	map2, ok := (<-allChan).(map[int]int)
	fmt.Println("map2", map2, "ok:", ok)
	
    //也可直接遍历 但在遍历前需要先关闭channel否则会出现 deadlock的错误
	//close(allChan)
	//for v := range allChan {
	//	fmt.Println("v:", v)
	//}
}

结果:
在这里插入图片描述
2.4、channel应用实例一
在这里插入图片描述
在这里插入图片描述
码:

 package main

import "fmt"

/**
 *  @Description: 完成goroutine和channel协同工作的案例
 *  @Author: guai
 *  @Date:2020/2/27 10:02
**/

func writeData(intChan chan int) {
	for i := 1; i <= 500000; i++ {
		//向管道中放入数据
		intChan <- i
		fmt.Println("wirteData:", i)
	}
	//关闭管道
	close(intChan)
}
func readData(intChan chan int, exitChan chan bool) {
	for {
		//从管道中取出数据  当数据全部不取出后 ok为false v为初始默认值
		v, ok := <-intChan
		if !ok {
			break
		}
		fmt.Println("readData:", v)
	}
	exitChan <- true
	close(exitChan)
}

func main() {
	//创建两个管到
	intChan := make(chan int, 500000)
	exitChan := make(chan bool, 1)

	go writeData(intChan)
	go readData(intChan, exitChan)

	//主线程与协程同时运行 当协程运行结束 exitChan 为false 主线程结束
	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}
}

结果:
在这里插入图片描述
思考:
在这里插入图片描述

2.5、channel应用实例二
在这里插入图片描述
在这里插入图片描述
代码:

package main

import (
	"fmt"
	"time"
)
/**
 *  @Description:channel 练习 统计1-8000的数字中,哪些是素数
 *  @Author: guai
 *  @Date:2020/2/27 10:20
**/

//将1-8000放入管道
func putNum(intChan chan int) {
	for i := 1; i <= 8000; i++ {
		intChan <- i
	}
	//关闭管道
	close(intChan)
}

//从intChan中取出素数,放入primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	var flag bool
	for {
		//time.Sleep(time.Millisecond * 5)
		//从管道中取出数据
		num, ok := <-intChan
		//当取不到数据时退出
		if !ok {
			break
		}
		flag = true
		//判断num是不是素数
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}
		//若num是素数将num放入primeChan中
		if flag {
			primeChan <- num
		}
	}

	fmt.Println("有一个primeNum协程因为取不到数据,退出")
	exitChan <- true
}
func main() {
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 20)
	exitChan := make(chan bool, 4)

	//开启一个协程向intChan放入 1-8000个数
	go putNum(intChan)
	//开启四个携程,从intChan取出数据,并判断是否为素数
	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}


	//如果只放不取会导致 deadLock  ,故需要取出 同时
	go func() {
		for i := 0; i < 4; i++ {
			fmt.Println(<-exitChan,"1212")
		}
		fmt.Println("close primeChan")
		close(primeChan)

	}()
	//遍历primeChan,取出结果
	fmt.Println("begin read")

	time.Sleep(time.Second*2)
	for res := range primeChan {
		//输出结果
		fmt.Println("素数=", res)
	}

	fmt.Println("主线程退出")

}

结果:
在这里插入图片描述
通过结果我们发现:
1)在所有协程结束前就已经开始输出结果:
原因:
我们在通过primeNum函数从intChan中取出数据并判断是否为素数并将素数存入primeChan时,主线程中的输出primeNum中的语句也在执行,也就是说并不是在判断完所有intChan中的数并将素数存入primeNum之后才开始从primeNum中读取并打印素数的,两者是并发/并行执行的
2)intChan的容量只有1000却保存了8000个整数
在putNum函数中将1-8000写入intChan中是 其实并不是一次性全部写入的,在写入过程中primeNum函数同时也在将intChan中的整数取出并判断是否为素数然后放入primeNum中两者是并发/并行执行的

2.6、channel应用实例三

package main
import "fmt"
/**
 *  @Description: 求1-100中每个数的阶乘
 *  @Author: guai
 *  @Date:2020/2/27 15:19
**/

/*思路:
1、将1-100个数保存到一个intChan管道中   (防止多协程取数据是发生死锁)
2、启用四个协程取从intChan取出数字并求阶乘
3、将结果保存到resChan中
4、遍历resChan输出结果

*/
func getIntChans(intChan chan int) {
	//向管道中添加数据
	for i := 1; i <= 100; i++ {
		intChan <- i
	}
	//关闭管道
	close(intChan)
}

func calRes(intChan chan int, resChan chan int, exitChan chan bool) {
	for {
		num, ok := <-intChan
		//当没有从intChan中取出数据是 结束循环
		if !ok {
			break
		}

		//计算阶乘
		res := 1
		for i := 1; i <= num; i++ {
			res *= i
		}
		//将结果保存到管道中
		resChan <- res
	}
	fmt.Println("有一个协程calRes因无法取出数据而退出")
	exitChan <- true
}
func main() {

	intChan := make(chan int, 1050)
	resChan := make(chan int, 1050)
	//用于控制主线程在所有协程执行完才结束
	exitChan := make(chan bool, 4)
	//通过一个协程向intChan写入数据
	go getIntChans(intChan)
	//开启4个协程
	for i := 0; i < 4; i++ {
		go calRes(intChan, resChan, exitChan)
	}
	//只有当此协程结束 主线程才能结束 此时上面的协程也都执行完完毕
	go func() {
		for i := 0; i < 4; i++ {
			<-exitChan
		}
		close(resChan)
	}()

	for val := range resChan {
		fmt.Println(val)
	}
	fmt.Println("主线程结束")
 
}

结果:可以看出结果中存在负值和零值,是因为结果超出了int类型的最大取值范围
在这里插入图片描述
2.7、只读/只写管道的介绍于应用:

package main

import "fmt"

/**
 *  @Description:管道使用细节
 *  @Author: guai
 *  @Date:2020/2/27 16:06
**/
func main() {
	//1、channel可以声明为只读,或者只写性质
	//默认情况下管道是双向的 即可读可写
	//1.1、声明为只写
	var intChan1 chan<- int
	intChan1 = make(chan int, 3)
	intChan1 <- 20
	fmt.Println("only write intChna1", intChan1)

	//1.2、声明为只读
	//声明一个可读可写的管道
	var intChan3 chan int
	intChan3 = make(chan int, 1)
	intChan3 <- 2

	var intChan2 <-chan int
	//通过一个可读可写的通道初始化
	intChan2 = intChan3
	//intChan2<-3  //err
	fmt.Println("only read intChan2:", <-intChan2)

	//1.3、channel只读和只写的最佳案例实践
	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 == 10 {
			break
		}
	}
	fmt.Println("over...")
}

//用于写管道的函数
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
	}
}

结果:
在这里插入图片描述

2.8、select的介绍即用于解决管道取数据的阻塞问题

package main

import (
	"fmt"
)

/**
 *  @Description:使用select解决从管道取数据的阻塞问题
 *  @Author: guai
 *  @Date:2020/2/27 16:37
**/
func main() {
	//在实际开发中我们有时无法确定什么时候关闭管道
	//此时可使用  当一个case后的读取阻塞将会自动到下一个case匹配知道default
	//select {
	// case: 读取管道
	// case : 读取管道
	// default: 当没有主动关闭管道是将会执行此 case
	//}

	//定义一个存放10个数据的管道
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	//定义一个存放5个数据的管道
	strChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		strChan <- "hello" + fmt.Sprint("%d", i)
	}
	//遍历管道
	for {
		select {
		//注意:如果intCHan一致读取不到不会一致阻塞而是自动到下一个case匹配
		case v := <-intChan:
			fmt.Println("intChan:", v)
			//time.Sleep(time.Second)
		case v := <-strChan:
			fmt.Println(
                        
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_44373940/article/details/104507041
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢