GO语言学习之goroutine和channel

GO语言学习之goroutine和 channel

1.goroutine 看需求

》需求:要求统计1-900000000的数字中,哪些是素数?

》分析思路:

1》传统的方法,就是使用一个循环,循环判断各个数是不是素数。

2》使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到 goroutine[速度提高4倍]

2.goroutine 基本介绍

2.1进程和线程介绍

1》进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

2》线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

3》一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行

4》一个程序至少一个进程,一个进程至少有一个线程

2.2程序,进程和线程的关系示意图略
2.3并发和·并行

1》多线程程序在单核上运行,就是并发

2》多线程程序在多核上运行,就是并行

总结:

并发:因为一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行。但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程,执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

2.4GO协程和go主线程

》Go主线程(有程序员直接称为线程/也可以理解成进程):一个 go线程上,可以起多个协程。你可以这样理解,协程是轻量级的线程[编译器做优化]

》GO协程的特点

1》有独立的栈空间

2》共享程序堆空间

3》调度由用户控制

4》协程是轻量级的线程

3.goroutine 快速入门

3.1案例说明

》请编写一个程序,完成如下功能

1》在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔1秒,输出’hello world’

2》在主线程中也每隔一秒输出 ‘hello,jobs’,输出10次后,退出程序

3》要求主线程和 goroutine 同时执行

4》画出主线程和协程执行流程图

package main

import (
	"fmt"
	"strconv"
	"time"
)

//编写一个函数 ,每隔一秒输出helloworld
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 Jobs,"+strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

输出效果:说明main主线程 和 test()协程同时执行


main() hello Jobs,1
test() hello ,world1
main() hello Jobs,2
test() hello ,world2
main() hello Jobs,3
test() hello ,world3
test() hello ,world4
main() hello Jobs,4
main() hello Jobs,5
test() hello ,world5
test() hello ,world6
main() hello Jobs,6
test() hello ,world7
main() hello Jobs,7
test() hello ,world8
main() hello Jobs,8
test() hello ,world9
main() hello Jobs,9
test() hello ,world10
main() hello Jobs,10

程序开始执行,

go test()开启协程-------》协程是for循环,如果主线程退出了,则协程即使 还没有执行完毕,也会退出

-------------------------------》当然协程也可以再主线程没有退出前,就自己结束 了,比如完成了自己的任务

主线程这里for循环执行

主线程结束(程序退出)

3.2快速入门小结

1》主线程是一个物理线程,直接作用在cpu上,是重量级的,非常消耗cpu资源

2》协程从主线程开启,是轻量级的线程,是逻辑态的,对资源消耗相对较小

3》golang中的协程机制是重要的特点,可以轻松地开启上万个协程。其他编程语言的并发机制是一般基于协程的。开启过多的线程,资源消耗大,这里就能显出golang在并发的优势

4.goroutine 的调度模型

4.1MPG模式基本介绍

1》M:操作系统的主线程是(物理线程)

2》P:协程执行需要的上下文

3》G:协程

4.2 MPG模式运行的状态1

1》当前程序有三个M,如果三个M都运行在一个 cpu上,就是并发,如果在不同cpu上运行就是并行

2》M1,M2,M3正在执行一个 G,M1的协程队列有三个,M2的协程队列有三个,M3协程队列有两个

3》go的协程是轻量级的线程,是逻辑态的,GO可以容易的起上万个协程

4》其他程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu

4.3 MPG模式运行的状态2

图片略

1》

5.设置GOLANG运行的Cpu数

介绍:为了充分利用多cpu优势,在go中,设置运行的cpu数目

package main

import (
	"fmt"
	"runtime"
)

func main() {

	//获取当前系统的cpu数量
	num:=runtime.NumCPU()
	//我这里设置 num-1 de cpu 运行go程序
	runtime.GOMAXPROCS(num)
	fmt.Println("num=",num)
}

1》go1.8后,默认让程序运行在多个核上,可以不用设置了

