标签 vscode 下的文章

使用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

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

Go团队的工作方式

本文永久链接 – https://tonybai.com/2024/06/22/how-things-get-done-on-the-go-team

Go 1.23版本即将发布(2024.8)之前,在GopherCon 2024开幕(2024.7)之前,Go团队成员Cameron Balahan(Go产品负责人)、 Sameer Ajmani(Go团队工程总监)和Russ Cox(Go团队技术负责人)参加了业界知名的播客栏目GoTime的最新一期活动,主题是“How things get done on the Go Team”。在这期活动中,Go团队这三个leader分享了Go团队的工作方式,包括:Go团队的组成、现状与职责划分、与社区互动、决策与规划流程、产品管理等方面。这里基于这期播客的脚本提炼了其中主要的观点,贴到这里供大家参考。

1. Go团队组成及职责划分

Go团队从2007年诞生,至今已经有17年了。最初的Go团队由罗伯·派克(Rob Pike)、罗伯特·格瑞史莫(Robert Griesemer)和肯·汤普森(Ken Thompson)三个Go语言之父组成。之后Russ Cox和Ian Lance Taylor加入团队,形成了Go团队最核心的五人组。

Sameer Ajmani在Go 1.0发布前后加入,当时团队有10几个人,我们熟悉的context包就是由他和Russ Cox一起设计并实现的。

Cameron Balahan在4年前加入Go团队,他也是今年在Google I/O大会上做“Go是一个平台”演讲的Go团队成员。

目前Google内部组织调整后,Go团队划归Google云团队管理,但其工作相对独立。 现在,Go团队由不同小组组成,主要包括三个小组:核心组、工具组和安全组。核心组负责编译器、运行时、链接器以及核心发布流程。工具组负责构建系统、Go命令、Go VSCode IDE插件以及gopls语言服务器等。安全组则专注于Go的供应链安全性漏洞扫描和修复等方面。

尽管划分了不同的小组,但Go团队在日常工作中感觉就像是一个整体,各小组之间合作紧密。特定任务往往需要几个小组共同参与,例如漏洞检测与修复功能的开发就涉及了核心组、工具组和安全组的工作。

Go团队的工作由核心成员和开源社区两部分组成。核心成员负责构建整体框架与关键功能,而开源社区则为Go语言贡献众多细节上的改进和完善。两者紧密互动,形成良性循环。

2. Go团队与Go社区的互动

Go社区对语言发展做出了重大贡献,因此Go团队始终采取非常积极开放的态度与社区互动。包括但不限于使用Slack、邮件列表、Issue跟踪、Go博客等多种渠道倾听Go社区声音,接纳Go社区贡献。任何人都可以参与讨论并提出建议。

目前较为正式的决策途径是“Go提案流程(Proposal Process)”。任何人都可以在这一平台上提出建议,供Go团队和全体社区评议。不论大小,只要通过审议,这些建议都可能被纳入语言或生态系统的未来发展规划。

除了直接参与讨论和决策外,Go社区还可以通过编写代码、发现并报告漏洞等方式为Go语言做贡献。Go团队会将高质量的外部代码整合进官方发行版。

3. 决策与规划流程

Go团队在做决策时,会优先考虑目标的一致性和充分的信息共享(比如公开利用Go遥测工具采集的数据)。如果出现分歧,通常是由于目标不一致或信息不对称(以类型别名加入Go的过程为例)造成的。因此,团队会先明确共同目标,并确保每个人掌握了相同的信息,然后再做出决策。

在规划过程中,Go团队首先要考虑Go语言既定的目标,即能够同时处理”生产规模化”(大量机器与海量数据)和”人力规模化”(大型项目与众多贡献者)。任何需要持续10年以上的重大决策,都必须符合这两个目标。

从长远来看,安全性与开源软件的可持续发展是Go团队需要重点关注的问题。他们将积极主导新标准与新模式,以提高整个行业的供应链安全性水平。

功能规划上,Go团队会同时考虑Go用户/社区和Google内部需求:Go用户和Go社区从Go中寻找价值,比如高生产力、高性能、高可靠和高安全;Google要确保其内部系统运行良好,开发人员满意,其系统可靠,安全,诸如此类。当然,Google也希望外部Go开发人员也这样做。同时,Google也希望那些外部的Go开发人员获得成功和快乐。为此,Go团队会寻求双赢解决方案。比如兼容性工作就是为了满足Kubernetes等重要系统的需求(IP地址解析)。在新特性开发过程中,Go团队会确保功能在整个生态链上保持一致性。

在发布规划上,Go团队需要考虑两个周期,一个是Go团队公开的Go版本发布周期,主版本一年两次。同时,Go团队leader还要考虑内部Google的规划周期,往往有一个年度计划周期,Go团队在其中执行 OKR、目标和关键结果。

4. 产品管理与Go的未来展望

作为Go产品负责人,CAMERON BALAHAN认为他会从优先级路线图、愿景角度以及Go团队为用户/社区和Google提供的价值的角度来弄清楚Go是什么,他认为Go是用于开发生产级软件的高效平台。作为编程语言,Go语言的产品管理理念就是构建一个高效且稳定的平台,支撑”生产级软件的高效开发”。

Go在解决云问题方面非常成功。云的大部分基础设施都是用Go编写的,并且Go在这方面做得很好,具有独特优势。Go团队希望Go在这一领域能够提供持续性的方案并取得持续性的成功,这决定了Go团队关注两个核心要素:生产效率和软件质量,这其中包括可靠性、安全性等重要的要素。

此外,Sameer认为人工智能的发展也为Go带来了新的机遇,随着越来越多的大公司、企业和初创公司希望在人工智能模型之上构建系统,而如何使Go成为构建智能基础设施以及基于大模型构建生产级、值得信赖、可靠的AI应用系统的语言,是下一个重要的前沿领域,Go团队将看到对此的大量需求,并认为Go是一个非常合适的选择。Go团队也在拭目以待!

编程语言的采用是一个缓慢的过程。Go语言目前已经到了一个关键的增长点,有望在新兴计算领域(AI)获得更广泛的使用。团队需要持续关注新的计算范式,及时调整以满足新需求。

Go社区对该语言的热爱是Go发展的重要动力。整个Go社区都是建立在Go之上的。Go团队本身无法建造所有东西,Go团队只要确保Go用户能够使用Go构建他们需要构建的东西,积极赋能社区发挥创造力,丰富Go的生态系统,才能继续让Go保持在人们需要的那种前沿,以便建立他们的业务、构建软件、构建他们需要的东西,生产级的高效、安全与可靠。

5. 不受欢迎的观点(GoTime常设环节)

  • Sameer:context is fine。
  • Cameron:I really like Go’s error handling.
  • Russ:null pointers are fine. They’re kind of a fundamental fact of computers, is that memory can be zeroed.

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