Golang中goroutine和channel使用介绍深入分析
作者:2019ab
1.goroutine-看一个需求
需求:要求统计1-900000000的数字中,那些是素数?
分析:
- 传统方法,就是使用一个循环,循环的判断各个数是不是素数。
- 使用并发或并行的方式,将统计素数的任务分配给多个goroutine去完成,这时就会使用到goroutine。
2.进程和线程介绍
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位。
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
3.并发和并行
- 多线程程序在单核上运行,就是并发
- 多个程程序在多核上运行,就是并行
并发:因为是在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
并行:因为是在多个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
4.Go协程和Go主线程
Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个携程,你可以这样理解,携程是轻量的线程
Go协程的特点
有独立的栈空间
共享程序堆空间
调度由用户控制
携程是轻量级的线程
案例说明
请编写一个程序,完成如下功能:
1.在主线程(可以理解成进程)中,开启一个goroutine,该携程每隔1秒输出“hello,world”
2.在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序
3.要求主线程和goroutine同时执行
4.画出主线程和协程执行流程图
代码实现
// 在主线程(可以理解成进程)中,开启一个goroutine,该协程每秒输出 “hello,world” // 在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序 // 要求主线程和goroutine同时执行 //编写一个函数,每隔1秒输出 “hello,world” 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) } }
总结
- 主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常耗费CPU资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对少。
- Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了
MPG模式基本介绍
M:操作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程
5.设置Golang运行的CPU数
介绍:为了充分利用多CPU的优势,在Golang程序中设置运行的CPU数目
package main import "fmt" import "runtime" func main(){ // 获取当前系统CPU的数量 num := runtime.NumCPU() // 这里设置num-1的CPU运行go程序 runtime.GOMAXPROCS(num) fmt.Println("num=",num) }
- go1.8后,默认让程序运行在多个核上,可以不用设置了
- go1.8前,还是要设置一下,可以更高效的利用CPU
6.channel(管道)看需求
需求:现在要计算 1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成
分析思路:
使用goroutine来完成,效率高,但是会出现并发/并行安全问题
这里就提出了不同goroutine如何通信的问题
代码实现
使用goroutine来完成(看看使用gorotine并发完成会出现什么问题?然后我们会去解决)
在运行某个程序时,如何知道是否存在资源竞争问题,方法很简单,在编译该程序时,增加一个参数 -race即可
不同goroutine之间如何通讯
1.全局变量的互斥锁
2.使用管道channel来解决
使用全局变量加锁同步改进程序
- 英文没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
- 解决方案:加入互斥锁
- 我们的数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)
源码
package main import ( "fmt" "time" "sync" ) // 需求:现在要计算 1-200的各个数的阶乘,并且把各个数的阶乘放入到map中 // 最后显示出来。要求使用goroutine完成 // 思路 // 1. 编写一个函数,来计算各个数的阶乘,并放入到map中 // 2. 我们启动的协程多个,统计的将结果放入到map中 // 3. map应该做出一个全局的 var ( myMap = make(map[int]int,10) // 声明一个全局的互斥锁 // lock 是一个全局的互斥锁 //sync 是包:synchornized 同步 // Mutex: 是互斥 lock sync.Mutex ) // test函数就是计算n!,让将这个结果放入到myMap func test(n int){ res := 1 for i := 1;i<=n;i++{ res *= i } // 这里我们将res放入到myMap // 加锁 lock.Lock() myMap[n] = res // concurrent map writes? // 解锁 lock.Unlock() } func main(){ // 我们这里开启多个协程完成这个任务[200个] for i := 1;i<=20;i++{ go test(i) } // 休眠10秒钟【第二个问题】 time.Sleep(time.Second * 10) lock.Lock() // 这里我们输出结果 变量这个结果 for i,v := range myMap{ fmt.Printf("map[%d]=%d\n",i,v) } lock.Unlock() }
channel(管道)-基本使用
channel初始化
说明:使用make进行初始化
var intChan chan int
intChan = make(chan int,10)
向channel中写入(存放)数据
var intChan chan int
intChan = make(chan int,10)
num := 999
intChan <-10
intChan <-num
管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
package main import ( "fmt" ) func main(){ // 演示一下管道的使用 // 1.创建一个可以存放3个int类型的管道 var intChan chan int intChan = make(chan int,3) // 2.看看intChannel是什么 fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n",intChan,&intChan) // 3.向管道写入数据 intChan<- 10 num := 211 intChan<- num // 注意点,当我们给管写入数据时,不能超过其容量 intChan<- 50 // intChan<- 98 //4. 看看管道的长度和cap(容量) fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 2,3 // 5.从管道中读取数据 var num2 int num2 = <-intChan fmt.Println("num2=",num2) fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 2,3 // 6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock num3 := <-intChan num4 := <-intChan // num5 := <-intChan fmt.Println("num3=",num3,"num4=",num4)//,"num5=",num5) }
channel使用的注意事项
1.channel中只能存放指定的数据类型
2.channel的数据放满后,就不能再放入了
3.如果从channel取出数据后,可以继续放入
4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
示例代码
package main import ( "fmt" ) type Cat struct{ Name string Age int } func main(){ // 定义一个存放任意数据类型的管道 3个数据 // var callChan chan interface{} allChan := make(chan interface{},3) allChan<- 10 allChan<- "tom jack" cat := Cat{"小花猫",4} allChan<- cat // 我们希望获得到管道中的第三个元素,则先将前2个推出 <-allChan <-allChan newCat := <-allChan // 从管道中取出的Cat是什么? fmt.Printf("newCat=%T,newCat=%v\n",newCat,newCat) // 下面的写法是错误的!编译不通过 // fmt.Printf("newCat.Name=%v",newCat.Name) // 使用类型断言 a := newCat.(Cat) fmt.Printf("newCat.Name=%v",a.Name) }
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
channel的遍历
channel支持for-range的方式进行遍历,请注意两个细节
- 在遍历时,如果channel没有关闭,则会出现deadlock的错误
- 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
代码演示:
package main import ( "fmt" ) func main(){ intChan := make(chan int,3) intChan<- 100 intChan<- 200 close(intChan) // close // 这是不能够再写入到channel // intChan<-300 fmt.Println("okook~") // 当管道关闭后,读取数据是可以的 n1 := <-intChan fmt.Println("n1=",n1) // 遍历管道 intChan2 := make(chan int,100) for i := 0; i< 100;i++{ intChan2<-i*2 // 放入100个数据到管道 } // 遍历管道不能使用普通的for循环 // 在遍历时,如果channel没有关闭,则会出现deadlock的错误 // 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 close(intChan2) for v := range intChan2{ fmt.Println("v=",v) } }
到此这篇关于Golang中goroutine和channel使用介绍深入分析的文章就介绍到这了,更多相关Go goroutine与channel内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!