2》go1.8前,还是要设置一下,可以更高效的利益cpu

6.channel(管道)-看个需求

需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用 goroutine 完成

》分析思路:

1》使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题

2》这里就提出了不同的Goroutuine 如何通信的问题

》代码实现

1》使用 goroutine 来完成(看看使用goroutine并发完成会出现什么问题?然后我们会去解决)

2》在运行某个程序时,如何知道是否存在资源竞争问题,方法很简单,在编译该程序时,增加一个参数 -rance[示意图]

package main

import (
	"fmt"
	"time"
)

//思路:
//1.编写一个函数,来计算各个数的阶乘,并放入到map中
//2.我们启动的协程多个,统计的将结构放入到map中
//3.map应该做出一个全局的
var(
	myMap=make(map[int]int,10)
)
//test函数就是计算 n! ,让将这个结果放入到 myMap
func testx(n int){
	res:=1
	for i:=1;i<=n ;i++{
		res*=i
	}
	//我们把 res放入到 myMap中
	myMap[n]=res//
}
func main() {

	//在这里我们开启多个协程完成这个任务【200个】
	for i:=1;i<=200;i++{
		go testx(i)
	}
	//休眠十秒
	time.Sleep(time.Second*10)
	//这里我们输出结果,变量这个结果

	for i,v:=range myMap{
		fmt.Printf("map[%d]=%d\n",i,v)
	}
}


6.1不同goroutine 之间如何通讯

1》全局变量的互斥锁

2》使用 管道 channel来解决

6.2使用全局变量加锁同步改进程序

因为没有对全局变量m 加锁,因此出现资源争夺的问题,代码会出现错误,提示 concurrent map writes

解决方案:加入互斥锁

我们的数的阶乘很大,结果会越界,可以将阶乘改成 sum+=uint64(i)

代码改进:

package main

import (
	"fmt"
	"sync"
)

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
	//解锁
	lock.Unlock()
}

func main() {
	//这里我们输出结果,变量这个结果
	lock.Lock()
	for i,v:=range myMap{
		fmt.Printf("map[%d]=%d\n",i,v)
	}
	lock.Unlock()
}
6.3为什么需要channel

1》前面使用全局变量加锁同步来解决goroutine 的通讯,很不完美

2》主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10秒,仅仅是估算

3》如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态。这时也会随主线程的退出而销毁

4》通过 全局变量加锁同步来实现 通讯,也并不利用 多个协程对全局变量的读写操作

5》上面种种分析都在呼唤一个新的通讯机制 -channel


6.4 channel的基本介绍

1》channel 的本质就是一个 数据结构-队列

2》数据是先进先出【FIFO:first in first out】

3》线程安全,多goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的

4》channel 有类型的,一个 string 的 channel 只能存放string 类型数据

5》channel 使线程安全,多个协程操作同一个管道时,不会发生资源竞争问题

6.5 定义/声明 channel

》var 变量名 chan 数据类型

》举例:

var intChan chan int (intChain 用于存放 int数据类型)

var mapChan chan map[int]string (mapChan用于存放 map[int]string 类型)

var perChan chan Person

var perChan2 chan *Person

》说明

channel 是引用类型

channel 必须初始化才能写入数据,即make后才能使用

管道是有类型的。intChan 只能写入 整数 int

6.6 管道的初始化,写入数据到管道,从管道读取数据以及基本的注意事项
package main

import "fmt"

func main() {
	//演示一下管道的使用
	//1.创建一个可以存放 3个int类型的管道
	var intChan chan int
	intChan =make(chan int,3)
	//2.看看intChan 是什么
	fmt.Printf("intChan 值为 %v,本身地址=%p\n",intChan,&intChan)

	//向管道写入数据
	intChan<-10
	num:=211
	intChan<-num
	intChan<-50
	//intChan<-98 //注意事项:当我们给管道写入数据时,
	//不能超过其容量

	//看看管道的长度和 cap (容量)

	fmt.Printf("channel len=%v cap=%v \n ",len(intChan),cap(intChan))

	//从管道中读取数据
	var  num2 int
	num2=<-intChan
	fmt.Println("num2=",num2)
	fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))

	//在没有使用协程的情况下,如果我们的管道数据已经全部取出,
	// 再取出就会报告deadlock
	num3:=<-intChan
	num4:=<-intChan
	num5:=<-intChan

	fmt.Println("num3=",num3,"num4=",num4,"num5=",num5)
}
6.7channel使用的注意事项

