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语言的启动速度,和运行效率真是很有可能在未来称王。

相关文章
|
2月前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
56 1
|
16天前
|
存储 Rust 安全
Rust 中的动态内存分配
【10月更文挑战第10天】在Rust中,动态内存分配主要用于运行时按需分配内存,与静态分配不同,它能处理大小不确定的数据结构。Rust通过`Box`类型实现堆分配,`Vec`类型则用于动态数组,两者均内置智能内存管理。`Rc`和`Arc`提供引用计数机制,支持数据共享并确保内存安全。Rust的内存安全管理机制有效避免了悬空指针和双重释放等问题。
|
24天前
|
Rust 安全 Java
软件工程师,是时候了解下Rust编程语言了
2024年年初,美国政府发布了一份网络安全报告,呼吁软件开发人员停止使用容易出现内存安全漏洞的编程语言,比如:C和C++,转而使用内存安全的编程语言。这份报告由美国网络空间总监办公室 (ONCD) 发布,旨在落实美国总统拜登的网络安全战略,目标是“保护网络空间的基石”。 内存安全指的是程序在访问内存时,能够避免出现错误和漏洞,比如:缓冲区溢出、野指针等。Java由于其运行时错误检测机制,被认为是一种内存安全的语言。然而,C和C++允许直接操作内存地址,并且缺乏边界检查,容易出现内存安全问题。
60 10
|
9天前
|
Rust 安全 网络安全
在 Rust 语言中,寻找企业上网行为管理软件的突破
在数字化企业环境中,上网行为管理软件对于保障网络安全和提升工作效率至关重要。Rust 语言凭借其安全性、高性能和并发性,为开发此类软件提供了新机遇。本文通过几个 Rust 代码示例,展示了如何实现网址检查、访问频率统计及访问控制等功能,旨在探索 Rust 在企业上网行为管理中的应用潜力。
19 0
|
1月前
|
Rust 安全 Java
探索 Rust:系统编程语言的新纪元
Rust 是一种由 Mozilla 研究院开发的开源系统编程语言,以其内存安全、高性能和现代并发工具著称,已连续多年被评为 Stack Overflow 最受喜爱的编程语言。Rust 通过所有权系统和借用检查等机制确保内存安全,并具备无垃圾回收、强大类型系统及高效并发编程特性。它广泛应用于系统级应用、WebAssembly、区块链技术和游戏开发等领域。Rust 提供丰富的学习资源和开发工具,包括官方文档、书籍、Cargo 包管理器和活跃社区支持,正逐渐成为系统编程领域的新力量。
|
1月前
|
编译器 Go
go语言学习记录(关于一些奇怪的疑问)有别于其他编程语言
本文探讨了Go语言中的常量概念,特别是特殊常量iota的使用方法及其自动递增特性。同时,文中还提到了在声明常量时,后续常量可沿用前一个值的特点,以及在遍历map时可能遇到的非顺序打印问题。
|
2月前
|
数据采集 Rust 安全
Rust在网络爬虫中的应用与实践:探索内存安全与并发处理的奥秘
【8月更文挑战第31天】网络爬虫是自动化程序,用于从互联网抓取数据。随着互联网的发展,构建高效、安全的爬虫成为热点。Rust语言凭借内存安全和高性能特点,在此领域展现出巨大潜力。本文探讨Rust如何通过所有权、借用及生命周期机制保障内存安全;利用`async/await`模型和`tokio`运行时处理并发请求;借助WebAssembly技术处理动态内容;并使用`reqwest`和`js-sys`库解析CSS和JavaScript,确保代码的安全性和可维护性。未来,Rust将在网络爬虫领域扮演更重要角色。
66 1
|
2月前
|
Rust 安全 程序员
Rust 语言的防错机制太惊人了!安全编码从此不再是难题,快来一探究竟!
【8月更文挑战第31天】《安全编码原则:Rust 语言中的防错机制》探讨了代码安全的重要性,并详细介绍了Rust语言如何通过内存安全模型、所有权与借用规则等特性,在编译阶段检测并阻止潜在错误,如缓冲区溢出和悬空指针。文章还讨论了类型安全、边界检查等其他安全特性,并提出了遵循不可变引用、避免裸指针及充分测试等实用编码原则,以进一步提升代码质量和安全性。随着Rust在软件开发中的应用日益广泛,掌握其安全编码原则变得尤为重要。
50 0
|
2月前
|
Rust 安全 调度
从零构建梦想操作系统:用Rust语言亲手打造专属内核,你也可以成为系统开发者!
【8月更文挑战第31天】开发操作系统内核虽具挑战,却也充满乐趣。本文将指导你从零开始,使用Rust语言构建一个简单的操作系统内核。首先安装Rust环境和交叉编译工具链,然后创建新项目`my_kernel`。通过修改`Cargo.toml`和编写启动函数,结合串口输出和`multiboot2`库,最终使用QEMU运行内核。此教程旨在帮助你理解Rust在系统开发中的应用,激发深入探索的兴趣。
68 1
|
2月前
|
Rust 安全 算法
揭秘Rust语言如何重塑区块链安全:打造坚不可摧的分布式账本新篇章!
【8月更文挑战第31天】自比特币诞生以来,区块链技术凭借其去中心化和不可篡改的特点备受关注。为了应对安全性挑战,Rust 语言凭借其内存安全特性逐渐成为区块链开发的优选。本文探讨了 Rust 如何助力区块链实现更安全的分布式账本。通过示例展示了 Rust 在避免内存泄漏、空指针引用及数据竞争等方面的优势,预示着 Rust 在高性能、高安全性需求的区块链应用中拥有广阔前景。
70 1