Go语言之路————并发

news2025/5/20 22:57:23

Go语言之路————并发

  • 前言
  • 协程
  • 管道
  • Select
  • sync
    • WaitGroup

前言

  • 我是一名多年Java开发人员,因为工作需要现在要学习go语言,Go语言之路是一个系列,记录着我从0开始接触Go,到后面能正常完成工作上的业务开发的过程,如果你也是个小白或者转Go语言的,希望我这篇文章对你有所帮助。
  • 有关go其他基础的内容的文章大家可以查看我的主页,接下来主要就是把这个系列更完,更完之后我会在每篇文章中挂上连接,方便大家跳转和复习。

协程

在学go之前,大家肯定听说过go底层天然支持并发,相信这也是很多人选择学习这款语言的原因之一,那么它到底怎么个天然法,怎么个支持,下面我就一一道来。

Goroutine(轻量级线程),正如标题一样,它也叫做协程,它是go的并发执行单元,是一种比线程更加轻量级的单位,创建一个协程非常简单,只需要用到一个关键词:go,go后面一定要更一个函数:

func main() {
	go func() {
		fmt.Print(1)
	}()
}

我这里用一个go启动一个匿名函数,如果你copy这个代码去执行,你会发现控制台没有任何打印,因为协程就跟Java的线程一样,它是并发去执行的,当我们的main方法跑完的时候,如果协程未执行,那么 整个程序都会关掉,就没有任何输出了。

那怎样让它正常输出呢?聪明的同学肯定会想到,让main线程沉睡一下不就行了,我们来看看代码:

func main() {
	go func() {
		fmt.Print(1)
	}()
	time.Sleep(1 * time.Second)
}

控制台打印:1

由此可见,让主线程沉睡确实可以做到这点,那么我就要提出下一个问题了,如果有多个协程呢?看看下面代码:

func main() {
	for i := 0; i < 10; i++ {
		go fmt.Println(i)
	}
	time.Sleep(1 * time.Second)
}

当把这段代码执行后,你会发现每次执行的结果都是不一样的,这也引出了协程的一个特性,那就是执行的时候是无序的,那有啥方法解决吗,我们先用上面的sleep看能否解决:
每次执行协程前,我们都让它沉睡一秒,然后主线程沉睡十秒

func main() {
	for i := 0; i < 10; i++ {
		time.Sleep(1 * time.Second)
		go fmt.Println(i)
	}
	time.Sleep(10 * time.Second)
}

执行后的结果:

0
1
2
3
4
5
6
7
8
9

目前来看,是做到了,但是这个方法太笨了,有啥办法可以优雅的解决吗,当然,go提供了管道、信号量、上下文、锁等各种工具来辅助开发者进行并发编程。

管道

管道:channel,官方对它的解释:Do not communicate by sharing memory; instead, share memory by communicating.
我用白话文在翻译一次:它的作用就是解决协程之间的通信的,数据传输或者共享的。
一个通道,用chan来定义,定义的时候必须要指定它存的数据类型:

var ch chan int

此时的管道还没初始化,是不能使用的,在go中,初始化一个管道,有且只有一个办法,那就是make关键词,make关键词提供一个额外参数:缓冲区

var ch = make(chan int, 1)

这里就是用make创建了一个缓冲区为1的管道,先看看使用:

func main() {
	var ch = make(chan int, 1)
	ch <- 1
	println(<-ch)
}
输出:1

结合例子,说一下管道的输出和输出:<-,没错就是用箭头表示,箭头的指向表示数据流向,a <- 1,表示把1发到a,<- a,表示从a读取数据

如何理解缓冲区:可以理解为Java中线程池中的阻塞队列,往管道中发送的数据会先存到缓冲区,然后才会被读取,如果一个管道没有缓冲区,那么发送信息后需要立马有读取的操作,否则程序就会阻塞,我们通过下面例子来看:

func main() {
	var ch = make(chan int)
	ch <- 1
	<-ch
}

我们创建一个没有缓冲区的管道,像管道里面输入1,马上再读取。看似人畜无害的代码,执行起来确是这个结果:deadlock

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	D:/goland/workspace/test/main.go:5 +0x2d

Process finished with the exit code 2

那读者又会想了,既然这样,那岂不是所有的管道创建都需要缓冲区。其实不然,如果我们通过协程去输入就能正常输出:

func main() {
	var ch = make(chan int)
	go func() {
		ch <- 1
	}()
	println(<-ch)
}
输出:1

思考:为啥有协程的参与就能正常读写?我们回到缓冲区的本质,它是存数据的缓冲的,如果我们没有缓冲区,那么证明这个管道是没办法存数据的,就意味着,我这边写了,必须马上有人读,但是通过同步操作是实现不了的,有协程异步来操作才可行。

注意:每个管道用完后需要我们手段关闭,直接调用系统提供的close方法,一个管道只能close一次,多次close会报错

func close(c chan<- Type)

但是通常,我们建议把通道的关闭结合defer来用:

func main() {
	var ch = make(chan int)
	go func() {
		ch <- 1
		defer close(ch)
	}()
	println(<-ch)
}

注意点,除了同步读写无缓冲管道会造成堵塞之外,下面几种情况也会造成deadlock:

  1. 缓冲区满了继续噻数据:
    func main() {
    	var ch = make(chan int, 1)
    	defer close(ch)
    	ch <- 1
    	ch <- 1
    	println(<-ch)
    }
    
    缓冲区大小为1,写入一个后满了没读,继续写
  2. 有缓冲区,但是数据为空
    func main() {
    	// 创建的有缓冲管道
    	intCh := make(chan int, 1)
    	defer close(intCh)
    	// 缓冲区为空,阻塞等待其他协程写入数据
    	<-intCh
    }
    
  3. 管道未初始化
    func main() {
      var intCh chan int
      intCh <- 1
    }
    

管道数据除了一个个读之外,我们还可以用for range来遍历一个管道:

func main() {
	intCh := make(chan int, 10)
	go func() {
		for i := 0; i < 10; i++ {
			intCh <- i
		}
	}()
	for ch := range intCh {
		println(ch)
	}
}

看看输出:

0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	D:/goland/workspace/test/main.go:10 +0xa8

在输出之后出现了阻塞,这是因为for range会一直去读写管道中的数据,当管道中数据为空时就会死锁,直到有其他协程向管道写入数据才会解除,所以我们代码改一下,在写入数据完毕后就关闭管道:

func main() {
	intCh := make(chan int, 10)
	go func() {
		for i := 0; i < 10; i++ {
			intCh <- i
		}
		close(intCh)
	}()
	for ch := range intCh {
		println(ch)
	}
}

最后再补充一个知识点,管道的读取其实是有返回值的:

v, ok := <-intCh

第一个是值,第二个是个bool代表是否读取成功:

func main() {
	intCh := make(chan int, 10)
	go func() {
		intCh <- 1
	}()
	a, ok := <-intCh
	println(a, ok)
}

输出:1 true

Select

在 Go 中,select 是一种管道多路复用的控制结构,某一时刻,同时监测多个元素是否可用,在这里我们可以用来检测多个管道:

func main() {
	ch1 := make(chan int, 10)
	ch2 := make(chan int, 10)
	ch3 := make(chan int, 10)
	defer func() {
		close(ch1)
		close(ch2)
		close(ch3)
	}()
	select {
	case i := <-ch1:
		fmt.Println("ch1 is ", i)
	case j := <-ch2:
		fmt.Println("ch2 is ", j)
	case k := <-ch3:
		fmt.Println("ch3 is ", k)
	default:
		fmt.Print("检测失败")
	}
}

创建三个管道,然后用select分别去监测三个管道的数据,然后doSomething,让我们没有往管道输入任何数据的时候,默认输出检测失败,我们在select前往ch1输入一个数据看看:

func main() {
	ch1 := make(chan int, 10)
	ch2 := make(chan int, 10)
	ch3 := make(chan int, 10)
	defer func() {
		close(ch1)
		close(ch2)
		close(ch3)
	}()
	ch1 <- 1
	select {
	case i := <-ch1:
		fmt.Println("ch1 is ", i)
	case j := <-ch2:
		fmt.Println("ch2 is ", j)
	case k := <-ch3:
		fmt.Println("ch3 is ", k)
	default:
		fmt.Print("检测失败")
	}
}

输出:ch1 is  1

sync

讲到了并发,怎么能离开锁,go的sync包下面提供了很多锁相关的工具类,就类似于Java的juc包,我们下面简单说点常用的。

WaitGroup

WaitGroup 即等待执行,它的方法只有三个,使用起来也非常简单:

  • Add:添加一个计数器,表示总数
  • Done:每调用一次计数器减1
  • Wait:如果计数器不为0,则等待

还记得我们文章开头提到的例子吗,就是在main线程中使用了协程,协程还未执行但是main已经结束了,当时我们用的是sleep方法,现在我们看看怎么用WaitGroup去解决这个问题:
先看看原例子:

func main() {
	println("start")
	go func() {
		println("doSomething")
	}()
	println("end")
}

再看看解决后的:

var waitGroup sync.WaitGroup

func main() {
	println("start")
	waitGroup.Add(1)
	go func() {
		println("doSomething")
		waitGroup.Done()
	}()
	waitGroup.Wait()
	println("end")
}

看看输出:
start
doSomething
end

go中常用的锁有两个:

  • 互斥锁:sync.Mutex
  • 读写锁:sync.RWMutex

互斥锁sync.Mutex ,实现了Locker 接口,它的用法非常简单,就三个:

func (m *Mutex) Lock() {
	m.mu.Lock()
}

func (m *Mutex) TryLock() bool {
	return m.mu.TryLock()
}

func (m *Mutex) Unlock() {
	m.mu.Unlock()
}

我们先来看看互斥锁Mutex,下面我来模拟一个经典的场景,就是不同线程对共享数据操作,让我们看看不用锁的情况下,会不会得到正确结果:

var wait sync.WaitGroup
var count = 0

func main() {
	wait.Add(10)
	for i := 0; i < 10; i++ {
		go func(data *int) {
			// 模拟访问耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 访问数据,这里必须要用temp当前数据存起来
			temp := *data
			// 模拟计算耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 修改数据
			*data = temp + 1
			fmt.Println(*data)
			wait.Done()
		}(&count)
	}
	wait.Wait()
	fmt.Println("最终结果", count)
}

运行起来发现,每次的输出都不一样,跟Java一样,多线程对共享数据的修改是不安全的,必须要加锁

1
1
2
1
1
1
1
1
1
3
最终结果 3

下面我们改进一下代码,将同步代码用互斥锁包起来,类似于Java的同步代码块:

var lock sync.Mutex
var wait sync.WaitGroup
var count = 0

func main() {
	wait.Add(10)
	for i := 0; i < 10; i++ {
		go func(data *int) {
			lock.Lock()
			// 模拟访问耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 访问数据
			temp := *data
			// 模拟计算耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 修改数据
			*data = temp + 1
			lock.Unlock()
			fmt.Println(*data)
			wait.Done()
		}(&count)
	}
	wait.Wait()
	fmt.Println("最终结果", count)
}

go的互斥锁很简单,用的时候就调用lock()方法,解锁就调用unlock()方法,看看输出:

1
2
3
4
5
6
7
8
9
10
最终结果 10

Process finished with the exit code 0

读写锁和互斥锁一样,只是说读写锁的精度更高一点,可以根据读多写少,或者读少写多的情况来判断,它同样实现了Locker接口,只是方法多一些,读写锁内部的读和写是互斥锁,并不是说有两个锁

// 加读锁
func (rw *RWMutex) RLock()

// 尝试加读锁
func (rw *RWMutex) TryRLock() bool

// 解读锁
func (rw *RWMutex) RUnlock()

// 加写锁
func (rw *RWMutex) Lock()

// 尝试加写锁
func (rw *RWMutex) TryLock() bool

// 解写锁
func (rw *RWMutex) Unlock()

下面看个读写锁的例子(本例来自官方中文文档):

var wait sync.WaitGroup
var count = 0
var rw sync.RWMutex

func main() {
	wait.Add(12)
	// 读多写少
	go func() {
		for i := 0; i < 3; i++ {
			go Write(&count)
		}
		wait.Done()
	}()
	go func() {
		for i := 0; i < 7; i++ {
			go Read(&count)
		}
		wait.Done()
	}()
	// 等待子协程结束
	wait.Wait()
	fmt.Println("最终结果", count)
}

func Read(i *int) {
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
	rw.RLock()
	fmt.Println("拿到读锁")
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
	fmt.Println("释放读锁", *i)
	rw.RUnlock()
	wait.Done()
}

func Write(i *int) {
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
	rw.Lock()
	fmt.Println("拿到写锁")
	temp := *i
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
	*i = temp + 1
	fmt.Println("释放写锁", *i)
	rw.Unlock()
	wait.Done()
}

该例开启了 3 个写协程,7 个读协程,在读数据的时候都会先获得读锁,读协程可以正常获得读锁,但是会阻塞写协程,获得写锁的时候,则会同时阻塞读协程和写协程,直到释放写锁,如此一来实现了读协程与写协程互斥,保证了数据的正确性。例子输出如下:

拿到读锁
拿到读锁
释放读锁 0
释放读锁 0
拿到写锁
释放写锁 1
拿到读锁
拿到读锁
拿到读锁
拿到读锁
拿到读锁
释放读锁 1
释放读锁 1
释放读锁 1
释放读锁 1
释放读锁 1
拿到写锁
释放写锁 2
拿到写锁
释放写锁 3
最终结果 3

Process finished with the exit code 0

OK 上面就是go中并发的一些常用案例,不多,但是一定是最常用的,掌握了这些你就可以去深入扩展了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2380298.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Logrotate:配置日志轮转、高效管理Linux日志文件

Logrotate 是 Linux 系统中用于自动化管理日志文件的工具&#xff0c;能够定期轮转、压缩、删除日志文件&#xff0c;确保系统日志不会无限制增长&#xff0c;占用过多磁盘空间。 它通常由 Cron 作业定期执行&#xff0c;也可以手动触发。 1. &#x1f527; 核心功能 日志轮转…

贵州某建筑物挡墙自动化监测

1. 项目简介 某建筑物位于贵州省某县城区内&#xff0c;靠近县城主干道&#xff0c;周边配套学校、医院、商贸城。建筑物临近凤凰湖、芙蓉江等水系&#xff0c;主打“湖景生态宜居”。改建筑物总占地面积&#xff1a;约5.3万平方米&#xff1b;总建筑面积&#xff1a;约15万平…

nginx服务器实验

1.实验要求 1&#xff09;在Nginx服务器上搭建LNMP服务&#xff0c;并且能够对外提供Discuz论坛服务。 在Web1、Web2服务器上搭建Tomcat 服务。 2&#xff09;为nginx服务配置虚拟主机&#xff0c;新增两个域名 www.kgc.com 和 www.benet.com&#xff0c;使用http://www.kgc.…

高速光耦在通信行业的应用(五) | 5Mbps通信光耦的特性

针对5MBd速率光耦市场&#xff0c;晶台推出KL2200、KL2201和KL2202系列光耦 ,对标大部分国外品牌产品的应用&#xff1b;它分别由一个红外发射二极管和一个高速集成光电检测器逻辑门组成。 它采用 8 引脚 DIP 封装&#xff0c;并提供 SMD 选项。KL2200 的检测器具有一个三态输出…

Apidog MCP服务器,连接API规范和AI编码助手的桥梁

#作者&#xff1a;曹付江 文章目录 1.了解 MCP2.什么是 Apidog MCP 服务器&#xff1f;3.Apidog MCP 服务器如何工作4.利用人工智能改变开发工作流程5.设置 Apidog MCP 服务器&#xff1a; 分步指南5.高级功能和提示5.1 使用 OpenAPI 规范5.2.多个项目配置5.3.安全最佳实践5.4…

国内MCP服务平台推荐 AIbase推出MCP服务器客户端商店

在当今数字化时代&#xff0c;人工智能&#xff08;AI&#xff09;技术正以前所未有的速度发展&#xff0c;不断改变着我们的生活和工作方式。2025年&#xff0c;AI领域迎来了一项重要的技术进展——MCP(Model Context Protocol&#xff0c;模型上下文协议)的广泛应用。这一技术…

Profinet转Ethernet IP主站网关:点燃氢醌生产线的智慧之光!

案例分享&#xff1a;转角指示器和Profinet转EthernetIP网关的应用 在现代工业自动化中&#xff0c;设备和系统之间的高效通信至关重要。最近&#xff0c;我们在某大型化工企业的生产线上实施了一个项目&#xff0c;旨在通过先进的设备和通信技术提高生产效率和安全性。该项目…

爬虫攻防战:从入门到放弃的完整对抗史与实战解决方案

爬虫攻防战:从入门到放弃的完整对抗史与实战解决方案 这张有趣的图片生动描绘了爬虫开发者与反爬工程师之间的"军备竞赛"。作为技术博主,我将基于这张图的各个阶段,深入分析爬虫技术的演进与对应的反制措施,提供一套完整的反爬解决方案,包括技术原理、实施方法…

[ctfshow web入门] web75

信息收集 启用了open_basedir&#xff0c;所以之前的方法又不能用了 解题 cforeach(new DirectoryIterator("glob:///*") as $a){echo($a->__toString(). ); } ob_flush();cif ( $a opendir("glob:///*") ) {while ( ($file readdir($a)) ! false …

交流学习 | 江西同为科技有限公司赴海尔总部考察交流

