【gogogo专栏】golang并发编程

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实现线程之间的通讯,来实现协程之间协调。

相关推荐
步行cgn23 分钟前
YAML 的语法规则
spring boot·后端
骇客野人40 分钟前
SpringBoot电商系统用户注册、登录、留存及数据埋点设计与落地实施方案
java·spring boot·后端
IT_陈寒4 小时前
React的状态更新坑得我差点加班到天亮
前端·人工智能·后端
码事漫谈5 小时前
一天搭出 80% 的 Agent,然后卡在 95% 到死
后端
leonkay5 小时前
C# 锁机制——【2】深入原理
开发语言·后端·spring·c#·个人开发
江湖十年5 小时前
在 Go 中使用 dyno 包处理动态对象
后端·面试·go
八角丶5 小时前
Node 网络编程 —— TLS 模块
javascript·后端·node.js
山岚的运维笔记5 小时前
mysql 专业笔记 -- 第 8 章:使用变量
运维·数据库·笔记·后端·学习·mysql·dba
平头哥AI7 小时前
Day 01 | go run 跑通第一个 Go 程序,go build 留下一个能拷走的 exe
开发语言·后端·golang
2601_962296518 小时前
如何使用Golang包路径_管理本地和远程模块引用
golang·数据转换·变量定义·常用库·编程语言区别