点击上方蓝字 江湖评谈设为关注/星标
前言
自从Rust出道以来,备受关注。美国DARPA(国防部高级研究计划局)推动了一项计划,把不安全的C/C++代码转换成安全的Rust。因为嫌弃人工转换太慢,这个过程后续会在AI的加持下,自动化的加速大规模转换,抛弃旧有体系下陈旧的C/C++技术。美国作为当今计算机互联网的基石国家,这种转换是否意味着C/C++的没落呢。
新旧交替,实是世界变化常理。唯一的不变,就是永远不断地变化的事物。
多线程
多线程的安全一直困扰着C++,对于Rust来说这自然是小菜一碟,不过现代化的C++也在增强这方面的处理过程。
C++:
int counter = 0;
void unsafe_increment() {
for (int i = 0; i < 100; ++i) {
++counter; // 未原子化优先级,数据竞争
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(unsafe_increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl; // 结果不确定
return 0;
}
以上代码是一个典型的多线程共享变量忘记了加锁,或者没有对多线程的共享变量进行原子化操作。emplace_back每次创建一个新线程,执行unsafe_increment函数,后者则在里面自增循环了counter变量100次。
以上代码的运行情况导致了counter自增结果的不可预测性,很容易出问题。正确代码的应该怎么做呢?针对safe_increment函数,可有如下改进。
添加一个全局锁,当counter循环自增的时候,添加锁保证线程访问的同步。这里用的是lock_guard 是RAII(资源获取即初始化)风格的代码。
std::mutex mtx;
void safe_increment() {
for (int i = 0; i < 100; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
++counter;
}
}
2.声明counter为原子化的类型,保证每次的递增安全
std::atomic<int> counter{0};
void atomic_increment() {
for (int i = 0; i < 100; ++i) {
++counter; // 原子操作
}
}
Rust
那么针对以上C++代码,Rust怎么处理这个问题呢?非常简单
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..100 {
let mut num = counter.lock().unwrap();
*num += 1;
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final counter value: {}", *counter.lock().unwrap());
}
Rust声明了一个线程安全的引用计数智能指针Arc,一个互斥锁Mutex,一个Arc指针克隆,一个lock锁的访问,再加上编译期间的错误检查能力。完全锁死了counter的自增结果,确保其万无一失的线程访问同步自增。既不会遗忘,也不会有出错的概率。
悬空指针和双重释放
悬空指针和双重释放是C++困扰多年的问题,Windows的高危漏洞暴露在MSF(msfconsole)上有近百个是这两个问题造出的。
C++悬空指针和双重释放:
void dangling_pointer() {
int* ptr = new int(42); // 动态分配内存
delete ptr; // 释放内存
std::cout << *ptr << std::endl; // 未定义行为,访问悬空指针
}
void double_free() {
int* ptr = new int(42);
delete ptr; // 第一次释放
delete ptr; // 第二次释放,未定义行为
}
int main() {
dangling_pointer();
double_free();
return 0;
}
Rust:
fn main() {
let x = Box::new(10); // Box 智能指针,负责堆内存分配
// 内存自动管理,无需手动释放
println!("{}", x);
// 离开作用域,内存自动释放,无法引发悬空指针或双重释放
}
Rust/C++优缺点对比
Rust比C++至少有以下优点
Rust比C++拥有更现代化的工具链,诸如:Cargo,Clippy,Rustfmt极大的提高了代码开发效率,质量和节省时间。而C++工具分散,Win/Linux/MacOS个一套自己的开发工具,号称跨平台的Cmake可阅读性非常的之差,兼容性也是一言难尽,难以忍受。
Rust内置标准包的管理和强大的社区生态,Rust 自带的Cargo和Crates.io生态系统,让开发者能够方便地引入高质量的开源库。C++则缺乏官方包管理工具,依赖管理通常由第三方工具(如Conan或vcpkg)解决,各个平台,各个厂商,各个系统五花八门,眼花缭乱。开发者的大部分精力都放在处理这些乱七八糟的东西上面了。
Rust有更好的错误处理机制,Rust 提供了类型化的错误处理(ResultOption),让错误变得显式且易于处理。编译器强制处理潜在的错误。明确错误的传播(通过?操作符)。C++错误处理通常依赖异常或返回码,容易被忽视,且难以阅读和查找理解。
结尾
C++垂垂老矣,近年来频繁添加的各种特性越来越向带GC的语言和Rust擅长的编译期间的检查特性靠拢了。比如constexpr提供编译期计算能力,优化性能,类似于Rust。比如std::shared_ptr:提供引用计数,与 Java和C#的对象引用类似,在最后一个引用离开作用域时自动释放内存。
但个人依旧认为离C++退场还有一段距离,不过这个距离应该不是太远了。
往期精彩回顾