使用TLA+形式化验证Go并发程序

本文永久链接 – https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus

Writing is nature’s way of letting you know how sloppy your thinking is – Guindon

在2024年6月份举办的GopherCon Europe Berlin 2024上,一个叫Raghav Roy的印度程序员(听口音判断的)分享了Using Formal Reasoning to Build Concurrent Go Systems,介绍了如何使用形式化验证工具TLA+来验证Go并发程序的设计正确性。

TLA+是2013年图灵奖获得者、美国计算机科学家和数学家、分布式系统奠基性大神、Paxos算法Latex的缔造者Leslie B. Lamport设计的一种针对数字系统(Digital Systems)的高级(high-level)建模语言,TLA+诞生于1999年,一直低调演进至今。

TLA+不仅可以对系统建模,还可以与模型验证工具,比如:TLC model checker,结合使用,对被建模系统的行为进行全面的验证。我们可以将TLA+看成一种专门用于数字系统建模和验证的DSL语言

注:TLA是Temporal Logic of Actions的首字母缩写,Temporal Logic,即时序逻辑,是一种用于描述和推理系统行为随时间变化的逻辑框架,由Arthur Prior在1950年代后期引入逻辑学。在后面对TLA+的进一步介绍中,大家可能就会逐渐理解为什么Lamport给这门语言命名为TLA+了。

这不是我第一次接触TLA+,去年就花过一些时间了解过TLA+的资料,可能是因为姿势不够正确,没有在本博客留下只言片语,而这次我打算写点有关TLA+的东西。

1. 为什么需要TLA+

从1999年Lamport发表的论文“Specifying Concurrent Systems with TLA+”以及他2014年在微软的演讲“Thinking Above the Code”中 ,我们大致可以得到Lamport在20多年前设计TLA+的朴素的动机:期望程序员能像科学家一样思考,在编码之前用一种精确的形式化的语言写出目标系统的spec,这个过程类似于建筑架构师在建筑施工之前编制建筑的蓝图(blueprint)

为什么要编写目标系统的spec呢?

综合来自Lamport的相关资料,大致可以梳理出以下两点:

  • 从程序员的角度来看,在开始编码之前,先在抽象的层面思考系统行为,而不是过早地陷入编程语言的具体语法中。并且先写下规格说明,可以帮助程序员明确需求,认知系统,发现潜在问题,并为后续的编码和维护提供指导。
  • 从系统复杂性的角度来看,对于日益复杂的并发和分布式系统,仅靠直觉思考很难保证正确性,传统的测试方法也已经不足以发现所有问题。这时候写spec(规格说明)并用配套的检查工具进行验证就变得非常必要。

那为什么要新设计TLA+来写spec呢,而不是使用像C++这类编程语言,或是其他已存在的形式化语言来编写spec呢?

Lamport给出的理由有以下几个:

  • 编程语言的局限性:像C++这样的编程语言主要是为了实现而设计的,而不是为了spec。它们往往过于关注实现细节,而不是高层次的系统行为,缺乏描述并发和分布式系统所需的抽象能力,不适合表达系统的时序性质和不变量。

  • 已有形式化语言的不足:当时存在的其他形式化语言大多存在要么过于学术化,难以在实际工程中应用,要么难以自然地表达并发和分布式系统的特性等问题;并且缺少工具支持,不具备spec验证功能。

  • 数学建模的局限:纯粹的数学公式虽然精确,但对非数学背景的工程师来说难以理解和使用,缺乏工具支持,难以自动化验证,难以直接映射到系统设计和实现。

Lamport设计的TLA+是建立在坚实的数学基础之上,这使得它能够支持严格的数学推理和证明与自动化验证工具(如TLC模型检查器)无缝集成。TLA+被设计为在高度抽象的层面描述系统,不会像编程语言那样受实现细节的束缚。此外,结合时序逻辑和状态机,TLA+可以描述并发和分布式系统,并在设计层面验证系统的正确性。

根据Lamport的不完全统计,TLA+在Intel、Amazon、Microsoft等大厂都有应用,一些知名的算法以及开源项目也使用TLA+进行了形式化验证,比如Raft算法的作者就给出了Raft算法的TLA+ spec,国内分布式数据库厂商pingcap也在项目中使用TLA+对raft算法以及分布式事务做了形式化的验证

