在Golang中使用串行限制纪律来实现线程安全的实例

172 阅读3分钟

我们通过将易变数据限制在一个单一的线程中,来避免易变数据的日期竞赛。通过这样做,我们防止其他线程直接读取或写入数据。实现这一目标的方法之一是,使用 "限制 "策略。

常见的做法是,在流水线中的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