1》channel中只能存放指定的数据类型

2》channel的数据放满后,就不能再放入了

3》如果从 channel取出数据后,可以继续放入

4》在没有使用协程的情况下,如果channel数据取完了,再取,就会报 dead lock

6.8读写channel案例演示

1》创建一个 intChain,最多可以存放 3个 int,演示 存3个数据到 intChain,然后再取出 三个int

package main

import "fmt"

func main() {
	var intChan chan int
	intChan =make(chan int,3)
	intChan<-10
	intChan<-20
	intChan<-10
	//因为 intChan的容量为 3,再存放会报告 deadLock
	num1:=<-intChan
	num2:=<-intChan
	num3:=<-intChan
	//因为 intchan这时已经没有数据了,再取就会报告 deadLock
	fmt.Printf("num1=%v num2=%v num3=%v",num1,num2,num3)
}

2》创建一个mapChan,最多可以存放10个 map[string]string 的key -val,

演示

package main

import "fmt"

func main() {
   var mapChan chan map[string]string
   mapChan=make(chan map[string]string,10)
   m1:=make(map[string]string,20)
   m1["city1"]="天津"
   m1["city2"]="beijing"

	m2:=make(map[string]string,20)
	m2["stu1"]="王小明"
	m2["stu2"]="刘德华"

	//写入
	mapChan<-m1
	mapChan<-m2
	
	x1:=<-mapChan
	x2:=<-mapChan
	fmt.Printf("x1=%v,x2=%v",x1,x2)



}

3》创建一个 catChan ,最多可以存放10个 cat 结构体变量,演示写入和读取的用法

package main

import "fmt"

type Cat struct {

	Name string
	Age int
}

func main() {
	var catChan chan Cat
	catChan=make(chan Cat,10)
	cat1:=Cat{Name:"MAOMAO",Age:19}
	cat2:=Cat{Name:"小菊",Age:18}
	//写入管道
	catChan<-cat1
	catChan<-cat2
	//取出
	cat11:=<-catChan
	cat12:=<-catChan

	fmt.Printf("cat11=%v,cat12=%v",cat11,cat12)

}

4》创建一个 catChan2 ,最多可以存放10个 *Cat 变量,演示写入和读取的用法

package main

import "fmt"

type Cat struct {
	Name string
	Age int
}
func main() {
	var catChan chan *Cat
	catChan=make(chan *Cat,10)

	cat1:=Cat{Name:"abenmao",Age:2}
	cat2:=Cat{Name:"pangpang",Age:6}

	catChan<-&cat1
	catChan<-&cat2
	//取出来
	cat111:=<-catChan
	cat112:=<-catChan

	fmt.Printf("cat111=%v,cat112=%v",cat111,cat112)
}

5》创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法

package main

import "fmt"

type Cat struct {

	Name string
	Age int
}
func main() {
	var allChan chan interface{}
	allChan=make(chan interface{},10)
	x1:=Cat{
		Name:"xiongmao",
		Age:6,
	}
	x2:=Cat{
		Name:"xiongmaomao",
		Age:666,
	}

	allChan<-x1
	allChan<-x2

	allChan<-10001
	allChan<-3.14159
	allChan<-'a'
	allChan<-true
	allChan<-"helloJava"

	s1:=<-allChan
	s2:=<-allChan
	s3:=<-allChan
	s4:=<-allChan
	s5:=<-allChan
	s6:=<-allChan
	s7:=<-allChan

	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)
	fmt.Println(s4)
	fmt.Println(s5)
	fmt.Println(s6)
	fmt.Println(s7)

}