在这些应用案例中,AWS的案例是典型代表。AWS也将应用TLA+过程中积累的经验以paper的形式发表了,其论文集合也被Lamport放置在其个人主页上了。从这些论文内容来看,AWS对TLA+的评价是很正面的:AWS使用TLA+对10个大型复杂的真实系统进行建模和验证,的确发现了多个难以通过其他方法发现的微妙错误。同时,通过精确描述设计,TLA+迫使工程师更清晰地思考,消除了“看似合理的含糊之处”。此外,AWS工程师认为TLA+ spec也是一种很好的文档形式,可以提供精确、简洁、可测试的设计描述,有助于新人快速理解系统。

铺垫了这么多,TLA+究竟是什么?它是如何在高级抽象层面对分布式系统和并发系统进行描述和验证的?接下来,我们就来看一下。

2. Lamport对TLA+的定义

在Lamport的论文、书籍以及一些演讲资料中,他是这么定义TLA+的:A language for high-level modeling digital systems。对于这个定义,我们可以“分段”来理解一下。

  • Digital System

什么是TLA+眼中的数字系统(Digital System)?Lamport认为数字系统包括算法(Algorithms)、程序(Programs)和计算机系统(Computer system),它们有一个共同特点,那就是可以抽象为一个按离散事件序列(sequence of discrete events)进行持续执行和演进的物理系统,这是TLA+后续描述(specify)数字系统的基础。随着多核和云计算的兴起,并发程序和分布式的关键(critical)系统成为了TLA+的主要描述对象,这样的系统最复杂,最难正确实现,价值也最高,值得使用TLA+对其进行形式化的验证。

  • High Level

TLA+面向设计层面,在代码实现层面之上,实施于编写任何实现代码之前。此外,High Level也意味着可以忽略那些系统中不是很关键(less-critical)的部分以及低层次的实现细节。

去除细节进行简化的过程就是抽象(Abstraction),它是工程领域最重要的环节。抽象可以让我们理解复杂的系统,如果不了解系统,我们就无法对系统进行正确的建模并实现它。

而使用TLA+编写系统spec其实就是一个学习对系统进行抽象的过程,学会抽象思考,可以帮助工程师提高设计能力。

  • Modeling

TLA+是通过描述系统的行为(behavior)来对数字系统进行建模的。那么什么是系统的行为呢?如下图所示:


此图由claude sonnet 3.5根据我的prompt生成

行为被Lamport定义为一系列的状态(Sequence of States),这些状态仍然按顺序排列,表示系统随时间的演变。而状态本身则是对变量的赋值。状态之间的转换由动作(action)描述,而系统的正确性由属性(properties)指定。

这种方法特别适合建模并发和分布式系统,因为它允许我们精确地描述系统的所有可能行为,包括不同组件之间的交互和可能的竞争条件,如下图所示:

在TLA+中,属性(properties)是用来描述系统应该满足的条件或特性,它们在验证系统行为的正确性方面起着关键作用。我们所说的系统工作正常就是指这些在执行过程中的属性都得到了满足。

在TLA+中,有两类属性是我们特别需要关注的,一类是安全属性(Safety Properties),一类则是活性属性(Liveness Properties)。前者确保“坏事永远不会发生”,比如使用不变量在并发系统中确保两个进程不会同时进入临界区;后者则是确保“好事最终会发生”,在分布式系统中的最终一致性(eventual consistency)是一个活性属性,它保证系统最终会达到一致的状态。TLA+允许我们精确地指定这些属性,然后使用TLC模型检查器来验证系统是否满足这些属性。这种方法特别适合于复杂的并发和分布式系统,因为它能够发现在传统测试中难以发现的微妙错误。

注:关于TLA+可以用来形式化描述(specify)和验证(check)数字系统的底层数学理论,可以参考Lamport老爷子那本最新尚未完成的书籍A Science of Concurrent Programs(2024.6.7版)

接下来,我们就来看看TLA+究竟如何编写。不过直接介绍TLA+语法比较抽象和枯燥,在我读过的TLA+语法资料中,Lamport在The TLA+ Video Course第二讲中将一个C示例程序一步一步像数学推导一样转换为TLA+语法的讲解对我帮助非常大,我觉得有必要将这个示例放到这篇文章中。

