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

相关推荐
狗头大军之江苏分军1 小时前
《潮水漫过十七岁》开学了
后端
苏三说技术2 小时前
如何看待GPT-6在UP主众测中碾压夺冠?它是现在最强大模型吗?
后端
mldong2 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
wno7042 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
Captaincc2 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
aramae3 小时前
模拟实现strlen()函数 (C语言)
c语言·开发语言·后端
参.商.3 小时前
【Day 53】76. 最小覆盖子串
leetcode·golang
IT_陈寒4 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
分支预测失败4 小时前
RISC-V 时间子系统深度专题:mtime 访问路径、SBI 定时器与 Linux tickless 协同
后端
65岁退休Coder4 小时前
PI Agent 开发一个生产级 Harness
后端·node.js·agent