开发者社区> 游客pq45jrzoavplw> 正文

GO、Rust这些新一代高并发编程语言为何都极其讨厌共享内存?

简介: 今天我想再来讨论一下高并发的问题,我们看到最近以Rust、Go为代表的云原生、Serverless时代的语言,在设计高并发编程模式时往往都会首推管道机制,传统意义上并发控制的利器如互斥体或者信号量都不是太推荐。
+关注继续查看


今天我想再来讨论一下高并发的问题,我们看到最近以Rust、Go为代表的云原生、Serverless时代的语言,在设计高并发编程模式时往往都会首推管道机制,传统意义上并发控制的利器如互斥体或者信号量都不是太推荐。

这里我们先来看一下并发和并行的概念,我们知道并发是一个处理器同时处理多个任务,这里同时是逻辑上的,而并行同一时刻多个物理器同时执行不同指令,这里的同时物理上的。并发是要尽量在目前正在执行的任务遇到阻塞或者等待操作时,释放CPU,让其它任务得以调度,而并行则是同时执行不同任务而不相互影响。

而传统的信号量、互斥体的设计都是为了让单核CPU发挥出最大的性能,让程序在阻塞时释放CPU,通过控制共享变量的访问来达到避免冲突的目的,而想控制好这些共享变量的行为,其关键因此在于设计好时序,从本质上讲控制时序就是给系统加上红绿灯并配备路障,而这里你一定要记住,高性能系统需要的是立交桥、地下隧道这些基础设施,而不是交通信号等控制手段,好的并发系统一定要用流的概念来建模,而不是到处增加关卡路障。现在的处理都是多核架构,因此编程也要向并行倾斜,不过笔者在网上看到很多所谓标榜高并发教程中所举的例子,都把信号灯设计的时序很完美,却偏偏把立交桥全给扔了…..

信号灯应该为导流服务,而不应为限流而生

下面我们来看三段分别对应信号灯控制的操作,互斥体统治的“并发”,以及单纯的串行的代码,代码的目标其实就是要完成从0一直加到3000000的操作

信号灯控制

其实这种信号量的代码已经基本退化回了顺序执行的方案了。正如我们在前文《GO看你犯错,但是Rust帮你排坑所说》,Rust的变量生命周期检查机制,并不能支持在不同线程之间共享内存,即便可以曲线救国,也绝非官方推荐,因此这里先用Go带各位读者说明。

package main
import (
        "fmt"
        "sync"
        "time"
)
var count int
var wg1 sync.WaitGroup
var wg2 sync.WaitGroup
var wg3 sync.WaitGroup
var wg4 sync.WaitGroup
func goroutine1() {
        wg1.Wait()
        len := 1000000
        for i := 0; i < len; i++ {
                 count++
        }
        wg2.Done()
}
func goroutine2() {
        wg2.Wait()
        len := 1000000
        for i := 0; i < len; i++ {
                 count++
        }
        wg3.Done()
}
func goroutine3() {
        wg3.Wait()
        len := 1000000
        for i := 0; i < len; i++ {
                 count++
        }
        wg4.Done()
}
func main() {
        now := time.Now().UnixNano()
        wg1.Add(1)
        wg2.Add(1)
        wg3.Add(1)
        wg4.Add(1)
        go goroutine1()
        go goroutine2()
        go goroutine3()
        wg1.Done()
        wg4.Wait()
        fmt.Println(time.Now().UnixNano() - now)
        fmt.Println(count)
}

image.gif

在这里三个子协程goroutine,在4个信号量的控制下以多米诺骨牌的方式依次对于共享变量count进行操作,这段代码的运行结果如下:

4984300
3000000
成功: 进程退出代码 0.

image.gif

互斥体控制

与信号量完全退化成顺序执行不同,互斥体本质上同一时刻只能有一个goroutine执行到临界代码,但每个goroutine的执行顺序却无所谓,具体如下:

package main
import (
        "fmt"
        "sync"
        "time"
)
var count int
var wg1 sync.WaitGroup
var mutex sync.Mutex
func goroutine1() {
        mutex.Lock()
        len := 1000000
        for i := 0; i < len; i++ {
               count++
        }
        mutex.Unlock()
        wg1.Done()
}
func main() {
        now := time.Now().UnixNano()
        wg1.Add(3)
        go goroutine1()
        go goroutine1()
        go goroutine1()
        wg1.Wait()
        fmt.Println(time.Now().UnixNano() - now)
        fmt.Println(count)
}

image.gif

从运行实序上来看,互斥体的方案应该和信号量差不多,不过结果却令人意,在互斥体的控制下,这个程序性能反而还下降了30%,具体结果如下:

5986800
3000000
成功: 进程退出代码 0.

image.gif

串行方式:

最后用最返璞归真的做法,串行操作代码如下:

package main
import (
        "fmt"
        //"sync"
        "time"
)
var count int
func goroutine1() {
        len := 1000000
        for i := 0; i < len; i++ {
               count++
        }
}
func main() {
        now := time.Now().UnixNano()
        goroutine1()
        goroutine1()
        goroutine1()
        fmt.Println(time.Now().UnixNano() - now)
        fmt.Println(count)
}

image.gif

可以看到从效率上来讲,直接串行的方式和信号量的方式是差不多的,结果如下:

4986700
3000000
成功: 进程退出代码 0.

image.gif

也就是说费了半天劲,最终结果可能还不如直接串行执行呢。

Rust Future初探

Rust中的future机制有点类似于 JavaScript 中的promise机制。Future机制让程序员可以使用同步代码的方式设计高并发的异步场景。目前虽然Go当中也有一些defer的机制,但远没有Rust中的future这么强大。Future机制将返回值value与其计算方式executor分离,从而让程序员可以不再关注于具体时序机制的设计,只需要指定Future执行所需要的条件,以及执行器即可。

我们来看以下代码。

注:cargo.toml

[dependencies]
futures = { version = "0.3.5", features = ["thread-pool"] }

image.gif

代码如下:

use futures::channel::mpsc;
use futures::executor;
use futures::executor::ThreadPool;
use futures::StreamExt;
fn main() {
    let poolExecutor = ThreadPool::new().expect("Failed");
    let (tx, rx) = mpsc::unbounded::<String>();
    let future_values = async {
        let fut_tx_result = async move {
       let hello = String::from("hello world");
         for c in hello .chars() {
        tx.unbounded_send(c.to_string()).expect("Failed to send");
    }
           
        };
        poolExecutor.spawn_ok(fut_tx_result);
        let future_values = rx
            .map(|v| v)
            .collect();
        future_values.await
    };
    let values: Vec<String> = executor::block_on(future_values);
    println!("Values={:?}", values);
}

image.gif

上述代码中我们通过async指定了future_values ,并将这个Future指定给poolExecutor这个线程池执行,最后通过await方法,就可以让future全部执行完毕,而不必再用信号量控制具体的时序。

这样一来,只要深度掌握future机制,就可以不必再关心互斥体、信号量,具体的高度方式完全放心交给计算机去做优化,不但可以节约程序员的时间,也能充分发挥编译器的威力,尾号是避免出现那种扔掉立交桥,只要信号灯低级的错误方式。

Java虽然也有一定的Future实现,并且有Rust不具备的反射能力,但是冷起动一直是困扰Java的痛。因此在目前云原生的时代,Go和Rust尤其是Rust语言以其近首于C语言的启动速度,和运行效率真是很有可能在未来称王。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Apache Flink 零基础入门(七):Table API 编程
本文主要包含三部分:第一部分,主要介绍什么是 Table API,从概念角度进行分析,让大家有一个感性的认识;第二部分,从代码的层面介绍怎么使用 Table API;第三部分,介绍 Table API 近期的动态。
2120 0
DL之RNN:人工智能为你写代码——基于TF利用RNN算法实现生成编程语言代码(C++语言)、训练&测试过程全记录
DL之RNN:人工智能为你写代码——基于TF利用RNN算法实现生成编程语言代码(C++语言)、训练&测试过程全记录
57 0
SAS学习笔记之《SAS编程与数据挖掘商业案例》(4)DATA步循环与控制、常用全程语句、输出控制
SAS学习笔记之《SAS编程与数据挖掘商业案例》(4)DATA步循环与控制、常用全程语句、输出控制 1. 各种循环与控制 DO组 创建一个执行语句块 DO循环 根据下标变量重复执行DO和END之间的语句 DO WHILE 重复执行直到条件为假则退出循环 DO UNTIL 重复执行直到条件为真则退出循环 DO OVER 对隐含下标
1325 0
C语言项目开发-项目架构和编程命名规范
一个项目的流程: 1、公司市场人员与客户交流,了解客户、引导客户使用公司最优资源并产出一份市场需求文档 2、公司需求人员(BA)与客户交流,了解客户需求并产出一个软件需求文档 3、项目经理、开发小组成员、需求人员(BA)一起开一个需求评审会议,对不合理的地方,    打回给BA,再由BA与客户沟通 4、程序员接到和充分了解软件需求文档后产生软件设计文档(包括概要设计文档和详细设计文档,    涉及到数据库的还需要进行数据库的设计) 5、程序员根据设计文档进行编码、调试、打包发布。
1292 0
gdb反汇编详解C函数底层实现笔记(程序堆栈、内存分配)
以下是在读《深入理解计算机系统》前面的章节“程序的机器级表示”时,自己动手在linux上使用了gdb对一个简单的C程序进行反汇编,通过不懈的努力终于查清楚弄明白了绝大多数的语句。
945 0
37
文章
0
问答
来源圈子
更多
阿里云最有价值专家,简称 MVP(Most Valuable Professional),是专注于帮助他人充分了解和使用阿里云技术的意见领袖阿里云 MVP 奖项为我们提供了这样一个机会,向杰出的意见领袖表示感谢,更希望通过 MVP 将开发者的声音反映到我们的技术路线图上。
+ 订阅
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载