3. 从C代码到TLA+:转换步骤详解

Lamport的这个过程展示了如何从一个具体的编程语言实现(以C代码为例)逐步抽象到一个数学化的、更加通用的系统描述。每一步都增加了抽象级别,最终得到一个可以用于形式化验证的TLA+规范(spec)。以下是这个演进过程的主要阶段:

3.1 初始C程序分析

下面是这个示例的原始C代码:

int i;
void main() {
    i = someNumber();
    i = i + 1;
}

这不是一个并发程序,它只有一个执行路线(execution),前面说过,一个行为(execution)是一个状态序列,我们就来定义这个状态序列以及它们之间的转换关系。

我们先识别出程序的状态变量:i以及引入的控制状态变量(PC),PC变量来表示程序的执行位置。接下来我们就来描述一个可以代码该程序所有状态的“状态机”。

3.2 状态机描述

该程序可以划分为三个状态:

  • 初始状态:i = 0, PC = “start”
  • 中间状态:i in {0, 1, …, 1000}(这里限定了someNumber函数返回的数值范围), PC = “middle”
  • 结束状态:i = i + 1, PC = “done”

下面用自然语言描述一下上述状态的转换关系:

if current value of pc equals "start"
    then next value of i in {0, 1, ..., 1000}
         next value of pc equals "middle"
    else if current value of pc equals "middle"
            then next value of i equals current value of i + 1
                 next value of pc equals "done"
            else no next values

接下来,我们就来将上述对于状态转换的描述变换一下,尽量用数学来表示。

3.3 转换为数学表示

这里的转换分为几步,我们逐一来看。

  • 换掉”current value of”
if pc equals "start"
    then next value of i in {0, 1, ..., 1000}
         next value of pc equals "middle"
    else if pc equals "middle"
            then next value of i equals i + 1
                 next value of pc equals "done"
            else no next values

替换后,pc即the current value of pc,i即current value of i。

  • 换掉”next value of”

我们用i’换掉”next value of i”, 用pc’换掉”next value of pc”,结果如下:

if pc equals "start"
    then i' in {0, 1, ..., 1000}
         pc' equals "middle"
    else if pc equals "middle"
            then i' equals i + 1
                 pc' equals "done"
            else no next values
  • 用”=”符号换掉equals

替换的结果如下:

if pc = "start"
    then i' in {0, 1, ..., 1000}
         pc' = "middle"
    else if pc = "middle"
            then i' = i + 1
                 pc' = "done"
            else no next values
  • 将in换为数学符号∈
if pc = "start"
    then i' ∈ {0, 1, ..., 1000}
         pc' = "middle"
    else if pc = "middle"
            then i' = i + 1
                 pc' = "done"
            else no next values

3.4 TLA+语法转换

  • 将集合表示换为正式的数学符号

{0, 1, …, 1000}并非数学表示集合的方式,替换后,结果如下:

if pc = "start"
    then i' ∈ 0..1000
         pc' = "middle"
    else if pc = "middle"
            then i' = i + 1
                 pc' = "done"
            else no next values

这里0..1000使用了TLA+的集合表示语法。

  • 转换为单一公式(formula)

将C代码转换为上面的最新代码后,你不要再按照C的语义去理解上述转换后的代码了。新代码并非是像C那样为了进行好一些计算而编写的一些指令,新代码是一个关于i、pc、i’和pc’的公式(formula),这是理解从C带TLA+的最为关键的环节,即上述这段代码整体就是一个公式

上述代码的意思并非if pc = “start”为真,然后执行then部分,否则执行else部分。其真正含义是如果pc = “start”为真,那么上述整个公式将等于then这个公式的值,否则整个公式将等于else公式的值。

不过我们看到在上面的then子句中存在两个独立的公式,以第一个then为例,两个独立公式分别为i’ ∈ 0..1000和pc’ = “middle”。这两个独立的公式之间是and的关系,我们需要将其转换为一个公式。TLA+中使用”/\”表示and连接,下面是使用”/\”将公式连接后的结果:

if pc = "start"
    then (i' ∈ 0..1000) /\
         (pc' = "middle")
    else if pc = "middle"
            then (i' = i + 1) /\
                 (pc' = "done")
            else no next values
  • 改造else公式

问题来了! 当存在某个状态,使得整个公式等于最后一个else公式的值时,我们发现这个值为”no next values”,而前面的then、else if then公式的值都为布尔值TRUE或FALSE。这里最后的ELSE公式,它的值应该为FALSE,无论i、pc、i’和pc’的值为什么,因此这里直接将其改造为FALSE:

if pc = "start"
    then (i' ∈ 0..1000) /\
         (pc' = "middle")
    else if pc = "middle"
            then (i' = i + 1) /\
                 (pc' = "done")
            else FALSE
  • TLA+的关键字为大写且TLA+源码为ASCII码

if、then、else 这些都是TLA+的关键字,而TLA+的关键字通常为大写,并且TLA+源码为ASCII码,∈需换成\in。这样改变后的结果如下:

IF pc = "start"
    THEN (i' \in 0..1000) /\
         (pc' = "middle")
    ELSE IF pc = "middle"
            THEN (i' = i + 1) /\
                 (pc' = "done")
            ELSE FALSE

到这里,我们就得到了一个美化后的的TLA+公式了!

3.5 干掉if else

前面说过,我们将C代码改造为了一个公式,但公式中依然有if else总是感觉有些格格不入,是不是可以干掉if else呢!我们来试一下!

我们先用A、B替换掉then语句中的两个公式:

IF pc = "start"
    THEN A
    ELSE IF pc = "middle"
            THEN B
            ELSE FALSE

如果整个公式为TRUE,需要(pc = “start”)和A都为TRUE,或(pc = “middle”)和B都为TRUE。TLA+引入一个操作符\/表示or,这样整个公式为TRUE的逻辑就可以表示为:

   ((pc = "start") /\ A)
\/ ((pc = "middle") /\ B)

好了,现在我们再把A和B换回到原先的公式:

   ((pc = "start") /\
    (i' \in 0..1000) /\
    (pc' = "middle"))
\/ ((pc = "middle") /\
    (i' = i+1 ) /\
    (pc' = "done"))

你是不是感觉不够美观啊!TLA+提供了下面等价的、更美观的形式:

\/ /\ pc = "start"
   /\ i' \in 0..1000
   /\ pc' = "middle"
\/ /\ pc = "middle"
   /\ i' = i+1
   /\ pc' = "done"

这种形式完全去掉了括号,并可以像列表一样表达公式!并且无论是/\还是\/都是可交换的(commutative),顺序不影响公式的最终结果。

3.6 完整的TLA+ spec

从数学层面,上面C代码将被拆分为两个公式,一个是初始状态公式,一个是下个状态的公式:

初始状态公式:(i = 0) /\ (pc = "start")
下一状态公式:
              \/ /\ pc = "start"
                 /\ i' \in 0..1000
                 /\ pc' = "middle"
              \/ /\ pc = "middle"
                 /\ i' = i+1
                 /\ pc' = "done"

但对于一个完整的TLA+ spec来说,还需要额外补充些内容:

---- MODULE SimpleProgram ----

EXTENDS Integers
VARIABLES i, pc

Init == (pc = "start") /\ (i = 0)
Next == \/ /\ pc = "start"
           /\ i' \in 0..1000
           /\ pc' = "middle"
        \/ /\ pc = "middle"
           /\ i' = i + 1
           /\ pc' = "done"
====

一个完整的TLA+ spec是放在一个module中的,上面例子中module为SimpleProgram。TLA toolkit要求tla文件名要与module名相同,这样上面代码对应的tla文件应为SimpleProgram.tla。

EXTENDS会导入TLA+内置的标准module,这里的Integers就提供了基础的算术运算符,比如+和..。

VARIABLES声明了状态变量,比如这里的i和pc。变量加上’即表示该变量的下一个状态的值。

接下来便是公式的定义。Init和Next并非固定公式名字,你可以选择任意名字,但使用Init和Next是惯用法。

“====”用于标识一个module的Body内容的结束。

对于上面简单的C程序,这样的spec是可以的。但在实际使用中,spec中的Next一般会很长,一个好的实践是对其进行拆分。比如这里我们就将Next拆分为两个子公式:Pick和Add1:

---- MODULE SimpleProgram ----

EXTENDS Integers
VARIABLES i, pc

Init == (pc = "start") /\ (i = 0)
Pick == /\ pc = "start"
        /\ i' \in 0..1000
        /\ pc' = "middle"
Add1 == /\ pc = "middle"
        /\ i' = i + 1
        /\ pc' = "done"
Next == Pick \/ Add1
====

4. 使用TLA+ Toolkit验证spec

Lamport提供了TLA+的Module Checker,我们可以从其主页提供的工具包下载链接下载TLA+ Toolkit。

先将上面的TLA+ spec存入一个名为SimpleProgram.tla的文件。然后打开TLA+ Toolkit,选择File -> Open spec -> Add New Spec…,然后选择你本地的SimpleProgram.tla即可加载该spec:

之后,我们可以点击菜单项“TLC Model Checker” -> New Model,便可以为该tla建立一个model配置(去掉deadlock),运行check后,你能看到下面结果:

我们看到model check一共检查了2003个不同的状态。

注:TLA+还提供了一个Visual Studio Code的扩展,也可以用来specify和check model。

5. 使用TLA+验证Go并发程序

Go语言因其强大的并发编程能力而备受青睐。然而,Go的并发方案虽然简单,但也并非银弹。随着并发程序复杂性的增加,开发者常常面临着难以发现和调试的错误,如死锁和竞态条件。这些问题不仅影响程序的正确性,还可能导致严重的系统故障。对于Go开发的并发系统的关键部分,采用TLA+进行形式化的验证是一个不错的提高系统正确性和可靠性的方法。

接下来,我们就建立一个生产者和消费者的Go示例,然后使用TLA+为其建模并check。理论上应该是先有设计思路,再TLA+验证设计,再进行代码实现,这里的Go代码主要是为了“描述”该并发程序的需求和行为逻辑。

// go-and-tla-plus/producer-consumer/main.go
package main

import (
    "fmt"
    "sync"
)

func producer(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for num := range ch {
        fmt.Println("Consumed:", num)
    }
}

func main() {
    ch := make(chan int)
    var wg sync.WaitGroup
    wg.Add(2)

    go producer(ch, &wg)
    go consumer(ch, &wg)

    wg.Wait()
}

任何Go初学者都可以很容易读懂上面的程序逻辑:Producer生产0到4四个数,每生成一个就通过unbuffered channel发出,consumer从channel接收数字并消费。Producer生产完毕后,关闭channel。Consumer消费完所有数字后,退出,程序终止。

下面是使用TLA+编写的ProducerConsumer的完整Spec:

// go-and-tla-plus/producer-consumer/ProducerConsumer.tla

---- MODULE ProducerConsumer ----
EXTENDS Integers, Sequences

VARIABLES
    ch,           \* 通道内容
    produced,     \* 已生产的消息数
    consumed,     \* 已消费的消息数
    closed        \* 通道是否关闭

TypeOK ==
    /\ ch \in Seq(0..4)
    /\ produced \in 0..5
    /\ consumed \in 0..5
    /\ closed \in BOOLEAN

Init ==
    /\ ch = <<>>
    /\ produced = 0
    /\ consumed = 0
    /\ closed = FALSE

Produce ==
    /\ produced < 5
    /\ ch = <<>>
    /\ ~closed
    /\ ch' = Append(ch, produced)
    /\ produced' = produced + 1
    /\ UNCHANGED <<consumed, closed>>

Close ==
    /\ produced = 5
    /\ ch = <<>>
    /\ ~closed
    /\ closed' = TRUE
    /\ UNCHANGED <<ch, produced, consumed>>

Consume ==
    /\ ch /= <<>>
    /\ ch' = Tail(ch)
    /\ consumed' = consumed + 1
    /\ UNCHANGED <<produced, closed>>

Next ==
    \/ Produce
    \/ Close
    \/ Consume

Fairness ==
    /\ SF_<<ch, produced, closed>>(Produce)
    /\ SF_<<produced, closed>>(Close)
    /\ SF_<<ch>>(Consume)

Spec == Init /\ [][Next]_<<ch, produced, consumed, closed>> /\ Fairness