6》看下面代码

package main

import "fmt"

type Cat struct {

	Name string
	Age int
}
func main() {
	var allChan chan interface{}
	allChan=make(chan interface{},10)
	x1:=Cat{
		Name:"xiongmao",
		Age:6,
	}
	x2:=Cat{
		Name:"xiongmaomao",
		Age:666,
	}

	allChan<-x1
	allChan<-x2

	allChan<-10001
	allChan<-3.14159
	allChan<-'a'
	allChan<-true
	allChan<-"helloJava"

	s1:=<-allChan
	s2:=<-allChan


	fmt.Println(s1)
	fmt.Println(s2)


//	fmt.Println(s1.Name)//这样会报错
	//看类型
	fmt.Printf("s1 type =%T",S1)
	
	//使用类型断言
	a:=s1.(Cat)
	fmt.Printf("s1.name=%v",a.Name)

}

7.channel的遍历和关闭

7.1channel的关闭

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

package main

import "fmt"

func main() {
	intChan:=make(chan int,4)
	intChan<-100
	intChan<-250
	close(intChan)//
	//关闭之后,就不能在写入数到 channel
	//
	fmt.Println("over")
	//当管道关闭后,读取数据是可以的
	n1:=<-intChan
	fmt.Println("n1=",n1)

}
7.2 channel 的遍历

channel支持for -range 的方式进行遍历,请注意两个细节

1》在遍历时,如果 channel没有关闭,则会出现deadlock的错误

2》在遍历时,如果channel已经关闭,则会正常变量数据,遍历完后,就会退出遍历

7.3 channel遍历和关闭的案例演示
package main

import "fmt"

func main() {
	intChan:=make(chan int,100)
	//放 100个数进去
	for i:=0;i<100;i++{
		intChan<-i*2
	}

	//遍历不能用普通for循环遍历
	close(intChan)
	for v:=range intChan{
		fmt.Println(v)
	}
}
7.4应用实例1
请完成goroutine和 channel协同工作的案例,具体要求:
1》开启一个writeData协程,向管道 intChan中写入 50个整数
2》开启一个readData协程,从管道intChan中读取writeData写入的数据
3》注意:writeData和 readData操作的是同一个管道
4》主线程需要等待writeDate和readData协程都完成工作才能退出【管道】

代码:

package main

import (
	"fmt"
	"time"
)

//writeData
func writeData(intChan chan int){
	for i:=1;i<=50;i++{
		//放入数据
		intChan<-i
		fmt.Println("writeData",i)
		time.Sleep(time.Second)
	}
	close(intChan)
}
//read DATA
func readData(intChan chan int,exitChan chan bool){

	for {
		v,ok:=<-intChan
		if !ok{
			break
		}
		fmt.Printf("readData 读到数据=%v\n",v)
		time.Sleep(time.Second)
	}
	//读完数据后,任务完成
	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
		}
	}
}
7.5应用案例2-阻塞
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
		}
	}
}

只有写,没有读就会线程死锁,如果写的快,读的慢,写管道和读管道的频率不一致无所谓

7.6应用实例-3

》要求:

要求统计 1-200000的数字中,哪些是素数这个问题在本章开篇就提出了,现在我们有goroutine和 channel的知识中,就可以完成了

》分析思路:

传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok]

使用并发/并行的方式,将统计素数的任务分配给多个(4个)goroutine去完成,完成任务时间短

package main

import (
	"fmt"
	"time"
)

//向 intChan放入 1-8000个数
func putNum(intChan chan int){
	for i:=1;i<=8000;i++{
		intChan<-i
	}
	//关闭 intChan
	close(intChan)
}
//从 intCHan取出数据,并判断是否为素数,如果是 ,就放入 primeChan
func primeNum(intChan chan int, primeChan chan int,exitChan chan bool)  {

	//使用for循环

	var flag bool
	for {
		time.Sleep(time.Microsecond*10)
		num,ok:=<-intChan
		if !ok{
			break
		}
		flag=true//假设是素数
		//判断 num 是不是素数
		for i:=2;i<num;i++{
			if num%i==0{//说明不是素数
				flag=false
				break
			}
		}
		if flag{
			//将这个数放入到 primeChan
			primeChan<-num
		}
	}
	fmt.Println("有一个 primeNum协程因为 取不到数据,退出")
	//这里我们不能关闭 primeChan
	//向 exitChan写入 true
	exitChan<-true

}

