golang并发编程
并发编程的工具
在golang中,并发编程是比较简单的,不像java中那么麻烦,golang天然的支持协程(线程)的管理,通常用goroutine和channel来管理go的并发编程。
goroutine介绍
goroutine在go语言中叫做协程,相当于java中线程的概念,使用起来也非常简单,在方法前面加个go就行了。
go Run() 例如这样就是开启一个新的协程去跑Run方法
协程管理器sync.WaitGroup
在使用协程时,如果主线程结束的时候,我们并不能知道协程里面发生了什么,也无法控制它,因此引入了sync.WaitGroup,来控制协程。
我们来看以下这个例子,在协程启动的时候开启了一个协程WaitGroup,当Add里面有数字的时候,当数字减到0才能结束。
go
func main() {
var wg sync.WaitGroup
wg.Add(1)
go Run(&wg)
wg.Wait()
}
func Run(wg *sync.WaitGroup) {
fmt.Println("我跑起来了")
wg.Done()
}
我们首先测试一下方法
wg.Add(1) ===> 输出结果:我跑起来了
wg.Add(0) ===> 输出结果:空
wg.Add(2) ===> 输出结果:我跑起来了 fatal error: all goroutines are asleep - deadlock!
我们可以看到这里有个报错,在 main 函数中使用了 wg.Wait() 来等待两个协程完成任务,但是在 Run 函数中,你只是调用了 wg.Done() 来通知 WaitGroup 任务完成,但是没有关闭等待。
因此推荐以下这种写法,比较安全,保证最终关闭等待。
go
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
Run()
}()
go func() {
defer wg.Done()
Run()
}()
wg.Wait()
}
func Run() {
fmt.Println("我跑起来了")
}
channel介绍
channel是用来负责协程之间的通信的,可以理解为一个管道,我们首先举一个最简单的例子,箭头表示读取,这样就表示往管道里面读入和获取一个数了。
go
func main() {
c1 := make(chan int, 1)
c1 <- 1
fmt.Println(<-c1)
}
c1 := make(chan int, 1) 这里的1代表缓冲区的容量,当有缓冲区的时候,存入数据会预先存进缓冲区,当需要的时候再取出。当c1 := make(chan int) 这样写的时候,则代表没有容量,写入的时候会阻塞,除非有其他goroutine进行消费,否则无法执行下一步操作。
因此调用下面这个方法则会造成死锁。
go
func main() {
c1 := make(chan int)
c1 <- 1
fmt.Println(<-c1)
}
但是如果是异步执行的话则可以正常执行获取,因为这个写入的操作跑到另一个cpu线程(时间分片)上去了,不会对主线程造成阻塞,当主线程去获取这个值的时候,则可以顺利拿到。
go
func main() {
c1 := make(chan int)
go func() {
c1 <- 1
}()
fmt.Println(<-c1)
}
下面我们再来看看缓冲区这个概念,以下面这段代码为例,无缓冲区时,向channel中存入则阻塞,有缓冲区时更像一种生产者消费者模型,充满缓冲区才阻塞。
go
func main() {
//c1 := make(chan int)
//c1 := make(chan int, 5)
c1 := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
c1 <- i
}
}()
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println(<-c1)
}
}
我们在这里打上断点可以很明显的看到变化。
readChannel和writeChannel
在channel中可以定义可读和可写channel
go
func main() {
c1 := make(chan int, 5)
var read <-chan int = c1
var write chan<- int = c1
write <- 1
fmt.Println(<-read)
}
close的用法
close可以关闭一个channel,使其无法往里面写,但是仍然可以往里面读。
go
func main() {
c1 := make(chan int, 5)
c1 <- 1
c1 <- 2
c1 <- 3
c1 <- 4
c1 <- 5
close(c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
fmt.Println(<-c1)
}
此时不用close也是可以执行的,但是当我们使用循环取的时候,必须要用close。
go
func main() {
c1 := make(chan int, 5)
c1 <- 1
c1 <- 2
c1 <- 3
c1 <- 4
c1 <- 5
close(c1)
for v := range c1 {
fmt.Println(v)
}
}
select的用法
我们以一下代码为例
go
func main() {
c1 := make(chan int, 1)
c2 := make(chan int, 1)
c3 := make(chan int, 1)
c1 <- 1
c2 <- 1
c3 <- 1
select {
case <-c1:
fmt.Println("c1")
case <-c2:
fmt.Println("c2")
case <-c3:
fmt.Println("c3")
default:
fmt.Println("都没执行")
}
}
使用select可以将能执行的都执行,比如上面的代码执行顺序就是三个case随机一个或全部执行。
通讯示例
go
func main() {
var wg sync.WaitGroup
wg.Add(2) // 增加等待组计数器,表示有两个协程需要等待
ch := make(chan int) // 创建整数类型的通道
go producer(ch, &wg) // 启动生产者协程
go consumer(ch, &wg) // 启动消费者协程
wg.Wait() // 等待所有协程执行完毕
}
func producer(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("Produced:", i)
ch <- i
}
close(ch) // 关闭通道,表示生产者完成生产
}
func consumer(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for data := range ch {
fmt.Println("Consumed:", data)
}
}
我们可以通过这种方式实现一个简单的生产者消费者模型来实现线程交互。
总结
在go语言中,我们完全可以通过goroutine和channel实现线程之间的通讯,来实现协程之间协调。