- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章Go并发编程实现数据竞争由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
虽然在 go 中,并发编程十分简单, 只需要使用 go func() 就能启动一个 goroutine 去做一些事情,但是正是由于这种简单我们要十分当心,不然很容易出现一些莫名其妙的 bug 或者是你的服务由于不知名的原因就重启了。 而最常见的bug是关于线程安全方面的问题,比如对同一个map进行写操作.
线程安全是否有什么办法检测到呢?
答案就是 data race tag,go 官方早在 1.1 版本就引入了数据竞争的检测工具,我们只需要在执行测试或者是编译的时候加上 -race 的 flag 就可以开启数据竞争的检测 。
使用方式如下 。
1
2
|
go test -race main.go
go build -race
|
不建议在生产环境 build 的时候开启数据竞争检测,因为这会带来一定的性能损失(一般内存5-10倍,执行时间2-20倍),当然 必须要 debug 的时候除外。 建议在执行单元测试时始终开启数据竞争的检测 。
执行如下代码,查看每次执行的结果是否一样 。
2.1.1 测试 。
代码 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var counter int
func main() {
// 多跑几次来看结果
for i := 0; i < 100000; i++ {
run()
}
fmt.Printf("Final Counter: %d\n", counter)
}
func run() {
// 开启两个 协程,操作
for i := 1; i <= 2; i++ {
wg.Add(1)
go routine(i)
}
wg.Wait()
}
func routine(id int) {
for i := 0; i < 2; i++ {
value := counter
value++
counter = value
}
wg.Done()
}
|
执行三次查看结果,分别是 。
Final Counter: 399950 Final Counter: 399989 Final Counter: 400000 。
原因分析:每一次执行的时候,都使用 go routine(i) 启动了两个 goroutine,但是并没有控制它的执行顺序,并不能满足顺序一致性内存模型.
当然由于种种不确定性,所有肯定不止这两种情况, 。
2.1.2 data race 检测 。
上面问题的出现在上线后如果出现bug会非常难定位,因为不知道到底是哪里出现了问题,所以我们就要在测试阶段就结合 data race 工具提前发现问题.
使用 。
1
|
go run -race ./main.go
|
输出: 运行结果发现输出记录太长,调试的时候并不直观,结果如下 。
main.main() D:/gopath/src/Go_base/daily_test/data_race/demo.go:14 +0x44 ================== Final Counter: 399987 Found 1 data race(s) exit status 66 。
2.1.3 data race 配置 。
在官方的文档当中,可以通过设置 GORACE 环境变量,来控制 data race 的行为, 格式如下
1
|
GORACE="option1=val1 option2=val2"
|
可选配置见下表 。
配置 。
1
|
GORACE="halt_on_error=1 strip_path_prefix=/mnt/d/gopath/src/Go_base/daily_test/data_race/01_data_race" go run -race ./demo.go
|
输出:
================== WARNING: DATA RACE Read at 0x00000064d9c0 by goroutine 8: main.routine() /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:31 +0x47 Previous write at 0x00000064d9c0 by goroutine 7: main.routine() /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:33 +0x64 Goroutine 8 (running) created at: main.run() /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:24 +0x75 main.main() /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:14 +0x3c Goroutine 7 (finished) created at: main.run() /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:24 +0x75 main.main() /mnt/d/gopath/src/Go_base/daily_test/data_race/demo.go:14 +0x3c ================== exit status 66 。
说明:结果告诉可以看出 31 行这个地方有一个 goroutine 在读取数据,但是呢,在 33 行这个地方又有一个 goroutine 在写入,所以产生了数据竞争。 然后下面分别说明这两个 goroutine 是什么时候创建的,已经当前是否在运行当中.
代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
|
输出:常见的答案就是会输出 5 个 5,因为在 for 循环的 i++ 会执行的快一些,所以在最后打印的结果都是 5 这个答案不能说不对,因为真的执行的话大概率也是这个结果,但是不全。因为这里本质上是有数据竞争,在新启动的 goroutine 当中读取 i 的值,在 main 中写入,导致出现了 data race,这个结果应该是不可预知的,因为我们不能假定 goroutine 中 print 就一定比外面的 i++ 慢,习惯性的做这种假设在并发编程中是很有可能会出问题的 。
正确示例:将 i 作为参数传入即可,这样每个 goroutine 拿到的都是拷贝后的数据 。
1
2
3
4
5
6
7
8
9
10
11
|
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
|
代码 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package main
import "os"
func main() {
ParallelWrite([]byte("xxx"))
}
// ParallelWrite writes data to file1 and file2, returns the errors.
func ParallelWrite(data []byte) chan error {
res := make(chan error, 2)
// 创建/写入第一个文件
f1, err := os.Create("/tmp/file1")
if err != nil {
res <- err
} else {
go func() {
// 下面的这个函数在执行时,是使用err进行判断,但是err的变量是个共享的变量
_, err = f1.Write(data)
res <- err
f1.Close()
}()
}
// 创建写入第二个文件n
f2, err := os.Create("/tmp/file2")
if err != nil {
res <- err
} else {
go func() {
_, err = f2.Write(data)
res <- err
f2.Close()
}()
}
return res
}
|
分析: 使用 go run -race main.go 执行,可以发现这里报错的地方是,21 行和 28 行,有 data race,这里主要是因为共享了 err 这个变量 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
root@failymao:/mnt/d/gopath/src/Go_base/daily_test/data_race# go run -race demo2.go
==================
WARNING: DATA RACE
Write at 0x00c0001121a0 by main goroutine:
main.ParallelWrite()
/mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:28 +0x1dd
main.main()
/mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:6 +0x84
Previous write at 0x00c0001121a0 by goroutine 7:
main.ParallelWrite.func1()
/mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:21 +0x94
Goroutine 7 (finished) created at:
main.ParallelWrite()
/mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:19 +0x336
main.main()
/mnt/d/gopath/src/Go_base/daily_test/data_race/demo2.go:6 +0x84
==================
Found 1 data race(s)
exit status 66
|
修正: 在两个goroutine中使用新的临时变量 。
1
2
3
4
|
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...
|
所谓全局变量是指,定义在多个函数的作用域之外,可以被多个函数或方法进行调用,常用的如 map数据类型 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 定义一个全局变量 map数据类型
var service = map[string]string{}
// RegisterService RegisterService
// 用于写入或更新key-value
func RegisterService(name, addr string) {
service[name] = addr
}
// LookupService LookupService
// 用于查询某个key-value
func LookupService(name string) string {
return service[name]
}
|
要写出可测性比较高的代码就要少用或者是尽量避免用全局变量,使用 map 作为全局变量比较常见的一种情况就是配置信息。关于全局变量的话一般的做法就是加锁,或者也可以使用 sync.Ma 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
var (
service map[string]string
serviceMu sync.Mutex
)
func RegisterService(name, addr string) {
serviceMu.Lock()
defer serviceMu.Unlock()
service[name] = addr
}
func LookupService(name string) string {
serviceMu.Lock()
defer serviceMu.Unlock()
return service[name]
}
|
一般讲成员变量 指的是数据类型为结构体的某个字段。 如下一段代码 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
type Watchdog struct{
last int64
}
func (w *Watchdog) KeepAlive() {
// 第一次进行赋值操作
w.last = time.Now().UnixNano()
}
func (w *Watchdog) Start() {
go func() {
for {
time.Sleep(time.Second)
// 这里在进行判断的时候,很可能w.last更新正在进行
if w.last < time.Now().Add(-10*time.Second).UnixNano() {
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
}
}
}()
}
|
使用原子操作atomiic 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
type Watchdog struct{
last int64
}
func (w *Watchdog) KeepAlive() {
// 修改或更新
atomic.StoreInt64(&w.last, time.Now().UnixNano())
}
func (w *Watchdog) Start() {
go func() {
for {
time.Sleep(time.Second)
// 读取
if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
}
}
}()
}
|
一个很有趣的例子 Ice cream makers and data races 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package main
import "fmt"
type IceCreamMaker interface {
// Great a customer.
Hello()
}
type Ben struct {
name string
}
func (b *Ben) Hello() {
fmt.Printf("Ben says, \"Hello my name is %s\"\n", b.name)
}
type Jerry struct {
name string
}
func (j *Jerry) Hello() {
fmt.Printf("Jerry says, \"Hello my name is %s\"\n", j.name)
}
func main() {
var ben = &Ben{name: "Ben"}
var jerry = &Jerry{"Jerry"}
var maker IceCreamMaker = ben
var loop0, loop1 func()
loop0 = func() {
maker = ben
go loop1()
}
loop1 = func() {
maker = jerry
go loop0()
}
go loop0()
for {
maker.Hello()
}
}
|
这个例子有趣的点在于,最后输出的结果会有这种例子 。
Ben says, "Hello my name is Jerry" Ben says, "Hello my name is Jerry" 。
这是因为我们在maker = jerry这种赋值操作的时候并不是原子的,在上一篇文章中我们讲到过,只有对 single machine word 进行赋值的时候才是原子的,虽然这个看上去只有一行,但是 interface 在 go 中其实是一个结构体,它包含了 type 和 data 两个部分,所以它的复制也不是原子的,会出现问题 。
1
2
3
4
|
type interface struct {
Type uintptr // points to the type of the interface implementation
Data uintptr // holds the data for the interface's receiver
}
|
这个案例有趣的点还在于,这个案例的两个结构体的内存布局一模一样所以出现错误也不会 panic 退出,如果在里面再加入一个 string 的字段,去读取就会导致 panic,但是这也恰恰说明这个案例很可怕,这种错误在线上实在太难发现了,而且很有可能会很致命.
使用 go build -race main.go和go test -race ./ 可以测试程序代码中是否存在数据竞争问题 。
https://lailin.xyz/post/go-training-week3-data-race.html#典型案例 https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data-races http://blog.golang.org/race-detector https://golang.org/doc/articles/race_detector.html https://dave.cheney.net/2018/01/06/if-aligned-memory-writes-are-atomic-why-do-we-need-the-sync-atomic-package 。
到此这篇关于Go并发编程实现数据竞争的文章就介绍到这了,更多相关Go 数据竞争内容请搜索我以前的文章或继续浏览下面的相关文章希望大家以后多多支持我! 。
原文链接:https://www.cnblogs.com/failymao/p/15336368.html 。
最后此篇关于Go并发编程实现数据竞争的文章就讲到这里了,如果你想了解更多关于Go并发编程实现数据竞争的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
背景: 我最近一直在使用 JPA,我为相当大的关系数据库项目生成持久层的轻松程度给我留下了深刻的印象。 我们公司使用大量非 SQL 数据库,特别是面向列的数据库。我对可能对这些数据库使用 JPA 有一
我已经在我的 maven pom 中添加了这些构建配置,因为我希望将 Apache Solr 依赖项与 Jar 捆绑在一起。否则我得到了 SolarServerException: ClassNotF
interface ITurtle { void Fight(); void EatPizza(); } interface ILeonardo : ITurtle {
我希望可用于 Java 的对象/关系映射 (ORM) 工具之一能够满足这些要求: 使用 JPA 或 native SQL 查询获取大量行并将其作为实体对象返回。 允许在行(实体)中进行迭代,并在对当前
好像没有,因为我有实现From for 的代码, 我可以转换 A到 B与 .into() , 但同样的事情不适用于 Vec .into()一个Vec . 要么我搞砸了阻止实现派生的事情,要么这不应该发
在 C# 中,如果 A 实现 IX 并且 B 继承自 A ,是否必然遵循 B 实现 IX?如果是,是因为 LSP 吗?之间有什么区别吗: 1. Interface IX; Class A : IX;
就目前而言,这个问题不适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the
我正在阅读标准haskell库的(^)的实现代码: (^) :: (Num a, Integral b) => a -> b -> a x0 ^ y0 | y0 a -> b ->a expo x0
我将把国际象棋游戏表示为 C++ 结构。我认为,最好的选择是树结构(因为在每个深度我们都有几个可能的移动)。 这是一个好的方法吗? struct TreeElement{ SomeMoveType
我正在为用户名数据库实现字符串匹配算法。我的方法采用现有的用户名数据库和用户想要的新用户名,然后检查用户名是否已被占用。如果采用该方法,则该方法应该返回带有数据库中未采用的数字的用户名。 例子: “贾
我正在尝试实现 Breadth-first search algorithm , 为了找到两个顶点之间的最短距离。我开发了一个 Queue 对象来保存和检索对象,并且我有一个二维数组来保存两个给定顶点
我目前正在 ika 中开发我的 Python 游戏,它使用 python 2.5 我决定为 AI 使用 A* 寻路。然而,我发现它对我的需要来说太慢了(3-4 个敌人可能会落后于游戏,但我想供应 4-
我正在寻找 Kademlia 的开源实现C/C++ 中的分布式哈希表。它必须是轻量级和跨平台的(win/linux/mac)。 它必须能够将信息发布到 DHT 并检索它。 最佳答案 OpenDHT是
我在一本书中读到这一行:-“当我们要求 C++ 实现运行程序时,它会通过调用此函数来实现。” 而且我想知道“C++ 实现”是什么意思或具体是什么。帮忙!? 最佳答案 “C++ 实现”是指编译器加上链接
我正在尝试使用分支定界的 C++ 实现这个背包问题。此网站上有一个 Java 版本:Implementing branch and bound for knapsack 我试图让我的 C++ 版本打印
在很多情况下,我需要在 C# 中访问合适的哈希算法,从重写 GetHashCode 到对数据执行快速比较/查找。 我发现 FNV 哈希是一种非常简单/好/快速的哈希算法。但是,我从未见过 C# 实现的
目录 LRU缓存替换策略 核心思想 不适用场景 算法基本实现 算法优化
1. 绪论 在前面文章中提到 空间直角坐标系相互转换 ,测绘坐标转换时,一般涉及到的情况是:两个直角坐标系的小角度转换。这个就是我们经常在测绘数据处理中,WGS-84坐标系、54北京坐标系
在软件开发过程中,有时候我们需要定时地检查数据库中的数据,并在发现新增数据时触发一个动作。为了实现这个需求,我们在 .Net 7 下进行一次简单的演示. PeriodicTimer .
二分查找 二分查找算法,说白了就是在有序的数组里面给予一个存在数组里面的值key,然后将其先和数组中间的比较,如果key大于中间值,进行下一次mid后面的比较,直到找到相等的,就可以得到它的位置。
我是一名优秀的程序员,十分优秀!