go语言之并发、信道、select、mutex、异常处理
一、并发和并行
并发:同一时间段多个任务在执行(单个CPU执行多个任务)。go使用go协程(goroutine)和信道(channel)来处理并发
并行:同一时刻多个任务在执行(多个cpu支持)
1、Goroutine ---->协程
goroutine--->协程---2kb大小,100
线程----》几个m大小
go协程会复用线程
goroutine之间通信,通过信道channel通信
go推崇用信道通信,而不推崇用共享变量通信(锁,死锁)
2.1、启动
package main import ( "fmt" "time" ) //启动一个goroutine func test() { fmt.Println("go go go") } func main() { fmt.Println("主线程开始执行") go test() go test() go test() go test() go test() //写几个代表开几个goroutine time.Sleep(1*time.Second) //此时等待了才会打印go go go fmt.Println("主线程结束执行") //go语言中,主线程不会等待goroutine执行完成,要等待它结束需要自己处理 } //循环匿名函数开goroutine //func main() { // fmt.Println("主线程开始执行") // for i:=0;i<10;i++ { // go func(){ // fmt.Println("go go go ") // }() // } //此时开了10个goroutine,前面必须加go关键字 // time.Sleep(1*time.Second) // fmt.Println("主线程结束执行") // //go语言中,主线程不会等待goroutine执行完成,要等待它结束需要自己处理 //}
2.2、GMP模型
G:开的gorputine
M:当做操作系统真正的线程,实际上是用户线程
P:professor:现在版本默认情况是cpu核数(可当做cpu)
用户线程,操作系统线程
Python中,开的线程开出用户线程,用户线程跟操作系统线程1:1的对应关系
某些语言中,用户线程和操作系统线程是n:1的关系
go语言中,用户线程和操作系统线程是n:m的关系
3.2例子
package main import ( "fmt" "runtime" "time" ) func main() { //设置P的大小,认为是cpu核数即可 runtime.GOMAXPROCS(1) fmt.Println("主线程开始执行") go func() { for { fmt.Println("xxxx") } }() //for i:=0;i<10;i++ { // go func(){ // for { // fmt.Println("我是死循环") // } // }() //} time.Sleep(10*time.Second) fmt.Println("主线程结束执行") }
二、信道
不同goroutne之间通信,通过信道channel实现
1、信道创建
package main import ( "fmt" "time" ) func main() { //1 定义channel var c chan int //2 信道的零值(引用类型,空值为nil,当做参数传递时,不需要取地址,改的就是原来的,需要初始化再使用) fmt.Println(c) //3 信道初始化 c=make(chan int) //数字暂时先不关注 //4 信道的放值 (注意赋值和放值) //c<-1 //c=12 //赋值报错 //5 信道取值 //<-c //6 取出来赋值给一个变量 int //var a int //a=<-c //a:=<-c //7 信道默认不管放值还是取值,都是阻塞的 //c是引用类型 go test1(c) //这里取值, a:=<-c //阻塞 不但实现了两条协程之间通信,还实现了等待协程执行结束。取到值后就执行了,取不到值就阻塞 fmt.Println(a) } func test1(a chan int) { fmt.Println("go go go ") time.Sleep(1*time.Second) //往信道中放一个值 a<-10 //阻塞,此时这里放值 }
2、小例子
package main //程序有一个数中 每一位的平方和与立方和,然后把平方和与立方和相加并打印出来 import ( "fmt" "time" ) func calcSquares(number int, squareop chan int) { sum := 0 //总和 for number != 0 { digit := number % 10 //589对10取余数,9 8 5 sum += digit * digit //sum=9*9 8*8 5*5 number /= 10 //num=58 5 0 } time.Sleep(2*time.Second) squareop <- sum //上面计算值出来后,放到信道里,就阻塞到这里 } func calcCubes(number int, cubeop chan int) { sum := 0 for number != 0 { digit := number % 10 sum += digit * digit * digit number /= 10 } time.Sleep(1*time.Second) cubeop <- sum } //线性执行的情况 //func calcSquares(number int, squareop chan int) int{ // sum := 0 //总和 // for number != 0 { // digit := number % 10 //589对10取余数,9 8 5 // sum += digit * digit //sum=9*9 8*8 5*5 // number /= 10 //num=58 5 0 // } // time.Sleep(2*time.Second) // return sum 此时就是返回值 //} // //func calcCubes(number int, cubeop chan int) int{ // sum := 0 // for number != 0 { // digit := number % 10 // sum += digit * digit * digit // number /= 10 // } // time.Sleep(2*time.Second) // return sum //} func main() { ctime:=time.Now().Unix() //取结束前的时间 fmt.Println(ctime) number := 589 //给定一个值 sqrch := make(chan int) //因为是引用类型 cubech := make(chan int) //num1:=calcSquares(number, sqrch) //num2:=calcCubes(number, cubech) go calcSquares(number, sqrch) //开启goroutine go calcCubes(number, cubech) squares, cubes := <-sqrch, <-cubech //传值 //squares:= <-sqrch //cubes:=<-cubech fmt.Println("Final output", squares + cubes) ttime:=time.Now().Unix() fmt.Println(ttime) fmt.Println(ttime-ctime) } //最耗时的一个执行完就整个程序执行完
3、信道关闭、循环和死锁
package main import "fmt" //信道的死锁现象,默认都是阻塞的,一旦有一个放,没有人取 或者一个人取,没有人放,就会出现死锁 //func main() { // //var c chan int =make(chan int ) // //c<-1 //其实放不进去,阻塞在这,就死锁 // //<-c //没有,取不到,阻塞在这,就死锁 //} //单向信道 //func sendData(sendch chan<- int) { //这里放置的箭头<-代表只写信道 // sendch <- 10 //} // //func main() { // //sendch := make(chan<- int) //定义了一个只写信道 // sendch := make(chan int) //定义了一个可读可写信道 // go sendData(sendch) //传到函数中转成只写信道,在goroutine中,只负责写,不能往外读,主协程读 // fmt.Println(<-sendch) //只写信道一旦读,就有问题 //} ///3 关闭信道 //func main() { // sendch := make(chan int) // //关闭信道 // //close(sendch) // // //信道可以用for循环循环 //} // 信道关闭close(sendch) ,for循环循环信道,如果不关闭会报死锁,如果关闭了,放不进去,循环结束 func producer(ch chan int) { for i := 0; i < 100; i++ { fmt.Println("放入了",i) ch <- i //开始阻塞,后面放值就不阻塞了 } close(ch) } func main() { ch := make(chan int) go producer(ch) //开启goroutine for v := range ch { //此时循环放值 fmt.Println("Received ",v) } }
4、缓冲信道
只在缓冲已满的情况,才会阻塞向缓冲信道,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据
package main import ( "fmt" "sync" "time" ) //func main() { // var c chan int =make(chan int,6) //无缓冲信道数字是0,数字是缓冲大小,为6代表可以放6个 // c<-1 // c<-2 // c<-3 // c<-4 // c<-5 // c<-6 // //c<-7 //死锁 // <-c // <-c // //<-c // //<-c // //<-c // //<-c // //<-c // 取空了,死锁(一个goroutine中会出现) // 2 长度 vs 容量 // //fmt.Println(len(c)) //目前放了多少 // //fmt.Println(cap(c)) //可以最多放多少 //} //3 WaitGroup 等待所有goroutine执行完成 func process1(i int,wg *sync.WaitGroup) { fmt.Println("started Goroutine ", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d ended\n", i) //一旦有一个完成,减一 wg.Done() } func main() { var wg sync.WaitGroup //定义一个WaitGroup变量。没有初始化,值类型,当做参数传递,需要取地址 //fmt.Println(wg) for i:=0;i<10;i++ { wg.Add(1) //启动一个goroutine,add加1 go process1(i,&wg) } wg.Wait() // 一直阻塞在这,直到调用了10个done,计数器减到零 }
三、select使用
谁先取到数据,就先执行谁。随机选取
package main import ( "fmt" "time" ) //func server1(ch chan string) { // time.Sleep(6 * time.Second) // ch <- "from server1" //} // // //func server2(ch chan string) { // time.Sleep(3 * time.Second) // ch <- "from server2" // //} // //func main() { // output1 := make(chan string) // output2 := make(chan string) // //开启两个协程执行server // go server2(output1) // go server2(output2) // select { // case s1 := <-output1: // fmt.Println(s1,"ddddd") // case s2 := <-output2: // fmt.Println(s2,"yyyy") // } //} //func process(ch chan string) { // time.Sleep(10500 * time.Millisecond) // ch <- "process successful" //} // //func main() { // ch := make(chan string) // go process(ch) // for { // time.Sleep(1000 * time.Millisecond) // select { // case v := <-ch: // fmt.Println("received value: ", v) // return // default: // // 可以干其他事,模拟非阻塞式io // fmt.Println("no value received") // } // } // //} //死锁 //func main() { // ch := make(chan string) // select { // case <-ch: // } //} // 随机选取 func server1(ch chan string) { ch <- "from server1" } func server2(ch chan string) { ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) time.Sleep(1 * time.Second) select { //谁先回来就执行谁 case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) } }
四、mutex
使用锁的场景:多个goroutine通过共享内存在实现数据通信
临界区:当程序并发地运行时,多个 [Go 协程]同时修改共享资源的代码。这些修改共享资源的代码称为临界区。
如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的
package main import ( "fmt" "sync" ) //var x = 0 //全局,各个goroutine都可以拿到并且操作 //func increment(wg *sync.WaitGroup,m *sync.Mutex) { // m.Lock() //加锁 // x = x + 1 // m.Unlock() //解锁 // wg.Done() //} //func main() { // var w sync.WaitGroup // var m sync.Mutex //是个值类型,函数传递需要传地址,所以前面加* // fmt.Println(m) // for i := 0; i < 1000; i++ { // w.Add(1) // go increment(&w,&m) // } // w.Wait() // fmt.Println("final value of x", x) //} // 通过信道来做 var x = 0 func increment(wg *sync.WaitGroup, ch chan bool) { ch <- true // 缓冲信道放满了,就会阻塞 x = x + 1 <- ch wg.Done() } func main() { var w sync.WaitGroup ch := make(chan bool, 1) //定义了一个有缓存大小为1的信道 for i := 0; i < 1000; i++ { w.Add(1) go increment(&w, ch) } w.Wait() fmt.Println("final value of x", x) } // 不同goroutine之间传递数据:共享变量,通过信道 // 如果是修改共享变量,建议加锁 // 如果是协程之间通信,用信道
五、异常处理
与Python的异常捕获对比
defer:延迟执行,并且即便程序出现严重错误,也会执行
panic:主动抛出异常 raise
recover:恢复程序,继续执行
package main import ( "fmt" "os" ) //func main() { // defer fmt.Println("我最后执行") //注册一下,并不执行,等main函数执行完了以后,从下往上执行defer定义的东西 // defer fmt.Println("我倒数第二个打印") // fmt.Println("我先执行") // //var a []int // //fmt.Println(a[10]) // panic("我出错了") // fmt.Println("ccccc") // // //假设打开一个文件 // //f:=open() // //defer f.close() // // // //出错了 // // // //} //例子 //func f1(){ // fmt.Println("f1 f1") //} //func f2(){ // defer func() { //这个匿名函数永远会执行 // //error:=recover() //恢复程序继续执行 // //fmt.Println(error) //如果没有错误,执行recover会返回nil 如果有错误,,执行recover会放错误信息 // if error:=recover();error!=nil{ // //表示出错了,打印一下错误信息,程序恢复了,继续执行 // fmt.Println(error) // } // // 相当于finally // fmt.Println("我永远会执行,不管是否出错") // }() // fmt.Println("f2 f2") // //panic("主动抛出错误") //} //func f3(){ // fmt.Println("f3 f3") //} // //func main() { // //捕获异常,处理异常,让程序继续运行 // // f1() // // f2() // f3() //} /* try: 可能会错误的代码 except Exception as e: print(e) finally: 无论是否出错,都会执行 */ /*现在这么写 defer func() { if error:=recover();error!=nil{ //except的东西 fmt.Println(error) } //相当于finally,无论是否出错,都会执行 }() 可能会错误的代码 */ // go的错误处理 func main() { f, err := os.Open("/test.txt") if err != nil { fmt.Println(err) return } fmt.Println(f.Name(), "opened successfully") }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具