30天拿下Rust之并发

简介: 30天拿下Rust之并发

概述

随着多核处理器和分布式系统的普及,并发编程成为了现代软件开发中不可或缺的一部分。然而,并发编程也是一项极具挑战性的任务,因为它涉及到数据共享、线程同步和竞态条件等复杂问题。在这些挑战面前,Rust以其独特的内存安全性和并发原语,为开发者提供了一个安全、高效且优雅的并发编程环境。

线程

线程是Rust中最基本的并发单元。在Rust中,可以使用std::thread::spawn函数来创建一个新的线程。这个函数接收一个闭包作为参数,这个闭包会在新线程中执行。通过使用std::thread::spawn函数,开发者可以轻松地创建新的线程来执行并发任务。这些线程在操作系统级别进行调度,可以实现真正的并行执行。

use std::thread;  
use std::time::Duration;  
  
fn main() {
    // 创建一个新线程
    thread::spawn(|| {
        println!("from a thread");
        // 为了让线程运行足够长的时间,以便观察其输出,我们可以让线程休眠一段时间
        thread::sleep(Duration::from_secs(1));
    });

    // 主线程继续执行其他任务
    println!("from main thread");

    // 等待子线程完成
    thread::sleep(Duration::from_secs(2));
}

在上面的示例代码中,我们创建了一个新线程来打印一条消息,并让主线程继续执行。注意:我们没有等待新线程完成,所以主线程可能会在新线程之前或之后结束。

在实际应用中,我们可能需要使用join方法来确保线程结束。join方法是线程句柄的一个方法,用于阻塞当前线程(调用join的线程),直到被join的线程完成执行。换句话说,join方法会等待另一个线程结束。一旦被等待的线程结束,join方法就会立即返回。

use std::thread;  
use std::time::Duration;  
  
fn main() {
    // 创建一个新线程
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("from a thread: {}", i);
            thread::sleep(Duration::from_secs(1));
        }
    });

    // 主线程继续执行其他任务
    println!("from main thread");

    // 使用unwrap简化代码,等待子线程完成
    handle.join().unwrap();
}

在上面的示例代码中,我们创建了一个新线程,每隔1秒钟打印一条消息,一共打印4次。在主线程中,我们打印了一条消息,然后使用join方法等待子线程结束。执行本程序后,输出大致如下。

from main thread
from a thread: 1
from a thread: 2
from a thread: 3
from a thread: 4

我们还可以通过闭包(匿名函数)向线程中传入参数。当使用std::thread::spawn创建一个新线程时,我们可以传递一个闭包作为参数,这个闭包可以捕获外部作用域中的变量,并将它们作为参数传递给新线程中执行的代码。

use std::thread;
use std::time::Duration; 
  
fn main() {
    let text: &str = "Hello World";
    let thread_id = thread::current().id();
    let handle = thread::spawn(move || {
        // 在这里,message  thread_id 是从外部作用域捕获的变量
        println!("{}", text);
        println!("{:?}", thread_id);

        // 线程也可以有自己的局部变量
        let text_local = "Hello Github";
        println!("{}", text_local);

        thread::sleep(Duration::from_secs(1));
    });  

    // 主线程继续执行其他任务
    println!("from main thread");

    // 等待子线程完成
    handle.join().unwrap();
}

在上面的示例代码中,我们定义了一个字符串text和一个线程 ID thread_id,并将它们作为闭包的捕获变。move关键字用于确保这些变量被移动到闭包中,这样它们就可以在新线程中使用了。注意:如果没有move,这些变量可能会被借用,而Rust的借用规则不允许在多个线程中同时借用同一个变量。闭包中的代码会在新线程中执行,并且可以访问从外部作用域捕获的变量text和thread_id。同时,新线程也可以有自己的局部变量,比如:text_local。

通过这种方式,我们可以向线程中传入任意数量的参数,只要它们能够被安全地移动到闭包中即可。这包括:基本数据类型、复杂的数据结构,甚至是其他线程句柄或同步原语。注意:当向线程中传入引用类型的参数时(比如:在堆上分配的数据的引用),我们需要确保这些引用在线程执行期间仍然有效,否则可能会出现悬挂引用或数据竞争的问题。在大多数情况下,使用值的移动而不是引用是更安全的选择。

通道

在Rust中,通道是用于在不同线程之间进行通信的一种机制。它们由两个端点组成:一个发送端和一个接收端。发送端用于向通道发送消息,而接收端用于从通道接收消息。这种通信方式在并发编程中非常有用,允许线程之间安全地传递数据。

Rust标准库提供了两种主要类型的通道:std::sync::mpsc和crossbeam_channel。

std::sync::mpsc提供的是多生产者单消费者(Multiple Producer Single Consumer)通道,这意味着多个发送者可以向一个接收者发送数据。这种通道在std::sync模块中定义,适用于传统的同步线程间通信场景。

use std::thread;
use std::sync::mpsc;

fn main() {
    // 创建一个新的通道
    let (tx, rx) = mpsc::channel();

    // 在新线程中发送数据
    thread::spawn(move || {
        tx.send(66).unwrap();
    });

    let received_value = rx.recv().unwrap();
    // 主线程接收数据,输出:66
    println!("{}", received_value);
}

在上面的示例代码中,我们首先通过调用mpsc::channel()方法创建了一个通道。这个通道返回一个发送端tx和一个接收端rx。发送端用于发送数据,而接收端用于接收数据。接着,我们使用thread::spawn来创建一个新的线程。这个新线程会执行传递给它的闭包。在闭包内部,我们调用tx.send(66)来发送一个整数66到通道。unwrap()用于处理可能的错误,但在实际代码中,应该更优雅地处理错误。最后,在主线程中,我们调用rx.recv()来从通道接收数据。同样,我们使用unwrap()来处理可能的错误。

注意:Rust中通道的recv方法是阻塞的。当调用rx.recv()时,如果通道中没有可用的数据,接收者线程将会阻塞,直到有数据可用为止。这种阻塞行为确保了数据按照发送的顺序被接收,并且只有在数据实际可用时,接收者才会继续执行。为了避免阻塞,Rust还提供了其他方法,比如:try_recv、recv_timeout。try_recv方法尝试立即返回一个挂起的值,而不会阻塞调用线程。如果没有可用的数据,它将返回一个错误。recv_timeout方法则尝试在指定的超时时间内等待一个值。如果超时时间内没有接收到数据,它将返回一个错误。这些方法提供了更多的灵活性,可以根据具体的需求选择使用。

crossbeam_channel是Rust中一个流行的并发通道库,它提供了高效、无锁的通道实现,用于在并发任务之间传递消息。关于这个库的具体使用,我们会在后续其他专栏中专门介绍,这里就不再赘述了。

互斥锁

在Rust中,互斥锁是用于同步访问共享资源的机制,确保在任意时刻只有一个线程可以访问特定的数据。Rust标准库中的互斥锁可以通过std::sync::Mutex<T>类型来实现,其中T是被保护的数据类型。当一个线程获取到互斥锁时,其他尝试获取该锁的线程会被阻塞,直到持有锁的线程释放它。

use std::sync::{Mutex, Arc};
use std::thread;
use std::time::Duration;

fn main() { 
    let counter = Arc::new(Mutex::new(0));
    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
    }

    thread::sleep(Duration::from_secs(1));
    let final_count = (*counter.lock().unwrap()) as usize;
    println!("{}", final_count);
}

在上面的示例代码中,Arc(原子引用计数)用于跨线程安全地共享Mutex包装的计数器实例,而lock()方法用于获取互斥锁并返回一个MutexGuard,它是对内部数据的一个可变引用。当MutexGuard超出作用域时,互斥锁会自动释放,允许其他线程获取锁并访问共享资源。unwrap()用于在获取锁失败时引发panic,但在实际应用中通常会使用更稳健的错误处理方式。

读写锁

读写锁是一种更为精细的同步原语,它允许多个读取者同时访问共享资源,但同一时间内只允许一个写入者访问,以此来提高并发性能。相比于互斥锁,读写锁在读取操作密集且写入操作较少的情况下能提供更好的并发性能。

在Rust标准库中,读写锁由std::sync::RwLock<T>类型实现,其中T是被保护的数据类型。

use std::sync::{RwLock, Arc};
use std::thread;

fn main() {
    // 创建一个被读写锁保护的整数
    let shared_data = Arc::new(RwLock::new(66));

    // 创建读取线程
    let reader_threads = (0..5).map(|_| {
        let shared_data_clone = Arc::clone(&shared_data);
        thread::spawn(move || {
            // 获取读锁
            let data = shared_data_clone.read().unwrap();
            println!("read: {}", *data);
            // 读取完成后,读锁会自动释放
        })
    });

    // 创建写入线程
    let writer_thread = {
        let shared_data_write = Arc::clone(&shared_data);
        thread::spawn(move || {
            // 获取写锁
            let mut data = shared_data_write.write().unwrap();
            *data += 1;
            println!("write data: {}", *data);
            // 写入完成后,写锁会自动释放
        })
    };

    // 等待所有读取线程完成
    for reader in reader_threads {
        reader.join().unwrap();
    }

    // 等待写入线程完成
    writer_thread.join().unwrap();

    // 读取已更新的数据
    let final_data = shared_data.read().unwrap();
    println!("{}", *final_data);
}

在上面的示例代码中,我们首先创建了一个被Arc<RwLock<i32>>保护的整数,初始值为66。Arc使得数据能够在多个线程之间安全地共享,而RwLock用于控制对这个整数的并发访问。

接着,我们使用Arc::clone()创建shared_data的克隆引用,这样每个读取线程都能拥有独立的引用,并且它们指向同一个受保护的数据。然后,使用thread::spawn()创建5个读取线程,每个线程内部获取读锁,这会阻塞线程直到获得读锁。当read方法返回的RwLockReadGuard超出作用域时,读锁会自动释放。

同样的,我们使用Arc::clone()创建写入线程所需的shared_data_write引用。然后,使用thread::spawn创建一个写入线程,线程内部获取写锁,这会阻塞线程直到获得写锁。递增被锁定的整数后,我们打印更新后的数据。当write方法返回的RwLockWriteGuard超出作用域时,写锁会自动释放。

最后,我们等待所有读取线程和写入线程完成,并从共享数据中读取已更新的整数值进行了打印输出。

总结

Rust以其强大的内存安全性和丰富的并发原语,为开发者提供了一个安全、高效且优雅的并发编程环境。通过合理利用Rust的并发特性,开发者可以编写出高性能、高可靠性的并发应用程序,满足现代软件开发的需求。然而,并发编程仍然是一项具有挑战性的任务,需要开发者具备深厚的编程经验和良好的设计思维。


相关文章
|
18天前
|
Rust 安全 云计算
Rust语言入门:安全性与并发性的完美结合
【10月更文挑战第25天】Rust 是一种系统级编程语言,以其独特的安全性和并发性保障而著称。它提供了与 C 和 C++ 相当的性能,同时确保内存安全,避免了常见的安全问题。Rust 的所有权系统通过编译时检查保证内存安全,其零成本抽象设计使得抽象不会带来额外的性能开销。Rust 还提供了强大的并发编程工具,如线程、消息传递和原子操作,确保了数据竞争的编译时检测。这些特性使 Rust 成为编写高效、安全并发代码的理想选择。
15 0
|
3月前
|
数据采集 Rust 安全
Rust在网络爬虫中的应用与实践:探索内存安全与并发处理的奥秘
【8月更文挑战第31天】网络爬虫是自动化程序,用于从互联网抓取数据。随着互联网的发展,构建高效、安全的爬虫成为热点。Rust语言凭借内存安全和高性能特点,在此领域展现出巨大潜力。本文探讨Rust如何通过所有权、借用及生命周期机制保障内存安全;利用`async/await`模型和`tokio`运行时处理并发请求;借助WebAssembly技术处理动态内容;并使用`reqwest`和`js-sys`库解析CSS和JavaScript,确保代码的安全性和可维护性。未来,Rust将在网络爬虫领域扮演更重要角色。
77 1
|
3月前
|
Rust 并行计算 安全
揭秘Rust并发奇技!线程与消息传递背后的秘密,让程序性能飙升的终极奥义!
【8月更文挑战第31天】Rust 以其安全性和高性能著称,其并发模型在现代软件开发中至关重要。通过 `std::thread` 模块,Rust 支持高效的线程管理和数据共享,同时确保内存和线程安全。本文探讨 Rust 的线程与消息传递机制,并通过示例代码展示其应用。例如,使用 `Mutex` 实现线程同步,通过通道(channel)实现线程间安全通信。Rust 的并发模型结合了线程和消息传递的优势,确保了高效且安全的并行执行,适用于高性能和高并发场景。
59 0
|
3月前
|
安全 开发者 数据安全/隐私保护
Xamarin 的安全性考虑与最佳实践:从数据加密到网络防护,全面解析构建安全移动应用的六大核心技术要点与实战代码示例
【8月更文挑战第31天】Xamarin 的安全性考虑与最佳实践对于构建安全可靠的跨平台移动应用至关重要。本文探讨了 Xamarin 开发中的关键安全因素,如数据加密、网络通信安全、权限管理等,并提供了 AES 加密算法的代码示例。
59 0
|
3月前
|
开发框架 Android开发 iOS开发
跨平台开发的双重奏:Xamarin在不同规模项目中的实战表现与成功故事解析
【8月更文挑战第31天】在移动应用开发领域,选择合适的开发框架至关重要。Xamarin作为一款基于.NET的跨平台解决方案,凭借其独特的代码共享和快速迭代能力,赢得了广泛青睐。本文通过两个案例对比展示Xamarin的优势:一是初创公司利用Xamarin.Forms快速开发出适用于Android和iOS的应用;二是大型企业借助Xamarin实现高性能的原生应用体验及稳定的后端支持。无论是资源有限的小型企业还是需求复杂的大公司,Xamarin均能提供高效灵活的解决方案,彰显其在跨平台开发领域的强大实力。
42 0
|
3月前
|
Rust 安全 数据处理
【揭秘异步编程】Rust带你走进并发设计的神秘世界——高效、安全的并发原来是这样实现的!
【8月更文挑战第31天】《异步编程的艺术:使用Rust进行并发设计》一文探讨了如何利用Rust的`async`/`await`机制实现高效并发。Rust凭借内存安全和高性能优势,成为构建现代系统的理想选择。文章通过具体代码示例介绍了异步函数基础、并发任务执行及异步I/O操作,展示了Rust在提升程序吞吐量和可维护性方面的强大能力。通过学习这些技术,开发者可以更好地利用Rust的并发特性,构建高性能、低延迟的应用程序。
47 0
|
6月前
|
Rust 并行计算 安全
Rust中的并行与并发优化:释放多核性能
Rust语言以其内存安全和高效的并发模型在并行计算领域脱颖而出。本文深入探讨了Rust中的并行与并发优化技术,包括使用多线程、异步编程、以及并行算法等。通过理解并应用这些技术,Rust开发者可以有效地利用多核处理器,提高程序的性能和响应能力。
|
11月前
|
Rust 安全 API
Rust学习笔记之并发(二)
Rust学习笔记之并发(二)
|
11月前
|
Rust JavaScript 前端开发
Rust学习笔记之并发(一)
Rust学习笔记之并发(一)
|
8天前
|
Rust 安全 Java
探索Rust语言的并发编程模型
探索Rust语言的并发编程模型