golang goroutine
一.goroutine基本介绍
1.进程和线程说明介绍
1)进行就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位(比如迅雷进程)
2)线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位(比如迅雷中有好几个任务正在下载)
3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。(并发是时间片很短,来回切换,同一时间只有一个在进行,而不是并行)
4)一个程序至少有一个进程,一个进程至少有一个线程。(比如同时打开一个程序的多个客户端就是起了多个进程)
2.并发和并行
go语言可以把并发转成并行
1)多线程程序在单核上运行,就是并发。多个任务作用在一个cpu上。从微观角度来看,在一个时间点,只有一个任务在执行,多个任务切换进行。
2)多线程程序在多核上运行,就是并行。多个任务作用在多个cpu上。从微观的角度来看,在同一时间点,有多个任务在同事执行。并行速度比并发快。
3.Go协程和Go主线程。(goroutine go中可以跑上w个协程)
1)Go线程(有程序员直接称为线程/可以理解为进程):一个线程上,可以起多个协程。协程是轻量级的线程【编译器做了优化】
2)Go协程的特点
有独立的栈空间、共享程序堆空间、调度由用户控制、协程是轻量级的线程
二.案例
1.快速入门案例
如下图所示:
1)如果主线程退出了,则协程即使还没有执行完毕,也会退出
2)协程可以在主线程没有退出前就结束
package main import ( "fmt" "strconv" "time" ) func test() { for i := 0; i < 10; i++ { fmt.Println("test()hello.world" + strconv.Itoa(i)) time.Sleep(time.Second) } } func main() { go test() //开启协程 for i := 0; i < 10; i++ { fmt.Println("main()hello.golang" + strconv.Itoa(i)) time.Sleep(time.Second) } }
快速入门小结:
1)主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源。
2)协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。
3)golang的协程机制是中药的特点,可以轻松开启上万的协程,其他编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就凸显出golang在并发上的优势了。因为线程要有各自的堆栈,而协程是有自己的栈,共享堆。
二.实例
1.高效利用cpu
常用函数 runtime包:设置go程序使用多少cpu。go1.8以前需要手动设置,go1.8以后会默认设置多核运行
1)func NumCPU() int: NumCPU返回本地机器的逻辑CPU个数。
2)func GOMAXPROCS: func GOMAXPROCS(n int) int GOMAXPROCS设置可同时执行的最大CPU数,并返回先前的设置。 若 n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU 查询。本函数在调度程序优化后会去掉。
package main import ( "fmt" "runtime" ) func main() { //查看本地机器逻辑cpu的个数 fmt.Println(runtime.NumCPU()) //设置go程序可同时使用cpu的个数 runtime.GOMAXPROCS(4) }
2.不同协程之间的通讯问题(资源争夺)
主要体现在不能同时写入,会报错fatal error: concurrent map writes
(1)全局变量加锁同步 使用同步互斥锁
package sync :import "sync"
sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
type Mutex :type Mutex struct {// 包含隐藏或非导出字段}
Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。
func (*Mutex) Lock:func (m *Mutex) Lock()
Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。
func (*Mutex) Unlock:func (m *Mutex) Unlock()
Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。
package main import ( "fmt" "sync" "time" ) var ( map1 = make(map[int]int, 10) //lock是一个全局互斥锁 //sync是包:synchornized 同步 //Mutex: 互斥 lock sync.Mutex ) func test(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } //写入前上锁 lock.Lock() map1[n] = res lock.Unlock() //写入后解锁 } func main() { for i := 1; i <= 200; i++ { go test(i) } //休眠5s,这个时间不能把握,所以使用channel time.Sleep(time.Second * 5) //在读之前,因为系统不能确定主线程休眠时间是否足够协程使用,所以还需要加锁解锁 lock.Lock() for i, v := range map1 { fmt.Printf("map1[%v]=%v\n", i, v) } lock.Unlock() }
(2.)使用channel(管道)解决资源竞争
1)为什么需要channel
使用全局变量加锁同步来解决goroutine的通讯,但不完美
①主线程在等待所有goroutine全部完成的时间很难确定,这里设置的5s,仅是估算
②如果休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这是也会随主线程的退出而销毁
③通过全局变量加锁来实现通讯,也并不利用多个协程对全局变量的读写操作。
④上面种种分享都在呼唤一个新的通讯机制channel
2)channel的介绍
①channel本质就是一个数据结构-队列
②数据是先进先出(FIFO: first in first out)
③channel是线程安全,多个协程同时操作一个管道时,不会发生资源竞争问题,不用加锁
④channel时有类型的,一个string的channel只能存放string类型的数据
3)channel的定义/声明
var 变量名 chan 数据类型
举例:
var intChan chan int (intChan用于存放int数据)
var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
var perChan chan Person(结构体)
var perChan2 chan *Person(结构体指针)
...
说明
①channel是引用类型
②channel必须初始化才能写入数据,即make后才能使用
③管道是有类型的,intChan只能写入整数int
4)channel使用与注意事项
①channel中只能存放指定的数据类型
②channel的数据存满后,就不能再放入了
③在没有使用协程的情况下,如果channel数据取完了,再取,就会报错dead lock
package main import "fmt" type Cat struct { Name string Age int } func main() { //1.int //创建一个intchan,最多可以存放3个int,演示存3数据到intChan,然后再去除三个int var intChan chan int intChan = make(chan int, 3) intChan <- 10 intChan <- 20 intChan <- 30 n1 := <-intChan n2 := <-intChan n3 := <-intChan fmt.Println(n1, n2, n3) //2.map //创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入与读取 mapChan := make(chan map[string]string, 10) m1 := make(map[string]string, 20) m1["name1"] = "李逵" m1["name2"] = "林冲" m1["name3"] = "宋江" m2 := make(map[string]string, 20) m2["city1"] = "北京" m2["city2"] = "上海" m2["city3"] = "深圳" mapChan <- m1 mapChan <- m2 mc1 := <-mapChan mc2 := <-mapChan fmt.Println(mc1, mc2) //3.struct //创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和取出的用法 catChan := make(chan Cat, 10) c1 := Cat{"tom", 3} c2 := Cat{"jack", 12} catChan <- c1 catChan <- c2 cc1 := <-catChan cc2 := <-catChan fmt.Println(cc1, cc2) //4.*struct //创建一个catChan2,最多可以存放10个*Cat结构体变量,演示写入和取出的用法 catChan2 := make(chan *Cat, 10) c21 := Cat{"tom", 3} c22 := Cat{"jack", 12} catChan2 <- &c21 catChan2 <- &c22 cc21 := <-catChan2 cc22 := <-catChan2 fmt.Println(*cc21, *cc22) //5.interface{} //创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法 allChan := make(chan interface{}, 10) a1 := Cat{"tom!", 15} a2 := Cat{"lily", 6} allChan <- 32 allChan <- "张强" allChan <- a1 allChan <- a2 //希望得到管道中的第三个元素,则先将前两个数据推出 <-allChan <-allChan a11 := <-allChan fmt.Printf("a11=%T,a11=%v\n", a11, a11) //系统认为a11是空接口,所以应该是不存在.Name这个字段的,在编译阶段会报错。 //fmt.Printf("a11.Name=%v\n", a11.Name) //使用类型断言处理.Name a := a11.(Cat) fmt.Printf("a11.Name=%v\n", a.Name) }
5)channel的遍历和关闭
(1)channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读数据。
在内置包builtin的close函数
func close :func close(c chan<- Type)
内建函数close关闭信道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。对于已关闭的信道,语句:x, ok := <-c 还会将ok置为false。
func main() { //1.int //创建一个intchan,最多可以存放3个int,演示存3数据到intChan,然后再去除三个int var intChan chan int intChan = make(chan int, 3) intChan <- 10 intChan <- 20 intChan <- 30 close(intChan) intChan <- 40 n1 := <-intChan n2 := <-intChan n3 := <-intChan fmt.Println(n1, n2, n3) } //panic: send on closed channel
(2) channel的遍历
channel支持for-range的方式进行遍历(不能使用一般for循环是因为一般for循环len()为长度每次取出数据来长度会减少,所以若是100个数据,最后只能取出50个数据来),请注意两个细节。
①在遍历时,如果channel没有关闭,则会出现deadlock的错误
②在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
func main() { intChan2 := make(chan int, 100) for i := 0; i < 100; i++ { intChan2 <- i * 2 } //遍历管道不能使用普通的for循环 //for i :=0; i < len(intChan2); i++{ // //} //在遍历时,如果channel没有关闭,则会出现deadlock的错误 //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 close(intChan2) //使用for-range进行管道的遍历,管道只返回一个参数,而不会返回下标参数,因为下标是不可变的 for v := range intChan2 { fmt.Println("v=", v) } }
(3) goruntine和channel结合使用
解决主进程退出时机问题,避免协程没完成而主进程退出从而引起协程也退出的问题。
思路:设置两个管道,一个管道是写入和读取,另一个管道在读取完成后写入一个true,在主进程读取这个管道,若读取到false即管道关闭了,则退出主进程,此时协程也已经完成。
判断是否完成时不看exitChan的值是否为true,而是看v,ok中的ok是否为false,当管道关闭时,会给ok赋值为false,此时标志读取协程readData完成,主进程退出
package main import ( "fmt" ) func writeData(intChan chan int) { for i := 0; i < 50; i++ { intChan <- i fmt.Printf("writeData写入数据=%v\n", i) } close(intChan) //关闭 } func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } fmt.Printf("readData读到数据=%v\n", v) } //读取完成后,即任务完成,赋值管道exitData为true,传递给主进程,使主进程关闭 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 } } }
(4)管道阻塞
当管道设置的个数比较小,而写入的数据比较多,那么当有读取的动作,即使读的很慢也会进行等待,如果没有读取的动作而只有写入的动作,那么就会发生死锁
如果,编译器(运行),发现一个管道只有写没有读,则该管道会阻塞。如果管道有写有°,但是写管道和读管道的频率不一致,不会发生阻塞。
package main import ( "fmt" "time" ) func writeData(intChan chan int) { for i := 1; i <= 8000; i++ { intChan <- i } close(intChan) } func primeData(intChan chan int, primeChan chan int, exitChan chan bool) { for { flag := true v, ok := <-intChan if !ok { break } for i := 2; i < v; i++ { if v%i == 0 { flag = false break } } if flag { primeChan <- v } } fmt.Println("有一个primeData协程因为取不到数据,退出") //这里还不能关闭primeChan,因为有4个协程 exitChan <- true } func main() { intChan := make(chan int, 1000) primeChan := make(chan int, 2000) exitChan := make(chan bool, 4) start := time.Now().UnixMicro() //开启写入数据的协程 go writeData(intChan) //开启判断素数的协程 for i := 0; i < 4; i++ { go primeData(intChan, primeChan, exitChan) } //使用匿名函数起一个协程来判断4个primeChan协程是否完成 go func() { for i := 0; i < 4; i++ { <-exitChan } close(primeChan) end := time.Now().UnixMicro() fmt.Println("使用协程的时间是", end-start) }() //遍历primeNum,把结果取出 //channel如果只存不取,管道满了,存后面的数据会报错 //需要在这判断primeChan是否完成,进行主线程的等待 for { _, ok := <-primeChan if !ok { break } //fmt.Printf("primeData()v=%v\n", res) } fmt.Println("主线程结束") }
(5)注意事项和细节
①管道可以声明为只读或者只写
在默认情况下,管道是双向的
car chan1 chan int //可读可写
声明为只写
var chan2 chan<- int
声明为只读
var chan3 <-chan int
②使用select可以解决从管道取数据的阻塞问题
label := false for { select { case v := <-intChan: fmt.Printf("intChan v=%v\n", v) case v := <-stringChan: fmt.Printf("stringChan v=%v\n", v) default: fmt.Printf("找不到数据,退出") label = true //return } if label == true { break } }
③goruntine中使用recover,解决协程中出现的panic,导致程序崩溃问题
package main import ( "fmt" "time" ) func test1() { for i := 0; i < 10; i++ { fmt.Printf("test1()v=%v\n", i) } } func test2() { //这里可以使用defer + recover defer func() { //捕获test2抛出的panic if err := recover(); err != nil { fmt.Println("test()发生错误", err) } }() var m1 map[int]string m1[0] = "hello" //没有make,erro } func main() { go test1() go test2() time.Sleep(time.Second) }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2022-03-16 java项目报错javax.xml.bind不存在