首页 > 编程语言 >还不敢写多线程程序?看看Go如何让并发控制简单有趣

还不敢写多线程程序?看看Go如何让并发控制简单有趣

时间:2024-01-15 21:49:31浏览次数:31  
标签:互斥 goroutine sync 并发 Go 操作 多线程 通道

还不敢写多线程程序?看看Go如何让并发控制简单有趣

原创 萤火架构 萤火架构 2024-01-12 19:50 发表于北京 听全文

所谓并发控制,就是同一程序进程内不同线程间访问相同资源时的冲突处理,有时也称为进程内同步。比如一个简单的内存累加计数操作,如果不进行同步,不同的线程可能就会获取到同样的数值,累加出相同的结果,最终结果也就不准确了。如下图所示,线程1和线程2按照图中的顺序操作变量n就会出现同步问题。进程内同步就是用来解决这种问题的。

在Go语言中,应用程序没有直接使用线程,使用的是一种轻量级的线程:goroutine,很多时候也被称为协程,所以Go语言中进程内的同步就是协程之间的同步。在Go语言中进程内同步有多种不同的实现机制,主要包括sync包下的工具和channel(通道)。接下来,我将逐一介绍这些同步机制,它们的用途、原理和应用实例,让大家对Go的进程内同步有个清晰的认识。

sync包的同步机制

1. atomic原子操作

原子操作在Go中是通过atomic包提供的,其底层是通过硬件CPU的支持实现的。原子操作能够确保变量的操作在计算机的最基本操作层面是不可分割的,从而避免竞态条件。

原子操作的优点是效率高,因为它们不需要复杂的锁机制。但缺点是原子操作只适用于简单的数据操作,对于复杂的同步需求,原子操作就不太适用了。

在Go语言中,atomic包提供了一系列的函数来执行原子性的增加、减少、加载和存储等操作。

我们来看一个计数器的例子:

package main

import (
"fmt"
"sync"
"sync/atomic"
)

func main() {
var count int32
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for c := 0; c < 1000; c++ {
atomic.AddInt32(&count, 1)
}
}()
}

wg.Wait()
fmt.Println("Count:", count)
}

在这个例子中,我们创建了5个goroutine,每个都尝试对`count`变量增加1000次。我们使用`atomic.AddInt32`来确保增加操作的原子性。最后,我们使用`WaitGroup`来等待所有goroutine完成后,并打印出最终的`count`值,这个值应该是5000。

2. mutex互斥锁

互斥锁是一种常见的同步机制,用于保护共享资源,确保一次只有一个goroutine可以访问该资源。

Go的sync包提供了Mutex类型来实现互斥锁。通过LockUnlock方法控制锁的获取和释放。

互斥锁可以解决复杂的同步问题,但它可能会导致性能问题,比如锁竞争和死锁。适当地使用互斥锁是并发编程的一门艺术。

我们还是以计数器为例,来演示互斥锁的使用方法:

package main

import (
"fmt"
"sync"
)

func main() {
var count int
var lock sync.Mutex
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for c := 0; c < 1000; c++ {
lock.Lock()
count++
lock.Unlock()
}
}()
}

wg.Wait()
fmt.Println("Count:", count)
}

可以看到我们只是修改了数字累加部分的代码,将原子操作替换为了锁操作。

互斥锁的用途十分广泛,我们再举个例子:在Web服务中,可能会有多个goroutine同时尝试写日志到同一个文件,使用互斥锁可以确保日志的顺序和完整性。

package main

import (
"fmt"
"log"
"os"
"sync"
"time"
)

// 日志文件的全局变量和互斥锁
var (
logFile *os.File
mutex sync.Mutex
)

// 初始化日志文件
func init() {
var err error
logFile, err = os.OpenFile("webserver.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
}

// 写日志的函数,这里使用互斥锁来同步
func logMessage(message string) {
mutex.Lock() // 在写入之前,锁定
defer mutex.Unlock() // 使用defer来确保互斥锁会被解锁

// 这里模拟写入日志需要一些时间
time.Sleep(time.Second)
logFile.WriteString(time.Now().Format("2006-01-02 15:04:05") + " - " + message + "\n")
}

func main() {
// 模拟web服务,启动5个goroutine尝试写日志
for i := 0; i < 5; i++ {
go func(id int) {
logMessage(fmt.Sprintf("Log entry from goroutine %d", id))
}(i)
}

// 等待足够的时间让goroutine完成日志写入
time.Sleep(10 * time.Second)

// 关闭日志文件
if err := logFile.Close(); err != nil {
log.Fatalf("error closing file: %v", err)
}
}

注意我们这里使用了init函数,他会在程序启动的时候执行,这里用来打开日志文件,并在程序的运行时间内一直可写。

3. WaitGroup

在上边累加计数的示例中我们都使用了WaitGroup,这里再详细介绍下它的能力。

WaitGroup是Go语言中用来等待一组goroutine执行完成的同步机制。在一些场景下,你可能需要启动多个goroutine去执行任务,而主goroutine需要等待这些任务都完成后才能继续执行。WaitGroup可以用来等待这一组goroutine的结束。

WaitGroup有三个主要的方法:AddDoneWaitAdd用来增加等待的goroutine数量,Done用来表示一个goroutine完成了它的工作,Wait用来阻塞,直到所有的goroutine都调用了Done

WaitGroup是一种简单有效的等待多个goroutine的方法。但是它不能被重用,一旦你用Wait等待它,WaitGroup就不能再添加新的goroutine了。

channel的同步机制

channel是Go语言中的通道,它可以用来在goroutine之间传递消息,确保数据的顺序和完整性。它也可以用来控制并发,比如限制并发的数量。

图片

有两种类型的通道:

  • 无缓冲:无缓冲通道是指在发送操作完成之前必须有相应的接收操作才能开始执行,否则发送操作会一直阻塞。

  • 有缓冲:有缓冲通道有一个固定的存储空间,只有当缓冲区满时,发送操作才会阻塞;只有当缓冲区空时,接收操作才会阻塞。

使用方法:

  • make:通过make函数可以创建一个通道,可以指定它的缓冲大小。

  • ch <- 和 <- ch:使用ch <-可以向通道发送值,使用<- ch可以从通道接收值。

  • close:当通道不再需要发送数据时,可以使用close函数来关闭它。

通道的原理:

  • 共享内存:通道背后是一块共享内存,无缓冲通道上的操作必须是发送和接收同时发生,而有缓冲通道则有一个环形队列存储数据。

  • 锁:操作通道时,Go运行时会使用锁来保证操作的原子性和顺序性。

一个典型的应用是生产者-消费者模型,在这个模型中,生产者goroutine将产品发送到通道,消费者goroutine从通道接收产品并处理。下面我们来看这个例子:

package main

import (
"fmt"
"time"
)

func main() {
message := make(chan string, 2)

go func() {
for {
msg := <-message
fmt.Println(msg)
}
}()

message <- "buffered"
message <- "channel"
time.Sleep(time.Second)
message <- "example"
fmt.Println("All messages sent")
close(message)
}

在这个例子中,我们创建了一个有缓冲的通道message,它可以存储最多2个元素。我们启动一个goroutine来接收并打印从通道接收到的消息。因为通道是有缓冲的,所以发送操作不会立即阻塞,除非缓冲区已满。

结语

在Go语言的并发编程中,sync包和channel是两个非常重要的工具。它们通过不同的机制提供了强大的进程内同步功能。使用原子操作和互斥锁可以保护共享资源,使用WaitGroup可以等待一组goroutine的完成,而通道则可以用来在goroutine之间传递消息和控制并发。正确地使用这些工具,可以让你的并发程序更加稳定和高效。

最后用一张图总结本文:

图片

关注萤火架构,加速技术提升!

萤火架构 短期看执行,中期看方法,长期看认知。 125篇原创内容 公众号

 

萤火架构

赞赏二维码喜欢作者

多线程1 go语言2 阅读 89 萤火架构 ​ 喜欢此内容的人还喜欢   降本增笑,领导要求程序员半年实现一个金蝶     我看过的号 萤火架构 不看的原因   Golang中的工厂模式:灵活选择存储方式实现文件存储     我看过的号 程序员的碎碎念 不看的原因   基于FX构建大型Golang应用     我看过的号 DeepNoMind 不看的原因   写留言              

人划线

标签:互斥,goroutine,sync,并发,Go,操作,多线程,通道
From: https://www.cnblogs.com/cheyunhua/p/17966402

相关文章

  • 深入了解 Python MongoDB 操作:排序、删除、更新、结果限制全面解析
    PythonMongoDB排序对结果进行排序使用sort()方法对结果进行升序或降序排序。sort()方法接受一个参数用于“字段名”,一个参数用于“方向”(升序是默认方向)。示例按名称按字母顺序对结果进行排序:importpymongomyclient=pymongo.MongoClient("mongodb://localhost:270......
  • 高并发场景下如何实现系统限流?
    限流要结合容量和压测来进行,当外部请求接近或者达到系统最大阈值时,触发限流,采取其他手段进行降级,保证系统不被压垮,常见降级策略包括延迟处理,拒绝服务,随机拒绝等。 计数器法:将时间划分固定窗口大小,如1s设定100请求,该窗口时间之后的请求进行丢弃处理滑动窗口计数:将时间拆分......
  • Go Websocket库推荐
    gws常用的操作json格式参考homeassiatant文档中的那个定义:hawebsocket文档定义handler,它是gws的websocket的回调方法集合定义的接口//ClientEventHandler是Websocket事件回调的模板.//有open,close,ping,pong(客户端其实没有ping操作,所以就自然不存在pong......
  • Django rest_framework用户认证和权限
    完整的代码https://gitee.com/mom925/django-system使用jwt实现用户认证pipinstalldjangorestframework-simplejwt重新定义一下User类classUsers(AbstractUser):classMeta:db_table="system_users"verbose_name="用户表"......
  • 如何设计一个高并发系统?
    所谓高并发系统,是指能同时处理大量并发请求,并及时响应,从而保证系统的高性能和高可用那么我们在设计一个高并发系统时,应该考虑哪些方面呢?1.搭建集群如果你只部署一个应用,只部署一台服务器,那抗住的流量请求是非常有限的。并且,单体的应用,有单点的风险,如果它挂了,那服务就不可用了......
  • Google earth engine(GEE)示例:地形分析
    //导入研究区域varstudyArea:Tableprojects/assets/study_area//导入SRTM地形数据varsrtm=ee.Image('USGS/SRTMGL1_003');//提取研究区域的高程varelevation=srtm.clip(studyArea);//计算坡度varslope=ee.Terrain.slope(elevation);//计算坡向va......
  • Go中的闭包和defer关键字
    闭包基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体。packagemainimport"fmt"funcmain(){ //使用AddUpper函数//当反复调用f函数,因为n是初始化一次,因此每调用一次就进行累计。//我们要搞清楚闭包的关键,就是要分析出返回的函数引用到了哪些变......
  • 关于ArcEngine在多线程模式下的注意点
    仅以我的环境来描述的我问题和解决方案,超出该范围的暂时没有考虑。一、环境ArcEngine10.2语言:C#.net版本:4.6.1二、需求创建GDB数据库,并从json文件把数据写入GDB中,包含了图形数据,为了兼顾效率,我使用了多线程来生成GDB,但也做了控制,一个线程只会对一个GDB进行操作。三、问题:......
  • Dithered golden interleaver 黄金分割伪随机交织器 代码备份
    目录公式来源DitheredgoldeninterleaverTheMatrix-DitheredGoldenInterleavingAlgorithm有错误欢迎指正公式来源DesignofaModifiedInterleavingAlgorithmBasedonGoldenSectionTheoryEnhancingthePerformanceofTurboCodesDitheredgoldeninterleaver(*......
  • Go中init函数和匿名函数
    init函数:每一个源文件中都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是init会在Main函数之前被调用。通常可以在init函数中完成初始化工作。import"fmt"funcmain(){ fmt.Println("main函数")//后输出}funcinit(){ fmt.Println("init方法")......