THEOREM Spec => []TypeOK

ChannelEventuallyEmpty == <>(ch = <<>>)
AllMessagesProduced == <>(produced = 5)
ChannelEventuallyClosed == <>(closed = TRUE)
AllMessagesConsumed == <>(consumed = 5)

====

这个Spec不算长,但也不短,你可能看不大懂,没关系,接下来我们就来说说从main.go到ProducerConsumer.tla的建模过程,并重点解释一下上述TLA+代码中的重要语法。

针对main.go中体现出来的Producer和Consumer的逻辑,我们首先需要识别关键组件:生产者、消费者和一个通道(channel),然后我们需要确定状态变量,包括:通道内容(ch)、已生产消息数(produced)、已消费消息数(consumed)、通道是否关闭(closed)。

接下来,我们就要定义action,即导致状态变化的step,包括Produce、Consume和Close。

最后,我们需要设置初始状态Init和下一个状态Next,并定义安全属性(TypeOK)和一些活性属性(如AllMessagesConsumed等)

现在,我们结合上述TLA+的代码,来说一下上述这些逻辑是如何在TLA+中实现的:

---- MODULE ProducerConsumer ----

这一行定义了模块名称,模块名称与文件名字(ProducerConsumer.tla)要一致,否则TLA+ Toolkit在Open Spec时会报错。

EXTENDS Integers, Sequences

这行会导入整数和序列模块,以使用相关运算符。

VARIABLES
    ch,           \* 通道内容
    produced,     \* 已生产的消息数
    consumed,     \* 已消费的消息数
    closed        \* 通道是否关闭

这里使用VARIBALES关键字定义了四个状态变量,整个TLA+程序的函数逻辑就围绕这四个变量进行,TLC Model check也是基于这些状态变量对TLA+ module进行验证。

TypeOK ==
    /\ ch \in Seq(0..4)
    /\ produced \in 0..5
    /\ consumed \in 0..5
    /\ closed \in BOOLEAN

定义不变量,确保变量状态在系统的所有行为过程中始终保持在合理范围内,该TypeOK不变量即是整个程序的安全属性。

Init ==
    /\ ch = <<>>
    /\ produced = 0
    /\ consumed = 0
    /\ closed = FALSE

这是初始状态的公式,对应了四个变量的初始值。

Produce ==
    /\ produced < 5
    /\ ch = <<>>
    /\ ~closed
    /\ ch' = Append(ch, produced)
    /\ produced' = produced + 1
    /\ UNCHANGED <<consumed, closed>>

这里定义了生产操作的公式,只有在produced < 5,ch为空且closed不为true时,才会生产下一个数字。这里设定ch为空作为前提条件,主要是为了体现Channel的unbuffered的性质。

Close ==
    /\ produced = 5
    /\ ch = <<>>
    /\ ~closed
    /\ closed' = TRUE
    /\ UNCHANGED <<ch, produced, consumed>>

这里定义了关闭操作的公式,这里的ch = <<>>子公式的目的是等消费完之后再关闭channel,当然这里与Go的机制略有差异。

Consume ==
    /\ ch /= <<>>
    /\ ch' = Tail(ch)
    /\ consumed' = consumed + 1
    /\ UNCHANGED <<produced, closed>>

这里定义了消费操作的公式,只有channel不为空,才进行消费。

Next ==
    \/ Produce
    \/ Close
    \/ Consume

这里基于三个操作公式定义了下一个状态(Next)的公式,使用\/运算符将这三个操作连接起来,表示下一步可以执行其中任意一个操作。

Fairness ==
    /\ SF_<<ch, produced, closed>>(Produce)
    /\ SF_<<produced, closed>>(Close)
    /\ SF_<<ch>>(Consume)

这里定义了公平性条件,确保各操作最终会被执行。

Spec == Init /\ [][Next]_<<ch, produced, consumed, closed>> /\ Fairness

这里定义了整个并发程序的规范,包括初始条件Init和下一步动作约束以及Fairness条件。/\连接的第二段Next表示系统的每一步都必须符合Next定义的可能动作,并且不会改变 <<ch, produced, consumed, closed>> 元组中变量之外的其他变量。Fairness 表示系统必须满足前面定义的 Fairness 条件。

