Share Memory By Comunication
传统的线程模型(通常在编写 Java、C++ 和Python 程序时使用)程序员在线程之间通信需要使用共享内存。通常,共享数据结构由锁保护,线程都将争用这些锁来访问数据。在某些情况下,通过使用线程安全的数据结构(如 Python 的Queue),这会变得更容易。
Go 的并发原语 goroutines 和 channels 为构造并发软件提供了一种优雅而独特的方法。Go 没有显式地使用锁来协调对共享数据的访问,而是鼓励使用 chan 在 goroutine 之间传递对数据的引用。这种方法确保在给定的时间只有一个 goroutine 可以访问数据。
Do not communicate by sharing memory;instead, share memory by communicating.
Demo:
// 互斥锁方式:
type Resource struct {
url string
polling bool // is polling?
lastPolled int64 // lastpolled time
}
type Resources struct {
data []*Resource
lock *sync.Mutex
}
// Poller can be visited by goroutines
func Poller(res *Resources) {
for {
// get the last recently-polled Resource and mark it as being polled
res.lock.Lock()
var r *Resource
for _, v := range res.data {
if v.polling {
continue // we continue code when polling
}
if r == nil || v.lastPolled < r.lastPolled {
r = v
}
}
// change status
if r != nil {
r.polling = true
}
// do sth...
res.lock.Unlock()
if r == nil {
continue
}
// poll the URL
// update the Resource's polling and lastpolled
res.lock.Lock()
r.polling = false
u, _ := time.ParseDuration("1ns")
r.lastPolled = u.Nanoseconds()
res.lock.Unlock()
}
}
// channel 方式
type Resource string
// in为输入,out为输出的 chan
func Poller(in, out chan *Resource) {
// 循环取获取这个输入
for r := range in {
// poll the URL
// send the processed Resource to out
out <- r // 将输入处理后的放入另一个chan
}
}
Detecting Race Conditions With Go
**data race 是两个或多个 goroutine 访问同一个资源(如变量或数据结构),并尝试对该资源进行读写而不考虑其他 goroutine。**这种类型的代码可以创建您见过的最疯狂和最随机的 bug。通常需要大量的日志记录和运气才能找到这些类型的bug。
早在Go 1.1中,Go 工具引入了一个 race detector。竞争检测器是在构建过程中内置到程序中的代码。然后,一旦你的程序运行,它就能够检测并报告它发现的任何竞争条件。它非常酷,并且在识别罪魁祸首的代码方面做了令人难以置信的工作。
go build -race
go test -race
package main
import (
"fmt"
"sync"
)
var Wait sync.WaitGroup
var Counter int = 0
func main() {
for routine := 1; routine <= 2; routine++ {
Wait.Add(1)
go Routine(routine)
}
Wait.Wait()
fmt.Printf("Final Counter: %d\n", Counter)
}
func Routine(id int) {
for cout := 0; cout < 2; cout++ {
// time.Sleep(1 * time.Nanosecond)
value := Counter
value++
Counter = value
}
Wait.Done()
}
>go build -race main.go
>main.exe
==================
WARNING: DATA RACE
Read at 0x000000f64608 by goroutine 8: // 进行了 read 行为
main.Routine()
D:/code/go/test/main.go:23 +0x4e
Previous write at 0x000000f64608 by goroutine 7: // 进行了 write 行为
main.Routine()
D:/code/go/test/main.go:26 +0x6a
Goroutine 8 (running) created at:
main.main()
D:/code/go/test/main.go:14 +0x7c
Goroutine 7 (finished) created at:
main.main()
D:/code/go/test/main.go:14 +0x7c
==================
Final Counter: 4 // 理论上结果应该为4
Found 1 data race(s) // 发现开了多个goroutine,一个goroutine同时产生了读写行为,发现data // race,会将整个进程退掉
工具似乎检测到代码的争用条件。如果您查看race condition 报告下面,您可以看到程序的输出: 全局计数器变量的值为 2 或者 4。
等于4:我们启动了两个 goroutine,一个 goroutine 在赋值结束后 Counter 为2,这时另一个 goroutine 启动,读取到的 Counter 为2,所以最终结果为4。
等于2:当启动一个 goroutine 的时候,还未对 Counter 进行赋值,另一个 goroutine 也启动了,这时两个 Counter 读取的都是0,所以最终结果就是2。
我们将上面代码的 Routine 中 sleep 解注释,看看有什么区别:
==================
WARNING: DATA RACE
Write at 0x000000e65608 by goroutine 8:
main.Routine()
D:/code/go/test/main.go:27 +0x77
Previous read at 0x000000e65608 by goroutine 7:
main.Routine()
D:/code/go/test/main.go:24 +0x4e
Goroutine 8 (running) created at:
main.main()
D:/code/go/test/main.go:15 +0x7c
Goroutine 7 (running) created at:
main.main()
D:/code/go/test/main.go:15 +0x7c
==================
==================
WARNING: DATA RACE
Write at 0x000000e65608 by goroutine 7:
main.Routine()
D:/code/go/test/main.go:27 +0x77
Previous read at 0x000000e65608 by goroutine 8:
main.Routine()
D:/code/go/test/main.go:24 +0x4e
Goroutine 7 (running) created at:
main.main()
D:/code/go/test/main.go:15 +0x7c
Goroutine 8 (running) created at:
main.main()
D:/code/go/test/main.go:15 +0x7c
==================
Final Counter: 2 // 结果变为了2
Found 2 data race(s)
试图通过 i++ 方式来解决原子赋值的问题,但是我们通过查看底层汇编:
0064 (./main.go