大家好,欢迎来到IT知识分享网。
目录
一、goroutine(协程)
进程和线程说明
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
- 并发: 因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
- 并行: 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。
1、goroutine
- Go主线程(可理解为进程):一个Go线程上可以起多个协程。协程是轻量级的线程。
- 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
- 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。
2、MPG模式(goroutine的调度模型)
- M:操作系统的主线程(是物理线程)
- P:协程执行需要的上下文
- 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]=- mmap[74]=0 mmap[46]= mmap[152]=0 mmap[109]=0 mmap[149]=0 mmap[183]=0 mmap[197]=0 mmap[37]=0 mmap[105]=0 因为阶乘超过int存储的最大范围所以上面结果是0
还可以使用下面的channel(管道)来解决资源争抢的问题
二、channel(管道)
1、channel(管道)
- channel本质就是一个数据结构-队列
- 数据是先进先出
- 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
- 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)) }
- channel中只能存放指定的数据类型
- channel的数据存放满后,就不能再放入了
- 如果从channel取出数据后,可以继续放入
- 在没有使用协程的情况下,如果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的方式进行遍历
- 在遍历时,如果channel没有关闭,则会出现deadlock的错误
- 在遍历时,如果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、管道的使用细节
- 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) }
- 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 }
- 使用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 } } }
- 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" }
Golang学习+深入(十四)-反射
干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/136384.html