DAY 4:Guarded Suspension Pattern,你不会死的,因为我会保护你

什麽是 Guarded Suspension Pattern?

如果 thread 执行时条件不符,就停下等待,等到条件符合时再开始开始执行

问题情境

当设计线上即时显示留言板时,会接受大量的留言,并将其显示出来,我们需确保留言不出现 race condition,也需确保没留言时不执行显示。

相关的 code 在Github - go-design-patterns

实作有问题的系统如下,使用者不断用SendMessage()发送留言cool至相当於 queue 的 m.messages slice,Show()会不断读取 m.messages 显示,并移除讯息:

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type MessageBoard struct {
	messages []string
}

func (m *MessageBoard) SendMessage(message string) {
	m.messages = append(m.messages, message)
}

func (m *MessageBoard) Show() {
	for {
		fmt.Println(m.messages[0])
		m.messages = m.messages[1:]
	}
}

func main() {
	messageBoard := new(MessageBoard)
	go func() {
		for {
			time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) //模拟各个client发送的随机处理时间
			messageBoard.SendMessage("cool")
		}
	}()
	go func() {
		for {
			time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) //模拟各个client发送的随机处理时间
			messageBoard.SendMessage("cool")
		}
	}()
	go func() {
		messageBoard.Show()
	}()
	time.Sleep(10 * time.Second) //等待goroutine执行完毕
}

由於 m.messages 可能不存在讯息,此时读取会发生 error 如下:

解决方式

需要在 m.messages 读取时,新增执行条件,即「m.messages 为空的时候等待而不读取,等到有讯息实再读取显示」,此时可以透过 channel 来实现,将 slice 改为 channel 如下

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type MessageBoard struct {
	messages chan string
}

func (m *MessageBoard) SendMessage(message string) {
	m.messages <- message
}

func (m *MessageBoard) Show() {
	for {
		fmt.Println(<-m.messages)
	}
}

func CreateMessageBoard() *MessageBoard {
	messageBoard := new(MessageBoard)
	messageBoard.messages = make(chan string, 100) //可容纳100条讯息
	return messageBoard
}

func main() {
	messageBoard := CreateMessageBoard()
	go func() {
		for {
			time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) //模拟各个client发送的随机处理时间
			messageBoard.SendMessage("cool")
		}
	}()
	go func() {
		messageBoard.Show()
	}()
	time.Sleep(10 * time.Second) //等待goroutine执行完毕
}

channel 有几个特性:

  • 在读写时不会产生 race condtion
  • 在读取时如果没有元素,会等待到有值再执行
  • 如果使用 unbuffered channel,e.g. make(chan string),写入後如果没有读取,会在写入处等待
  • 如果使用 buffered channel,e.g. make(chan string, 100),写入後如果没有读取,可以继续写入,直到到达 buffer 数才会等待读取

所以在程序码中,当 m.messages 不存在讯息时,Show()会等待到有讯息在进行显示。channel 可容纳 100 条讯息,不会让SendMessage()每条讯息都要等到Show()显示完才能继续发送。

跟经典 Java 模式比对

事实上,Guarded Suspension Pattern 经典的做法是采用值来当作「执行条件」,以此范例来说就是:

for len(m.messages) != 0 {
	fmt.Println(m.messages[0])
	m.messages = m.messages[1:]
}

并且为了避免 for 回圈一直重复检查len(m.messages) != 0让 cpu 飙升,所以需使用sync.Cond{}Wait()来让 goroutine 暂停,并在SendMessage()时用Signal() or Broadcast()启动已暂停的 goroutine,其实相当於 java synchronized 的Wait()Notify() or notifyAll()

但 golang 提倡「share memory by communicating」,即 CSP(Communication Sequential Process)的目标之一,透过 channel 来在不同 goroutine 间分享资料,如果以len(m.messages) != 0来实作执行条件即是「communicate by sharing memory」,我们需透过Lock()Unlock()来保护 m.messages,也需透过Wait()Signal()来避免 goroutine 的执行与否,程序码会复杂较多,而 channel 可以简单乾净的实现以上需求,因此多数 gopher 面对 Guarded Suspension Pattern 会采用 channel 而非sync.Cond{},甚至有着废除sync.Cond{}issue,不过在更复杂的场景与效能考量上,例如唤起不同停住的 goroutine,所以sync.Cond{}还是有存在的必要,所以此方案目前没有被采用。

感谢

同事 Vic 协助校稿


<<:  Day 2. 安装Unity

>>:  【心得】你今天青蛙了吗?flex之路-flex设定了宽却没有用???

鬼故事 - 这不是後门这是工程模式!

鬼故事 - 这不是後门这是工程模式! Credit: sandserif 故事开始 故事回到小弯的公...

第 5 天 还我漂漂拳| property binding、interface

前情提要 将英雄们显示在 Mat-Card 上後,我们进一步地要对英雄资料做点加工,并且制作英雄详细...

Windows Event探索练习--开关机和Office的大小事件

今天要来研究一些常见的事件,来看看有那些东西会被系统纪录下来,他们的意义又是什麽。 笔者查了查发现不...

Day 20 - 物件导向与向量 - Class 粒子系统

今天要来做一下粒子系统 首先要来了解 为什麽要了解 class生成物件呢 大量快速建立同类型的物件 ...

Day 14 - Arrow Function Expression & this

this 在 JavaScript 里,this 指向 window,在 function 中, t...