func main() {
	intChan:=make(chan int,1000)
	primeChan:=make(chan int,2000)//放入结果

	//标识退出管道
	exitChan:=make(chan bool,4)//4个

	//开启协程 ,向 intChan 放入 1--8000个数
	go putNum(intChan)
	//开启4个协程,从 intChan 取出数据,并判断是否为素数,如果是
	//就放入到 primeChan
	for i:=0;i<4;i++{
		go primeNum(intChan,primeChan,exitChan)
	}
	//这里我们主线程,进行处理
	go func(){

		for i:=0;i<4;i++{
			<-exitChan
		}
		//当我们从 exitChan取出了四个结果,就可以放心关闭 primeChan
		close(primeChan)
	}()
	//遍历我们的 primeChan ,把结果取出
	for {
		res,ok:=<-primeChan
		if !ok{
			break
		}
		//将结果输出
		fmt.Printf("素数=%d\n",res)
	}
	fmt.Println("main线程退出")
}

结论:使用 go 协程后,执行的速度,比普通方法提高至少 4倍

8.channel使用细节和注意事项

1》.channel可以声明为只读,或者只写性质
package main

import "fmt"

func main() {
	//管道可以声明为只读或者只写
	//1.在默认情况下,管道时双向的
	//var chan1 chan int
	//2.声明为只写
	var chan2 chan<-int
	chan2=make(chan int,3)
	chan2<-30
	//num:=<-chan2
	fmt.Println("chan2=",chan2)

    //声明为 只读
    var chan3<-chan int
    num3:=chan3
    fmt.Println("num3=",num3)
}

2》channel只读和只写的最佳实践案例
3》使用 select 可以解决从管道取数据的阻塞问题
package main

import (
	"fmt"
	"time"
)

func main() {
	//使用 select 可以解决从管道取数据的阻塞问题
	//1.定义一个管道 10个数据 int
	intChan:=make(chan int,10)
	for i:=0;i<10;i++{
		intChan<-i
	}
	//2.定义一个管道 5个数据 string
	stringChan:=make(chan string,5)

	for i:=0;i<5;i++{
		stringChan<-"hello"+fmt.Sprintf("%d",i)
	}
	//将传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadLock
	//在实际开发中,可能我们不好确定什么关闭该管道
	//可以使用 select 方式 可以解决
	for{
		select {
		//注意:这里 ,如果 intChan一直1没有关闭,不会一直阻塞而deadlock
		//,会自动下一个case匹配
			case v:=<-intChan:
				fmt.Printf("从 intChan读取的数据%d \n",v)
				time.Sleep(time.Second)
			case v:=<-stringChan:
				fmt.Printf("从 stringChan 获取得数据 %s \n",v)
				time.Sleep(time.Second)
		default:
				fmt.Printf("都取不到了,不玩了,\n")
				time.Sleep(time.Second)
				return

		}
	}
}

4》goroutine中使用 recover ,解决协程中出现 panic ,导致程序崩溃问题

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

package main

import (
	"fmt"
	"time"
)

func sayHello(){
	for i:=0;i<10;i++{
		time.Sleep(time.Second)
		fmt.Println("hello,world")
	}
}
//函数
func test(){
	//这里我们使用 defer+recover
	defer func(){
		if err:=recover();err!=nil{

			fmt.Println("test()发生错误",err)
		}
	}()
	//定义了一个 map
	var myMap map[int]string
	myMap[0]="liuyifei"
}

func main() {
	go sayHello()
	go test()

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值