我们通过将易变数据限制在一个单一的线程中,来避免易变数据的日期竞赛。通过这样做,我们防止其他线程直接读取或写入数据。实现这一目标的方法之一是,使用 "限制 "策略。
常见的做法是,在流水线中的goroutines之间共享一个变量/字段,通过一个通道将其内存地址从一个阶段传递到下一个阶段。如果流水线的每个阶段在将变量/字段发送到下一个阶段后都不去访问它,那么对该变量/字段的所有访问都是顺序的。因此,变量/字段被限制在流水线的一个阶段,然后被限制在下一个阶段,以此类推。这就是所谓的串行限制。由于其他的goroutine不能直接访问该变量/字段,它们必须使用一个通道来向限制的goroutine发送操作该变量/字段的请求,这就是Golang中 "不要通过共享内存进行通信;相反,通过通信共享内存 "的含义。
-
不要通过共享内存进行通信 - 假设有多个线程想要读/写一个共享变量。例如,他们会使用mutexes来锁定内存,以防止其他人在操作完成之前对其进行读/写。在这里,他们一次锁定一个内存,然后在完成后立即解锁,以便下一个线程可以拿起它。
var i string
sync.Mutex.Lock() // Sharing
i = "done" // Read/Write
sync.Mutex.Unlock() // Communicate to say "I am done"
// Then next deals with it.
// Then next ...
// Then next ...
-
通过通信共享内存-- 让我们遵循上述相同的假设,但这一次我们将不使用mutexes。相反,我们将使用通道。共享变量/值将从一个通道移动到另一个通道,而不是锁定内存。发送方通道将通知接收方通道从该通道接收变量。这就是他们通过通信共享内存的方式。这里的关键点是,执行是以特定的顺序进行的。这就是所谓的happens-before。
var i string
aChan := make(chan i, 1)
bChan := make(chan i, 1)
cChan := make(chan i, 1)
// Operation a starts first
i = "a done"
aChan<- i
for v := range aChan {
i = "b done"
}
for v := range bChan {
i = "c done"
}
// ...
在这个例子中,我们是通过使用限制/再入性原则来实现线程安全。它们通过将数据 "限制 "在一个单一的线程中(upload,resize,save )来避免易变的Image.state 字段的数据竞赛。这样,我们就不会让多个线程同时改变Image.state 。
例子1
import (
"fmt"
"time"
)
type Image struct {
state string
upChan chan struct{}
reChan chan struct{}
}
func New() *Image {
return &Image{
state: "",
upChan: make(chan struct{}, 1),
reChan: make(chan struct{}, 1),
}
}
func (i *Image) Upload() *Image {
i.state = "UPLOADED"
fmt.Println(time.Now().UTC(), "-", i.state)
i.upChan <- struct{}{}
close(i.upChan)
return i
}
func (i *Image) Resize() *Image {
for range i.upChan {
i.state = "RESIZED"
fmt.Println(time.Now().UTC(), "-", i.state)
i.reChan <- struct{}{}
close(i.reChan)
}
return i
}
func (i *Image) Save() {
for range i.reChan {
i.state = "SAVED"
fmt.Println(time.Now().UTC(), "-", i.state)
}
}
测试
没有goroutines
package main
import (
"fmt"
"internal/image"
)
func main() {
fmt.Println("START")
image.New().Upload().Resize().Save()
fmt.Println("FINISH")
}
START
2020-04-16 19:38:30.092691 +0000 UTC - UPLOADED
2020-04-16 19:38:30.092737 +0000 UTC - RESIZED
2020-04-16 19:38:30.092754 +0000 UTC - SAVED
FINISH
有goroutines
package main
import (
"fmt"
"time"
"internal/image"
)
func main() {
fmt.Println("START")
img := image.New()
go img.Save()
go img.Upload()
go img.Resize()
time.Sleep(1 * time.Second)
fmt.Println("FINISH")
}
正如你所看到的,尽管我们调用方法的顺序是错误的,但它们的运行顺序是正确的。
START
2020-04-16 20:55:23.206613 +0000 UTC - UPLOADED
2020-04-16 20:55:23.206695 +0000 UTC - RESIZED
2020-04-16 20:55:23.206723 +0000 UTC - SAVED
FINISH
例二
package image
import (
"fmt"
"time"
)
type Image struct {
state string
}
func New() *Image {
return &Image{}
}
func Upload(upChan chan<- *Image, img *Image) {
img.state = "UPLOADED"
fmt.Println(time.Now().UTC(), "-", img.state)
upChan<- img
}
func Resize(reChan chan<- *Image, upChan <-chan *Image) {
for img := range upChan {
img.state = "RESIZED"
fmt.Println(time.Now().UTC(), "-", img.state)
reChan<- img
}
}
func Save(reChan <-chan *Image) {
for img := range reChan {
img.state = "SAVED"
fmt.Println(time.Now().UTC(), "-", img.state)
}
}
测试
package main
import (
"fmt"
"internal/image"
)
func main() {
fmt.Println("START")
var (
upChan = make(chan *image.Image, 1)
reChan = make(chan *image.Image, 1)
)
img := image.New()
image.Upload(upChan, img)
close(upChan)
image.Resize(reChan, upChan)
close(reChan)
image.Save(reChan)
fmt.Println("FINISH")
}
START
2020-04-16 10:58:31.018522 +0000 UTC - UPLOADED
2020-04-16 10:58:31.018572 +0000 UTC - RESIZED
2020-04-16 10:58:31.018578 +0000 UTC - SAVED
FINISH