THEOREM Spec => []TypeOK

这是一个定理,表示如果系统满足Spec规范,则一定会满足TypeOK这个不变量。其中的”=>”是蕴含的意思,A => B表示如果A为真,那么B必然为真。用一个例子可以解释这点,如果x > 3为真,那么 x > 1 必为真,我们可以将其写为:x > 3 => x > 1。

ChannelEventuallyEmpty == <>(ch = <<>>)
AllMessagesProduced == <>(produced = 5)
ChannelEventuallyClosed == <>(closed = TRUE)
AllMessagesConsumed == <>(consumed = 5)

这里定义了四个活性属性,用于在TLC Model check时验证最终状态使用,其中:ChannelEventuallyEmpty表示最终消息队列 ch 一定会为空;AllMessagesProduced表示最终一定会生产5条消息;ChannelEventuallyClosed表示最终消息队列一定会被关闭;AllMessagesConsumed表示最终一定会消费5条消息。

接下来,我们可以使用前面提到的TLA+ Toolbox来check该spec,下面是model的设置和model check的结果:


model设置


check结果

注:在VSCode中使用TLA+插件的Checker对上述tla进行check,会出现不满足活性属性的error结果。

6. 小结

在这篇文章中,我们从Lamport提供的C语言代码示例出发,一步步介绍了如何将其转换为TLA+ spec,并使用TLA+ Toolkit进行验证。然后我们又以一个Go语言的生产者-消费者并发程序为例,展示了如何使用TLA+对其进行建模和验证。

不过我必须承认,TLA+这种形式化验证语言是极小众的。对大多数程序员来说,可能没什么实际帮助。即便是在大厂,真正使用TLA+对分布式系统进行形式化验证的案例也很少。

但是,我认为TLA+仍然有其独特的价值:

  • 它迫使我们用更抽象和精确的方式思考系统设计,有助于发现潜在的问题。
  • 对于一些关键的分布式系统组件,使用TLA+进行验证可以极大地提高可靠性。
  • 学习TLA+的过程本身就是一次提升系统设计能力的过程。

当然,形式化方法并非万能。比如它无法解决性能退化等问题,也不能验证代码是否正确实现了设计。我们应该将其视为系统设计和验证的补充工具,而不是替代品。

总之,虽然TLA+可能不适合所有人,但对于那些构建复杂分布式系统的工程师来说,它仍然是一个值得学习和使用的强大工具。我希望这篇文章能为大家了解和入门TLA+提供一些帮助。

本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/go-and-tla-plus

本文部分源代码由claude 3.5 sonnet生成。

7. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Gopher Daily支持Feed订阅

本文永久链接 – https://tonybai.com/2024/08/04/gopherdaily-add-feed-support

在《Russ Cox“退位”,Go语言将迎来新领袖,未来将如何发展》一文的评论区,有Gopher问及gopher-daily.com的HTTPS证书过期的问题,这里再正式声明一下:gopher-daily.com并非Gopher Daily的官方邮件列表,而是由一位名为Bastengao的热心Gopher维护的第三方邮件列表。Gopher Daily以前没有官方邮件订阅功能,该邮件列表弥补了广大Gopher通过邮件订阅Gopher Daily的需求,这里再次表示感谢。

不过,一年前,我为Gopher Daily增加了官方邮件列表,大家可以在Gopher Daily主页实现邮件订阅

接下来,还是那位提问的Gopher又询问Gopher Daily是否支持RSS订阅,我当时如实相告:暂不支持。

不过,这个周末针对邮件列表支持RSS订阅的功能需求,我也简单调研了一下,似乎很多技术类Newsletter都不支持。不过,我询问了一下Claude 3.5 Sonnet邮件列表和RSS订阅的优缺点,下面是它的回答摘录:

邮件列表和RSS订阅都是传播信息的有效方式,但各有优缺点。

邮件列表的优点:

1. 直接送达:信息直接发送到订阅者的收件箱,更容易引起注意。
2. 个性化:可以根据订阅者的兴趣定制内容。
3. 互动性:读者可以直接回复邮件与发送者交流。
4. 数据分析:可以追踪打开率、点击率等数据。
5. 无需额外软件:用户只需要邮箱就可以接收。

