本文原文地址在本博主博客,点击链接返回:Go语言中有没有结构化并发?

什么是结构化并发?日常开发中咱们编写的最多就是多线程程序,服务器端利用更是如此,传统的形式都是依附着操作系统提供的1:1线程形式进行申请解决这对于治理和复用线程有很多挑战,如果一个一般线程大小2MB那么开启1000个线程,简直是无奈实现的,并且治理这些线程的状态也是很简单的。明天这篇文章要介绍的是结构化并发,就是为解决并发编程中线程并发工作治理,传统的形式非常容易造成管理混乱。结构化并发解决的问题就是对对立的工作和对立作用域下的工作进行治理,能够对立启动和对立敞开,如果读过我之前的Linux过程组那篇文章的话,就齐全能够了解是什么意思了,文章地址:Linux 过程树。

在理解构造并发编程范式之前得先讲讲编程语言流程管制发展史,理解一件事的全副应该是去理解残缺的历史,并且要找到正确的材料和原版材料去理解,而不是曾经批改几个版本的材料,让咱们回顾编程语言的一些历史:晚期如果想在计算机上写程序必须应用很低级的编程语言去写程序,例如汇编语言,通过一条一条硬件指令去操作计算机,并且程序执行的,这种编写程序的形式真是令人头疼的。这就使一些计算机界大佬想去从新设计一些编程语言,过后一些美籍计算机科学家们John Warner Backus和Grace Hopper开发了Fortran和FLOW-MATIC初代的编译命令式编程语言,最初在这些根底之上开发了商业通用编程COBOL语言。

乏味的事件是世界上的第一个Bug也是Grace Hopper所发现的,过后的计算机(Harvard Mark II)体积还很大。过后这台计算机在运算的时候老是呈现问题,然而通过排查编写的程序指令是没有问题的,最初发现原来是一只飞蛾意外飞入电脑外部的继电器而造成短路如下图所示,他们把这只飞蛾移除后便胜利让电脑失常运作,这就是世界上第一个计算机程序BUG。

晚期的FLOW-MATIC是第一种应用相似英语的语句来表白操作的编程语言,会事后定义输出和输入文件和打印输出,分为输出文件、输入文件和高速打印机输入,上面是一段程序代码的例子:

看完下面的实例,会发现和当初开发者所应用的更高级的Java或者C语言还是有一些差距的,例如没有函数代码块,没有条件管制语句,在FLOW-MATIC被推出的时候这些当初高级语言的个性还没有被创造进去,在过后看来FLOW-MATIC应该是能满足编写程序需要。

构想一下如果和输出指令一条一条执行程序是不是很麻烦,如果不能复用一些以有编写逻辑那就要从新编写一些代码逻辑会很费时费力,所以FLOW-MATIC的设计者在语言退出了GOTO语句块,goto能够让程序在执行的时候执行到了goto而后去执行指定地位的代码块,实质上还是非结构化编程,不过能够做到程序的代码复用和重执行,goto的退出FLOW-MATIC之后如下程序执行流程图:

FLOW-MATIC执行语句通常都是程序执行的,然而上面这种状况就会产生跳转操作,它能够间接将控制权转移到其余中央,例如上面从8行跳转到第4行。

极少量的goto语句是很清晰的,然而令人头疼的问题是程序代码逻辑质变多了之后就会产生很多无奈通过失常人类思维所了解的代码跳转逻辑,并且跟踪代码的逻辑很艰难。这种基于跳转的编程格调是FLOW-MATIC简直间接从汇编语言继承而来的。它功能强大,非常适合计算机硬件的理论工作形式,但间接应用它会十分凌乱。像下面图片中的箭头箭头太多了,就创造Spaghetti Code一词的起因,代码逻辑存在各种飞线关系,揉成一坨的代码逻辑。显然咱们开发者须要更好的流程管制设计,而不是让代码逻辑写进去像意大利面条一样。

当然目前探讨的话题是编程语言的结构化编程设计问题,这个不是本篇文章的重点,本篇文章更偏差的是一些编程语言在线程并发状态转播和管制治理上的一些问题,上面正式开始注释内容。

非结构化并发

介绍了晚期编程语言中的goto关键字,能够在以后的执行控制流中开一个分支去执行另外的操作,和咱们当初在高级编程语言中应用的thread差不多,例如上面代码:

package mainimport (    "fmt"    "time")func f(from string) {    for i := 0; i < 3; i++ {        go fmt.Println(from, ":", i)    }}func main() {    f("direct")    go f("goroutine")    go func(msg string) {        fmt.Println(msg)    }("going")    time.Sleep(2 * time.Second)    fmt.Println("done")}

在线运行代码地址: https://go.dev/play/p/wQ7Yz9mxXlu

在这个例子中我应用的是Go语言的goroutine为例,在Go语言中想启动一个协程就能够应用go关键字,这和下面咱们探讨的goto语句很靠近,会从主控制流中拆散出另一个代码逻辑执行分支,流程如下图:

当然在Go语言中是保留goto跳转语句块的,例如上面这行代码就是Go中的goto语句块:

package mainimport "fmt"func main() {   /* 定义局部变量 */   var a int = 10   /* 循环 */   LOOP: for a < 20 {      if a == 15 {         /* 跳过迭代 */         a = a + 1         goto LOOP      }      fmt.Printf("a的值为 : %d\n", a)      a++       }  }

在这个例子中goto代替了传统的break关键字的作用(那个例子精确来说应该是说相似于continue作用,看怎么用了,这里不承受任何反驳!),间接跳过满足a==15的逻辑块。这就是目前高级语言中的跳转利用,以后这种还是在主程序流上运行的指令的,于Go语言中的go func(){}关键字去跑起一个协程做并行任务解决是齐全不一样的,为此我特定花了一张图来比拟两者的关系,如下:

像下面这样的通过go关键字启动的协程就和一个不通明的盒子一样,你不晓得被启动代码块外面是否还有go关键字启动其余协程,递归启动协程是一件很难管制的事件,这就和mapreduce思维很像,最终还是要汇总的每个协程中产生的数据和管制协程状态的,如下图:

像下面这幅图中如果外面的每个圆圈⭕️都代表着一个正在并行处理工作的协程,咱们要如何治理这些协程状态呢?当然Go语言在设计的时候就引入了channel概念,咱们开发者能够显示将channel提供代码的形式嵌入到每个要执行协程工作代码块中;晚期的Go版本中为了管制协程中的协程状态是间接嵌入channel而后再每个协程外部编写具体状态控制代码,如果下级发送了告诉那么此协程会做出相应的动作,这是初步的Go版本状态管制。

在最新Go语言设计的版本中为了治理这些协程,在语言默认规范库中通过了context包所提供性能来做并行协程上下文通信和状态同步:

package mainimport (    "context"    "fmt"    "time")func doSomething(ctx context.Context) {    ctx, cancelCtx := context.WithCancel(ctx)        printCh := make(chan int)    go doAnother(ctx, printCh)    for num := 1; num <= 3; num++ {        printCh <- num    }    cancelCtx()    time.Sleep(100 * time.Millisecond)    fmt.Printf("doSomething: finished\n")}func doAnother(ctx context.Context, printCh <-chan int) {    for {        select {        case <-ctx.Done():            if err := ctx.Err(); err != nil {                fmt.Printf("doAnother err: %s\n", err)            }            fmt.Printf("doAnother: finished\n")            return        case num := <-printCh:            fmt.Printf("doAnother: %d\n", num)        }    }}

本示例代码在线地址:How To Use Contexts in Go

对于结构化并发在Go语言中一些问题下面是我个人见解,还有一些对于Go中的结构化并发探讨的文章能够查看这篇文章:Go statement considered harmful,在这篇文章外面作者对现有的Go语言协程设计抛出很多观点值得一读。

结构化并发设计

在下面我介绍了一些对于非结构化并发的程序设计问题,如果独自创立协程没有做好错误处理或者异常情况下的解决,可能就会呈现协程泄露问题,这就是本节要讲的结构化并发来做的并发管制设计。

其结构化并发外围设计准则是: <span style="color:red;">能够通过代码明确并发程序工作集的入口点和进口点,并确保所有衍生线程在退出之前实现的控制流结构来封装并发执行线程,能将子线程产生的谬误流传到控制结构到父范畴上,并且达到无子线程存在透露问题。</span>

这里我会拿我目前还略微相熟一点的Java语言举例,例如在Java19中增加的构造体并发个性,所采纳的线程管制就是结构化并发的利用,如下的示例代码:

void serve(ServerSocket serverSocket) throws IOException, InterruptedException {    try (var scope = new StructuredTaskScope<Void>()) {        try {            while (true) {                var socket = serverSocket.accept();                scope.fork(() -> handle(socket));            }        } finally {            // If there's been an error or we're interrupted, we stop accepting            scope.shutdown();  // Close all active connections            scope.join();        }    }}

下面代码的逻辑就是一个简略的socket解决逻辑,采纳的就是结构化并发,能够看到finally外面的异样解决逻辑和scope工作线程块,当然这些内容在Oracle公司的Open JDK设计草案外面就有地址如下:https://openjdk.org/jeps/428,我只是对这篇内容做了导读和个人见解分享,当然这里我拿几个语言作为例子不是为了探讨谁好谁坏,而是从语言设计角度来看每个不同语言面对这些问题是怎么解决的,瑕瑜互见。

小结

我集体认为结构化并发是将来的并发和并行程序设计方向,当初有结构化并发程序设计的语言Kotlin、Java、Swift等,Rust语言中也有这方面相干第三方实现目前还不够欠缺。由此可见通过作用域定义了主协程的子协程的生命周期和关系,事实证明,这一准则在协程中施行了层次结构。如果协程须要为本人创立子协程,那齐全没问题,就像您如何将if语句嵌套在一起并了解分支如何嵌套一样,协程也能够嵌套,最顶级的协程不仅取决于他们的孩子实现,还取决于他们孩子的孩子,这就是一个多叉树型的构造,更多相干钻研还要靠着开发者们一起摸索,我上面给出一些这方面当先的技术文章分享链接。

其余材料

  • Go statement considered harmful