Go语言编程笔记9:使用共享变量实现并发

本文介绍了Go语言中如何处理数据竞态,包括不写入数据、通过通道共享数据和使用互斥锁。通过示例展示了如何在并发环境中安全地访问共享变量,强调了使用互斥锁和通道的重要性,以及并发编程中的潜在问题和解决方案。
摘要由CSDN通过智能技术生成

Go语言编程笔记9:使用共享变量实现并发

image-20211108153040805

图源:wallpapercave.com

数据竞态

在多线程编程中,遇到的最大麻烦就是当多个线程对同一个数据进行操作时,因为代码交错执行引发的一些问题:

package main

import (
	"fmt"
	"sync"
)

type bank struct {
   
	amount int
}

func (b *bank) SaveMoney(amount int) {
   
	b.amount += amount
}

func (b *bank) GetAmount() int {
   
	return b.amount
}

func main() {
   
	for i := 0; i < 10; i++ {
   
		bankTest()
	}
	// bank amount:  300
	// bank amount:  300
	// bank amount:  300
	// bank amount:  100
	// bank amount:  300
	// bank amount:  300
	// bank amount:  300
	// bank amount:  300
	// bank amount:  300
	// bank amount:  300
}

func bankTest() {
   
	var b bank
	var gwg sync.WaitGroup
	gwg.Add(1)
	go func() {
   
		defer gwg.Done()
		b.SaveMoney(100)
		fmt.Println("bank amount: ", b.GetAmount())
	}()
	gwg.Add(1)
	go func() {
   
		defer gwg.Done()
		b.SaveMoney(200)
	}()
	gwg.Wait()
}

这是根据《Go程序设计语言》中的示例代码改写的一个例子,这里构建了一个结构体bank,代表一个共享的银行账户,可以存钱和查看余额。bankTest是测试并发访问下可能出现问题的函数,主goroutine启动两个goroutine来分别存钱,并且其中一个goroutine会在存完钱后查看余额。

实际运行多次我们发现结果并不一致,这是因为两个goroutine是同时运行的,并非是顺序执行,所以是可能A线程存完100块后,B线程存了200,然后A线程再查看余额,此时就是300块。除了这种情况以外,也有可能是A全部执行完后,B再存钱,此时输出结果就是100块。当然也可能是B存完钱后A再执行,此时结果也是300块。

但可能的情况并不仅仅是这三种,因为计算机实际执行程序时是以底层的汇编指令为最小执行单元来执行的,并非是高级语言的单行代码,这在多线程编程中尤其致命。

比如b.amount += amount这条代码,实际执行中可能会拆分成两条汇编语句:

c = b.amount + amount
b.amount = c

也就是说+运算和赋值运算可能并非是在一条汇编语句内完成的,这就会导致更诡异的结果,比如A线程执行到这里,刚完成+运算,还没有来得及进行赋值操作,此时B线程完成了赋值操作,也就是说此时的b.amount已经是200了。然后A再执行赋值的汇编语句,结果b.amount又变成了100。也就是说B线程存的200块“不翼而飞”。

《Go程序设计语言》对此的一句评论相当有趣——“不要相信你在多线程编程时的直觉,因为那往往是错的”。

在学习Python时我也看到过类似的话,关于多线程编程最好的告诫就是——“不要多线程编程”。当然这并不是说不要用编写并发程序,而是说不要写传统的多线程编程,因为那样你会遇到很多麻烦,且很难排查和解决。事实上很多编程语言在语言层面尝试解决该问题,比如Python的全局线程锁,这可以看作是试图将多线程这头老虎关在笼子里的做法,在此基础上使用并发或异步都可以很好地解决并发问题。当然这也并非没有代价,但综合来看是相当值得的。与Python相比,Go语言的goroutine更像是传统的多线程编程,不过采用了其它方式来避免传统多线程编程的问题,在后面我们会详细说明。

上面示例中展现的这种,因为并发线程交错执行代码,并针对同一个变量进行访问产生的问题,称作是“数据竞态”,在这种情况下,并发访问普通变量是“并发不安全的”,而如果一个变量可以安全地并发访问(比如通道或者net.Connect),则会被称为并发安全的。此外,如果一个类型的所有方法都可以安全地并发访问,则这种类型可以被视作是并发安全的。

解决数据竞态

针对数据竟态,有以下几种方式可以解决:

不写入数据

之所以会出现数据竟态,是因为多个线程并发访问共享数据时,有至少一个尝试写入,如果所有的并发线程都只尝试读取,而不写入,自然也就不存在数据竟态。

在编程中,我们经常会遇到一种“延迟初始化”的问题:

package main

type student struct {
   
	name string
	age  int
}

type students struct {
   
	stds map[string]*student
}

func (s *students) getStudent(name string) *student {
   
	student, ok := s.stds[name]
	if !ok {
   
		student := initStudent(name)
		s.stds[name] = student
	}
	return student
}

func initStudent(name string) *student {
   
	
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值