邮件列表的缺点:

1. 可能被当作垃圾邮件:有时会被误分类或过滤。
2. 信息过载:频繁的邮件可能让订阅者感到烦扰。
3. 隐私问题:需要收集用户的邮箱地址。
4. 送达率:不能保证100%送达。

RSS订阅的优点:

1. 实时更新:内容一发布就可以被获取。
2. 集中阅读:可以在一个RSS阅读器中浏览多个来源的内容。
3. 无垃圾邮件风险:不会被误认为是垃圾邮件。
4. 匿名性:不需要提供个人信息。

RSS订阅的缺点:

1. 需要额外软件:用户需要使用RSS阅读器。
2. 缺乏个性化:难以针对个别用户定制内容。
3. 互动性较差:不易与内容创作者直接交流。
4. 数据分析有限:难以获得详细的用户行为数据。

总的来说,邮件列表更适合需要个性化、互动性强的内容传播,而RSS则更适合频繁更新、希望读者自主选择阅读时间的内容。选择哪种方式取决于你的目标受众和内容类型。

我觉得邮件列表最大的问题就是到达率问题,现在各大电邮服务对于垃圾邮件的过滤还是很严格的,甚至“宁可错杀三千,不可放走一人”,我个人订阅的很多Newsletter也总是被误杀,导致偶尔收不到。出于让更多人能看到Gopher Daily的考虑,我决定给Gopher Daily Newsletter增加RSS订阅功能

给一个站点或邮件列表增加RSS订阅功能至少有两种方案,一种是利用一些RSSHub之类的信息聚合服务站点直接将站点转换为一个RSS源,这种方案就需要依赖这样的RSS源转换服务。另外一种就是自己实现RSS源服务。

RSS(Really Simple Syndication)实际上是在Web 1.0时代发展起来的,但它在Web 2.0的环境中得到了广泛应用。Web 2.0强调用户生成内容和互动,而RSS允许用户方便地订阅和获取来自不同网站的内容更新,促进了信息的分发和共享。

信息订阅技术目前演进到RSS 2.0和Atom订阅阶段。RSS 2.0是最广泛使用的RSS版本,但它并不是一个正式的IETF(Internet Engineering Task Force)标准,因此没有官方的RFC。然而,它有一个详细的规范文档,在RSS Advisory Board上可以看到。

鉴于RSS缺乏标准化,Atom格式被开发出来作为一个标准化的替代品。Atom是有正式的RFC规范的:RFC 4287: The Atom Syndication Format。不过,无论是RSS 2.0还是Atom规范,都不复杂。

Atom规范中举的一个最简单的single entry的Feed源的响应数据示例如下:

 <?xml version="1.0" encoding="utf-8"?>
   <feed xmlns="http://www.w3.org/2005/Atom">

     <title>Example Feed</title>
     <link href="http://example.org/"/>
     <updated>2003-12-13T18:30:02Z</updated>
     <author>
       <name>John Doe</name>
     </author>
     <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>

     <entry>
       <title>Atom-Powered Robots Run Amok</title>
       <link href="http://example.org/2003/12/13/atom03"/>
       <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
       <updated>2003-12-13T18:30:02Z</updated>
       <summary>Some text.</summary>
     </entry>

   </feed>

于是,我决定自己来基于Go http handler标准库的xml包为Gopher Daily服务加上Atom版的订阅支持,无需使用任何第三方包。增加订阅源后,还可以使用W3C的免费的Feed Validation Service来验证Feed是否是符合规范的:

下面是使用feeder.co/reader订阅Gopher Daily Atom源的效果图:

这张图上还保留了调试过程的“痕迹”,从最初的只有summary,到后期的可以输出全文(由于每一期Gopher Daily的篇幅都不多,因此直接在Feed源输出了全文)。

借这次机会,我顺便对Gopher Daily的模板做了调整,在原本放在每一期下方的快捷链接放到了最上方,这样可以更加方便大家的操作:

屏幕前的各位Gopher,如果你更喜欢Feed(RSS/Atom)订阅方式查看Gopher Daily,请现在就把Gopher Daily的订阅源(右键 -> 复制链接) – https://gopherdaily.tonybai.com/feed 加到你的Feed Reader里吧!


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats