Go并发:访问共享数据
来源:互联网 发布:水果软件12 编辑:程序博客网 时间:2024/06/06 03:19
竟险
竟险(竞争条件、Race Condition)是指多个协程(goroutine)同时访问共享数据,其结果取决于指令执行顺序的情况。
考虑如下售票程序。该程序模拟两个售票窗口,一个执行购票,一个执行退票。
package mainimport ( "fmt" "time")var tickCount = 200 // 总票数// 购票func buy() { tickCount = tickCount - 1 // A}// 退票func refund() { tickCount = tickCount + 1 // B}func main() { go buy() // 购票协程 go refund() // 退票协程 time.Sleep(time.Second * 1) // 为了简单,这里睡眠1秒,等待上面两个协程结束 fmt.Println("tick count:", tickCount) // 输出结果是什么?}
考虑到一共200张票,买了一张,卖了一张,应该还是剩余200张票。事实却不总是这样,多次运行程序发现有如下输出:
tick count: 201
和如下输出:
tick count: 199
多的一张和少的一张到哪里去了?无论是先执行买票(语句A)还是卖票(语句B),结果都应该是200才对啊!
NO!!!
在计算机看来语句A和语句B并不是一条不可分割的语句,而是两条语句:
A1: … = tickCount - 1
A2: tickCount = …
B1: … = tickCount + 1
B2: tickCount = …
它们的实际执行顺序有如下四种可能:
- A1->A2->B1->B2 结果为200
- B1->B2->A1->A2 结果为200
- B1->A1->A2->B2 结果为201
- A1->B1->B2->A2 结果为199
可见第三种和第四种执行顺序产生了意想不到的结果。原因在于两个协程同时访问并修改了共享变量(tickCount),而语句之间的顺序无法保证,导致意外的情况发生,这便是竟险。
竟险显然不是我们想要的结果。那么如何规避竟险呢?有三种方式:1. 禁止修改共享变量。2. 限制在同一个协程中访问共享变量。3. 利用互斥。下面分别来看看这三种方式。
禁止修改共享变量
可以通过禁止修改共享变量来达到规避竟险的目的。
考虑如下程序:
package mainvar config = map[string]string{}func loadConfig(key string) string { /*...*/ }// 惰性加载func getConfig(key string) string { value, ok := config[key] if !ok { value = loadConfig(key) config[key] = value } return value}func main() { go func() { user := getConfig("userName") // A 修改共享变量的值,发生竟险 // ... }() go func() { address := getConfig("address") // B 修改共享变量的值,发生竟险 // ... }() // ...}
注意该例中getConfig()为惰性加载,也就是在需要加载时再加载,这样便在语句A和语句B中发生了竟险,两条语句同时修改了共享变量config。如果修改为提前加载所有配置,则可规避竟险:
package main// 提前加载所有配置var config = map[string]string{ "userName": loadConfig("userName"), "address": loadConfig("address"),}func loadConfig(key string) string { /*...*/ }func getConfig(key string) string { return config[key]}func main() { go func() { user := getConfig("userName") // 访问共享变量,但不修改其值,不发生竟险 // ... }() go func() { address := getConfig("address") // 访问共享变量,但不修改其值,不发生竟险 // ... }() // ...}
这种方式仅仅可以用于协程不需要修改共享变量的情况。这显然满足不了我们的所有需求。在很多情况下协程必须修改共享变量。
限制在同一个协程中访问共享变量
将共享变量的访问限制在一个协程中,就避免了竟险。 不过这种方式略微有些复杂,必须要建立一个监听线程,来专门处理共享变量的修改。
package mainimport ( "fmt" "sync")var tickCount = 200 // 总票数var ch = make(chan int, 10) // 用来控制tickCount的同步,10表示模拟10个售/退票窗口var n sync.WaitGroup // 用来等待购票和售票动作完成var done = make(chan struct{}) // 用来等待监听协程退出// 购票func buy() { ch <- -1}// 退票func refund() { ch <- 1}func main() { // 监听协程 go func() { for amount := range ch { tickCount += amount n.Done() // 每次调用Done(),n的计数减1 } done <- struct{}{} // 监听线程结束,发送消息 }() n.Add(2) // 因为要执行两个动作,所以使n的计数加2 go buy() // 购票协程 go refund() // 退票协程 n.Wait() // 等待购票和退票动作完成 // Wait()会一直等待,直到n的计数为0 close(ch) // 关闭管道 <-done // 等待监听线程结束 fmt.Println("tick count:", tickCount)}
这种方式类似于在Windows的线程中处理消息循环,用PostThreadMessage来发送消息。可以对比一下:
#include <Windows.h>#include <iostream>#define WM_AMOUNT (WM_USER + 1)#define WM_DONE (WM_USER + 2)int tickCount = 200; // 总票数200DWORD dwThreadId = 0; // 监听线程IDHANDLE hEventState = NULL; // 用于同步监听线程的开始和结束// 监听线程DWORD WINAPI Monitor(LPVOID lpParameter) { // 确保建立消息循环 MSG msg = {}; PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); // 通知主线程消息循环建立完成 SetEvent(hEventState); // 处理消息 BOOL bRet; bool done = false; while (!done && (bRet = GetMessage(&msg, NULL, 0, 0)) != 0) { if (bRet == -1) { break; } else { switch (msg.message) { case WM_AMOUNT: tickCount += (int)msg.wParam; break; case WM_DONE: done = true; break; default: break; } } } SetEvent(hEventState); return 0;}// 购票void buy() { PostThreadMessage(dwThreadId, WM_AMOUNT, (WPARAM)-1, NULL);}// 退票void refund() { PostThreadMessage(dwThreadId, WM_AMOUNT, (WPARAM)1, NULL);}int main() { hEventState = CreateEvent(NULL, FALSE, FALSE, NULL); // 创建同步监听线程的事件 // 开启监听线程 HANDLE hThread = CreateThread(NULL, 0, Monitor, NULL, 0, &dwThreadId); CloseHandle(hThread); WaitForSingleObject(hEventState, INFINITE); // 确保监听线程已开启 buy(); // 购票 refund(); // 退票 PostThreadMessage(dwThreadId, WM_DONE, NULL, NULL); // 退出监听线程 WaitForSingleObject(hEventState, INFINITE); // 等待监听线程退出 CloseHandle(hEventState); // 关闭同步监听线程的事件 std::cout << "tick count: " << tickCount << std::endl;}
可见C++的版本复杂了一些,不过也差不了多少。
利用互斥
第三种方式是使用sync包中提供的互斥锁sync.Mutex。sync.Mutex是一个结构体,提供了Lock和Unlock两个方法,Lock用来锁定,Unlock用来解锁。 利用互斥锁,上面的程序变得更简单了:
package mainimport ( "fmt" "sync")var ( tickCount = 200 // 总票数 mu sync.Mutex // 互斥锁 n sync.WaitGroup)// 购票func buy() { defer n.Done() // 计数减1 mu.Lock() defer mu.Unlock() // 用defer保证函数返回时解锁 tickCount += 1}// 退票func refund() { defer n.Done() // 计数减1 mu.Lock() defer mu.Unlock() // 用defer保证函数返回时解锁 tickCount -= 1}func main() { n.Add(2) // 有两个动作,所以计数加2 go buy() // 购票协程 go refund() // 退票协程 n.Wait() // 等待购票和退票动作完成 // Wait一直阻塞,直到n的计数为0返回 fmt.Println("tick count:", tickCount)}
总结
- 多个协程同时访问共享数据时会造成竟险。
- 可以有三种方式规避竟险:禁止修改共享变量、限制在同一个协程中访问共享变量、 利用互斥对象。
- Go并发:访问共享数据
- 并发-同步访问共享的可变数据
- 【JAVA 并发】一 同步访问共享的可变数据
- go通过共享变量实现并发
- go语言坑之并发访问map
- 线程并发 共享数据及线程并发
- Go 语言的并发模型--通过通信来共享内存
- java多线程共享数据和数据并发
- Go语言学习笔记(4)-共享变量访问
- Go 并发
- go并发
- go-并发
- Go并发
- 在并发例程中保护共享数据
- 【java并发】线程范围内共享数据
- 共享一个通用的数据访问类
- 共享一个通用的数据访问类
- 对共享可变数据的同步访问
- 微信赞赏不适合国内免费模式主导的互联网市场
- 移动端mui框架写的手机wap模板
- 建造者模式(Builder Pattern)-创建型模式
- 函数式编程扫盲篇
- 函数式编程初探
- Go并发:访问共享数据
- 【EMGUCV】图像的直方图均衡化增强
- [JavaWeb]MyEclipse去掉Js等语法的验证
- Qt读取本地图片使用halcon读取并显示
- shiro 进行权限管理 —— 使用BigInteger进行权限计算获取菜单
- browser-sync搭建实时刷新页面效果
- Java思维导图(7)--Java IO
- 扫描识别行驶证的软件技术
- 重新生成索引标号与取消原来的索引标号