2025年4月8日至9日&#xff0c;江西同为科技有限公司在江西省科技装备商会的带领下&#xff0c;以蔡文君经理为代表&#xff0c;一行人赴山东青岛海尔总部开展两天的考察交流活动。本次考察不仅深入剖析了海尔企业的前沿技术与管理理念&#xff0c;更促进了行业内科技创新、商业…

React方向:react的基本语法-数据渲染

1、安装包(js库) yarn add babel-standalone react react-dom 示例图.png 2、通过依赖包导入js库文件 <script src"../node_modules/babel-standalone/babel.js"></script> <script src"../node_modules/react/umd/react.development.js"&g…

RK3568-鸿蒙5.1镜像烧录与调试

参考https://gitee.com/hihope_iot/docs/blob/master/HiHope_DAYU200/docs/%E7%83%A7%E5%BD%95%E6%8C%87%E5%AF%BC%E6%96%87%E6%A1%A3.md https://blog.csdn.net/pengjiadashaoye/article/details/144448126 固件烧录 缺了3个 , 没找着,烧录试试看 ,看了参考也不太一样 缺了…

游戏引擎学习第294天:增加手套

准备战斗 我们正在进行的是第294天的开发&#xff0c;目前暂时没有特别确定要做的内容&#xff0c;但我们决定继续研究移动模式相关的部分。虽然一些小型实体系统已经在运行&#xff0c;但并不确定最终效果如何。 今天我们决定实现一个全新的功能&#xff1a;战斗系统。这是游…

C# Try Catch Finally 执行顺序是什么?有返回值呢?

Try Catch Finally 执行顺序是什么&#xff1f;有返回值呢&#xff1f; 大部分程序员都认为&#xff1a;C#异常处理执行顺序&#xff0c;很简单&#xff0c;没什么可说的。 正常情况&#xff1a;执行顺序为 1、3(下图) 异常情况&#xff1a;执行顺序为1、2、3 文章目录 Tr…

水库雨水情测报与安全监测系统解决方案

一、方案概述 本水库雨水情测报与安全监测解决方案的核心目标在于利用尖端的技术手段&#xff0c;确保对水库雨水情势以及大坝安全状况的持续监控和及时预警&#xff0c;从而为水库的稳定运行提供坚实的支持和保障。该方案严格遵循“统筹协调、因库制宜、实用有效、信息共享”的…

架构选择/区别

目录 一、分层架构&#xff08;Layered Architecture&#xff09; 二、微服务架构&#xff08;Microservices Architecture&#xff09; 三、分布式架构&#xff08;Distributed Architecture&#xff09; 四、单体架构&#xff08;Monolithic Architecture&#xff09; 五…

嵌入式学习笔记 - STM32 ADC 模块工作模式总结

ADC 模式总结&#xff1a; 一 单ADC模式&#xff08;是指ADC1,ADC2,ADC3中只有一个ADC被使用&#xff09; ①单通道&#xff1a; 非连续模式&#xff1a;非连续的意思就是单次&#xff0c;一次转换完成后就停止转换&#xff0c;除非再次被软件或者被外部触发启动&#xff1b…

IPLOOK | 2025 MVNOs 世界大会:从Wi-Fi通话到卫星覆盖

2025 MVNOs 世界大会于5月12日至14日在奥地利维也纳举行&#xff0c;汇聚了来自50多个国家的550余位行业领袖&#xff0c;共同探讨移动虚拟网络运营商&#xff08;MVNO&#xff09;领域的变革趋势。本届大会聚焦数字化转型、技术创新与战略合作&#xff0c;其中IPLOOK凭借其创新…

零基础搭建!基于PP-ShiTuV2的轻量级图像识别系统(Docker+API部署指南)

以下是对该图像分类识别系统的的简单介绍&#xff1a; PP-ShiTuV2 是一个由百度飞桨团队发布的实用轻量级通用图像识别系统&#xff0c;由主体检测、特征提取、向量检索三个模块构成&#xff0c;适用于快速构建轻量级、高精度、可落地的图像识别应用image_classification是一个…

【C语言】贪吃蛇小游戏

文章目录 前言一、贪吃蛇游戏代码test.c文件Snake.h文件Snake.c文件 二、相关函数的介绍1.COORD2.Win32 API的介绍3.GetStdHandle4.GetConsoleCursorInfo5.CONSOLE_CURSOR_INFO5.SetConsoleCursorInf6.SetConsoleCursorPosition7.GetAsyncKeyState 总结 前言 哈喽各位好呀。今…