专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

从单线程小菜鸟到可以轻松地应对Rust并发编程,让CPU为我鼓掌!

  在现代计算机中,多核处理器已经非常常见。这也就意味着,我们需要编写能够利用多核处理器的并发程序,以充分利用计算机的资源。Rust 提供了一些机制来实现并发编程,其中最重要的是多线程。
  Rust 的多线程机制与其他语言的多线程机制不同,它采用了一种称为"所有权模型"的内存管理模型,以保证并发程序的安全性和正确性。这种模型使得多线程编程更容易和更安全,而不会出现内存泄漏和数据竞争等问题。
  接下来我们将继续深入学习 Rust 并发编程的基础知识和高级主题,以及 Rust 并发编程的实践经验和最佳实践。Rust 的并发编程是其最重要的特性之一,因为它支持多线程和异步编程,能够提高程序的性能和响应能力。在这个学习笔记中,我们将介绍 Rust 的并发模型,线程和共享状态的基础知识,以及 Rust 的锁、互斥体、原子类型、通道和消息传递等机制。我们还将探讨 Rust 中的异步编程和 Tokio 框架、高级同步构造和性能优化等主题,同时介绍 Rust 并发编程的实践案例和项目实战,以及 Rust 并发编程的最佳实践。本学习笔记将帮助您掌握 Rust 并发编程的核心概念和实践经验,使您能够在实际开发中应用 Rust 的并发编程技术来构建高效和可靠的软件系统。
  Rust 并发编程基础
  1.Rust 并发模型
  Rust 并发模型是一种基于线程和消息传递的并发编程模型,它允许多个线程同时执行任务,并通过消息传递进行通信和同步。
  在 Rust 并发模型中,每个线程都是独立的执行单元,线程之间没有共享的状态,这避免了线程间的竞争条件和数据竞争问题。相反,线程之间通过消息传递进行通信,每个线程都可以独立地处理消息。
  Rust 的并发模型可以用于多种场景,例如网络编程、图形界面编程、并行计算和多核处理等。
  举个例子,假设我们有一个 web 服务器,它需要同时处理多个客户端请求。在传统的并发编程模型中,我们可能会使用多线程或进程来实现,但是这会带来线程竞争和数据竞争等问题。而在 Rust 的并发模型中,我们可以创建多个独立的线程,每个线程负责处理一个客户端请求,线程之间通过消息传递进行通信和同步,从而避免了线程竞争和数据竞争问题。这种并发模型可以保证服务器的性能和稳定性,同时简化了编程模型。
  2.线程基础
  在 Rust 中,线程是基本的并发构建块之一,它允许我们同时执行多个任务。Rust 提供了创建和管理线程的标准库,使得在多线程环境下编写代码变得相对简单。
  在 Rust 中,线程可以通过 std::thread 模块来创建。以下是一个创建线程并运行简单任务的示例代码:use std::thread;  fn main() {     let handle = thread::spawn(|| {         println!("Hello from a thread!");     });      handle.join().unwrap(); }
  在这个例子中,我们使用 thread::spawn 函数创建一个新的线程,并将其与一个闭包绑定在一起。该闭包实现了线程要执行的代码逻辑。在 thread::spawn 调用后,我们会得到一个 JoinHandle 类型的返回值,它可以用于等待线程完成并获取其返回值。在这里,我们使用 handle.join() 来等待线程完成,以确保打印语句完成执行。
  在 Rust 中,线程的创建和管理有许多细节需要注意,例如线程的安全性、线程间的通信、错误处理等。我们需要仔细地设计和编写代码,以避免潜在的问题。
  3.共享状态与可变性
  在 Rust 并发编程中,共享状态和可变性是非常重要的概念,因为它们涉及到多个线程之间的数据交互问题。
  在单线程环境下,我们可以轻松地实现对变量的读取和写入,但在多线程环境下,由于多个线程可以同时访问和修改同一个变量,因此可能会导致数据竞争(Data Race)的问题。
  为了解决这个问题,Rust 提供了一些机制来保证共享状态的安全性,其中最基本的机制是通过在编译时检查来防止数据竞争。
  具体来说,Rust 的共享状态和可变性控制机制主要包括以下几个方面:所有权和借用机制:Rust 通过所有权和借用机制来确保在任何时刻,只有一个线程拥有对一个特定变量的所有权,并且只有在这个所有权被释放之后,其他线程才能访问这个变量。Sync 和 Send 特性:Rust 为多线程编程提供了一些特殊的 trait,如 Sync 和 Send。其中,Sync 表示类型是线程安全的,Send 表示类型可以在线程之间传递。内存模型:Rust 的内存模型采用了严格的线程安全规则,通过限制线程访问共享内存的方式来避免数据竞争问题。同时,Rust 还提供了一些原子类型和操作,如原子引用计数和原子计数器,以帮助开发者实现高效的并发编程。
  在实际编程中,开发者需要根据具体的需求来选择适合的共享状态和可变性控制方式。例如,对于一些简单的共享状态,可以使用 Rust 的原子类型和操作来保证线程安全性;对于更复杂的数据结构,可能需要使用锁或通道等高级同步构造来控制访问和修改权限。
  4.锁和互斥体
  在Rust中,锁和互斥体是处理共享状态的主要工具之一。锁是一种同步原语,它可以防止同时访问共享资源。当一个线程想要访问共享资源时,它必须先获得锁。如果锁已经被另一个线程持有,那么线程就会被阻塞,直到锁被释放。
  Rust中的互斥体是一种特殊的锁,它提供了对共享资源的独占访问。当一个线程获取了互斥体的所有权后,其他线程就不能再访问共享资源了,直到互斥体被释放。
  以下是一个使用互斥体保护共享计数器的例子:use std::sync::Mutex;  fn main() {     let counter = Mutex::new(0);      let mut handles = vec![];      for _ in 0..10 {         let handle = std::thread::spawn({             let counter = counter.clone();             move || {                 let mut num = counter.lock().unwrap();                  *num += 1;             }         });          handles.push(handle);     }      for handle in handles {         handle.join().unwrap();     }      println!("Result: {}", *counter.lock().unwrap()); }
  在这个例子中,我们使用了一个互斥体来保护计数器。在每个线程中,我们先克隆了互斥体的所有权,并调用lock()方法来获取计数器的可变引用。由于互斥体提供了独占访问,所以只有一个线程可以持有计数器的可变引用。其他线程必须等待锁被释放后才能访问计数器。在线程结束后,我们使用join()方法等待所有线程执行完毕,并打印出计数器的最终结果。
  需要注意的是,在使用互斥体时,一定要小心死锁的情况。死锁指的是多个线程互相等待对方释放锁,导致所有线程都被阻塞的情况。在使用互斥体时,我们应该尽量避免嵌套锁,以免死锁的发生。
  5.Atomics 和 Memory Ordering
  在Rust中,原子操作(atomic operations)可以用来实现并发访问共享数据的同步和互斥。原子操作指的是无法被中断的操作,即不会被其他线程干扰,保证了操作的原子性。Rust提供了一系列的原子类型,如AtomicBool、AtomicUsize等,这些类型都实现了一些原子方法,如fetch_add、fetch_sub等。这些方法可以保证操作的原子性,同时也保证了线程安全。
  Memory Ordering则是指Rust在进行原子操作时,保证操作顺序和可见性的机制。在Rust中,原子操作默认使用SeqCst(Sequential Consistent)的内存顺序,即顺序一致性。顺序一致性是指多线程执行时,所有线程看到的操作顺序都是一致的,这种机制可以保证操作的正确性,但可能会影响性能。
  此外,Rust也提供了其他的内存顺序,如Acquire和Release,可以在保证操作正确性的前提下提高性能。
  举例来说,当一个线程对一个原子计数器进行fetch_add操作时,Rust会保证该操作的原子性,并根据所选的内存顺序来保证该操作与其他操作的执行顺序。例如:use std::sync::atomic::{AtomicUsize, Ordering};  let counter = AtomicUsize::new(0);  // 线程1 counter.fetch_add(1, Ordering::SeqCst);  // 线程2 counter.fetch_add(1, Ordering::SeqCst);
  这里创建了一个AtomicUsize类型的计数器counter,并在两个线程中分别对它进行fetch_add操作。Rust会保证这些操作的原子性,并根据SeqCst内存顺序来保证操作的正确性。
  6.通道(channel)和消息传递
  在 Rust 中,通道(channel)是一种实现并发消息传递的机制,它允许多个线程通过发送和接收消息来进行通信和同步。通道分为两种类型:单向通道和双向通道。单向通道只能用于一种方向的通信,而双向通道则可以在两个方向上发送和接收消息。
  在 Rust 中,可以使用标准库中的mpsc模块来实现通道。mpsc模块中的channel函数可以创建一个新的通道,它返回一个发送器(sender)和一个接收器(receiver),这两个对象可以在不同的线程之间传递。通常情况下,通道的发送端和接收端是在不同的线程中创建的,它们通过通道来传递消息和同步。
  以下是一个简单的示例代码,展示了如何在 Rust 中使用通道进行消息传递和同步:use std::sync::mpsc; use std::thread;  fn main() {     let (tx, rx) = mpsc::channel();      let handle = thread::spawn(move || {         let data = rx.recv().unwrap();         println!("Received data: {}", data);     });      let data = "hello world".to_string();     tx.send(data).unwrap();      handle.join().unwrap(); }
  在这个示例中,我们首先调用了mpsc::channel()函数创建了一个通道,它返回一个发送器tx和一个接收器rx。然后,我们在新线程中使用rx.recv()方法来接收发送端发送的消息,并将消息打印出来。接着,我们在主线程中使用tx.send()方法向发送端发送了一条消息。最后,我们调用了handle.join()方法等待新线程执行完毕。
  通过通道,我们可以实现线程之间的协调和同步,从而避免了数据竞争和死锁等问题,提高了程序的可靠性和性能。Rust 并发编程高级主题
  1.生命周期和并发
  在 Rust 并发编程中,理解生命周期是非常重要的。生命周期可以用来描述变量引用的有效范围,对于并发程序来说,生命周期可以用来保证线程安全和避免数据竞争。
  Rust 的 borrow checker 确保在编译时就能检测出数据竞争的情况,因此可以帮助开发人员避免许多并发编程中的错误。在并发编程中,生命周期的正确使用可以保证多个线程之间共享数据时不会出现竞争和数据不一致的情况。
  同时,Rust 也提供了一些机制来处理并发编程中的生命周期问题,例如 Arc 和 Rc 用于共享拥有所有权的值,以及 MutexGuard 和 RefCell 用于在运行时跟踪借用规则。
  需要注意的是,在并发编程中使用生命周期时需要非常小心,否则可能会引入新的问题。因此,在学习并发编程的过程中,需要仔细研究 Rust 的生命周期规则,并在实践中多加练习。
  一个例子是,假设有一个 Vec 向量,并发地添加元素。在这种情况下,需要使用 Arc 和 Mutex 来确保线程安全。示例代码如下:use std::sync::{Arc, Mutex}; use std::thread;  fn main() {     let vec = Arc::new(Mutex::new(vec![]));      let mut threads = vec![];     for i in 0..10 {         let vec = vec.clone();         let thread = thread::spawn(move || {             vec.lock().unwrap().push(i);         });         threads.push(thread);     }      for thread in threads {         thread.join().unwrap();     }      println!("{:?}", vec.lock().unwrap()); }
  在这个例子中,Arc 用于引用计数和共享所有权,Mutex 用于确保在访问 vec 时只有一个线程能够访问。通过在并发代码中使用生命周期和 Rust 提供的线程安全机制,可以安全地并发地修改向量。
  2.异步编程和 Futures
  异步编程是一种基于事件驱动的编程模型,它通过避免阻塞线程来提高程序的性能和并发性。在 Rust 中,异步编程需要使用 Futures 和 async/await 语法。
  Future 是一个表示某个计算的结果的占位符,它可以让程序在等待某个 I/O 操作完成时不被阻塞。在 Rust 中,Future 是一个 trait,它定义了一系列方法,比如 poll 和 then,这些方法允许异步任务在后台进行处理。
  async/await 语法是 Rust 中用于异步编程的主要语法。它可以让开发者以同步的方式编写异步代码。通过 async/await,开发者可以将异步操作转换为类似于同步代码的样子,这使得异步代码更加易读易写。
  除了 Futures 和 async/await 语法之外,Rust 还提供了一些异步编程相关的库和工具,比如 Tokio 和 async-std。这些库可以帮助开发者更方便地进行异步编程,并提供了许多基于 Future 的异步 API 和工具。
  3.异步编程和 Tokio 框架
  以下是一个简单的 Tokio demo,用于说明 Tokio 的异步编程特性。
  首先,在 Cargo.toml 文件中添加 tokio 的依赖:[dependencies] tokio = { version = "1.15.0", features = ["full"] }
  接下来,创建一个异步函数 async_fetch_url,用于使用 Tokio 的异步 I/O 功能获取 URL 的响应内容:use std::error::Error;  #[tokio::main] async fn main() -> Result<(), Box> {     let url = "https://www.example.com";     let response = async_fetch_url(url).await?;     println!("{}", response);     Ok(()) }  async fn async_fetch_url(url: &str) -> Result> {     let response = reqwest::get(url).await?.text().await?;     Ok(response) }
  在这个例子中,我们使用了 tokio::main 宏,它会为我们创建一个异步运行时并将 main 函数转换为一个异步函数。
  在 async_fetch_url 函数中,我们使用了 reqwest 库获取 URL 的响应内容,并通过 async 和 await 关键字创建了异步执行的上下文。
  最后,在 main 函数中,我们调用了 async_fetch_url 函数并打印了获取到的响应内容。
  这个简单的例子展示了 Tokio 的异步编程特性,它可以让我们在 Rust 中使用高效的异步 I/O 操作,避免了阻塞和等待。
  4.并发编程中的性能优化
  在 Rust 并发编程中,性能优化非常重要,特别是对于高性能的应用程序。下面是一些常用的性能优化技巧:减少锁的使用:锁是在多线程程序中非常重要的同步工具,但是使用过多锁会降低程序的性能。因此,可以考虑减少锁的使用,例如使用无锁的数据结构,或者使用锁的粒度更细的数据结构。减少线程的创建和销毁:线程的创建和销毁需要消耗很多的系统资源,因此可以考虑使用线程池,重用已有的线程,避免不必要的创建和销毁。使用消息传递而非共享内存:消息传递是一种更轻量级的同步方式,比共享内存更容易实现和维护。在 Rust 中,可以使用通道(channel)来进行消息传递。避免不必要的内存分配和复制:在并发编程中,内存分配和复制是非常耗费性能的操作。因此可以考虑使用对象池、缓存等技术来减少不必要的内存分配和复制。避免过度同步:过度同步会降低程序的并发度,导致性能瓶颈。因此可以考虑采用更细粒度的同步方式,例如锁粒度更小的锁,或者采用更轻量级的同步方式,例如原子操作、读写锁等。
  具体的优化方式需要根据具体的应用场景来确定。在编写并发程序时,需要不断地测试和优化,才能获得更好的性能和并发度。
  5.错误处理和并发编程
  在 Rust 并发编程中,错误处理是非常重要的一部分,特别是在多线程、异步编程等场景中,错误处理往往会更加复杂和困难。
  Rust 提供了一些处理错误的机制,比如 Result 和 Option 等枚举类型,还有 panic! 宏用于在遇到不可恢复的错误时抛出异常。
  在并发编程中,需要考虑的错误类型也比较多,例如竞争条件(race condition)、死锁(deadlock)、饥饿(starvation)等。
  举个例子,假设我们有一个计数器(Counter)的结构体,多个线程会对其进行增加操作(increment),但是如果没有处理好竞争条件,就可能导致计数器的值出现错误的情况。use std::sync::atomic::{AtomicUsize, Ordering};  struct Counter {     value: AtomicUsize, }  impl Counter {     fn increment(&self) {         self.value.fetch_add(1, Ordering::SeqCst);     } }  fn main() {     let counter = Counter { value: AtomicUsize::new(0) };     let mut handles = vec![];      for _ in 0..10 {         let handle = std::thread::spawn(|| {             for _ in 0..10000 {                 counter.increment();             }         });         handles.push(handle);     }      for handle in handles {         handle.join().unwrap();     }      println!("Counter value: {}", counter.value.load(Ordering::SeqCst)); }
  在这个例子中,我们定义了一个 Counter 结构体,其中包含了一个 AtomicUsize 类型的 value 字段,用于存储计数器的值,并在其上实现了一个 increment 方法用于增加计数器的值。同时,我们使用了多线程的方式对该计数器进行了并发地增加操作,最终输出计数器的值。
  需要注意的是,由于 value 字段是一个原子类型,在并发场景下,我们需要使用正确的内存模型来确保正确性。在这个例子中,我们使用了 Ordering::SeqCst 内存模型,它是最保守和最常用的内存模型。
  如果我们不使用原子类型,而是使用普通的整型变量,那么在多线程并发访问时,就很容易出现竞争条件的情况。这种情况下,程序运行的结果是不确定的,并且会出现错误的计数器值。而使用原子类型,就可以保证在并发场景下仍然能够正确地增加计数器的值。
  6.原子引用计数和并发计数器
  原子引用计数是 Rust 中的一种线程安全的引用计数(Reference Counting)方式。与传统的引用计数不同,原子引用计数在并发场景下可以保证安全性,避免了数据竞争。在 Rust 标准库中,可以通过 std::sync::Arc 来使用原子引用计数。
  下面是一个简单的示例代码,演示了如何使用原子引用计数。在示例代码中,我们使用一个 Arc 对象 data 来同时让多个线程访问同一个 Vec 对象。use std::sync::Arc; use std::thread;  fn main() {     let data = Arc::new(vec![1, 2, 3, 4, 5]);      for i in 0..5 {         let data_ref = data.clone();         thread::spawn(move || {             let sum: i32 = data_ref.iter().sum();             println!("Thread {} sum: {}", i, sum);         });     } }
  在上面的示例中,我们使用了 Arc::new 来创建了一个包含整数序列的 Vec 对象,并使用 Arc::clone 来创建了多个 Arc 对象 data_ref,让多个线程共享同一个 Vec 对象。在每个线程中,我们使用 data_ref.iter().sum() 来计算 Vec 中所有元素的和,并输出结果。通过运行程序,我们可以看到每个线程都成功地计算出了元素的和,并输出了正确的结果。
  原子计数器(Atomic Counter)则是一种常用的原子类型,它可以在并发环境下进行安全的计数操作。在 Rust 标准库中,可以使用 std::sync::atomic::AtomicUsize 来创建一个原子计数器。
  下面是一个简单的示例代码,演示了如何使用原子计数器。在示例代码中,我们使用 AtomicUsize 来实现了一个计数器 counter,并让多个线程同时对其进行递增操作。use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread;  fn main() {     let counter = AtomicUsize::new(0);      for i in 0..5 {         let counter_ref = counter.clone();         thread::spawn(move || {             for j in 0..1000 {                 counter_ref.fetch_add(1, Ordering::Relaxed);             }             println!("Thread {} finished", i);         });     }      // 等待所有线程执行完毕     thread::sleep_ms(1000);      // 输出计数器的值     println!("Counter: {}", counter.load(Ordering::Relaxed)); }
  在上面的示例中,我们使用了 AtomicUsize::new 来创建了一个原子计数器,并使用 fetch_add 来对计数器进行递增操作。在每个线程中,我们使用了 for 循环来进行递增操作,最终输出线程执行完毕的提示。
  7.高级同步构造,如Barrier、Condvar和Semaphore
  在 Rust 并发编程中,有一些高级的同步构造可以用来更加灵活地控制线程之间的通信和同步。下面我们简单介绍一下其中三种:Barrier、Condvar 和 Semaphore。
  7.1. Barrier
  Barrier 是一个同步原语,它可以让多个线程在一个点上等待,直到所有线程都到达该点才能继续执行。Barrier 主要有两个方法:wait 和 wait_timeout。
  wait 方法会使当前线程等待其他线程到达 Barrier,直到所有线程都到达 Barrier,所有线程才会同时解除等待状态,继续往下执行。
  wait_timeout 方法也是等待其他线程到达 Barrier,但是可以设置一个超时时间,如果等待超时,当前线程就会继续执行。
  下面是一个简单的示例,演示了如何使用 Barrier:use std::sync::{Arc, Barrier}; use std::thread;  fn main() {     let mut threads = vec![];     let barrier = Arc::new(Barrier::new(4));      for i in 0..4 {         let c = barrier.clone();         let t = thread::spawn(move || {             println!("thread {} before wait", i);             c.wait();             println!("thread {} after wait", i);         });         threads.push(t);     }      for t in threads {         t.join().unwrap();     } }
  上面的代码中,我们首先创建了一个 Barrier 对象,并将它的引用保存到了 Arc 中。然后,我们创建了四个线程,每个线程都会先打印 "thread x before wait",然后等待其他线程到达 Barrier,最后打印 "thread x after wait"。
  在主线程中,我们调用了每个线程的 join 方法,以等待它们全部执行完毕。
  7.2.Condvar
  Condvar 是另一个同步原语,它提供了一种机制,可以让线程在某个条件变量满足时阻塞等待,而不是简单地一直轮询。Condvar 主要有三个方法:wait、wait_timeout 和 notify_one。
  wait 方法会使当前线程等待条件变量满足,直到另外一个线程调用了 Condvar 的 notify_one 方法来唤醒它。
  wait_timeout 方法也是等待条件变量满足,但是可以设置一个超时时间,如果等待超时,当前线程就会继续执行。
  notify_one 方法会唤醒一个在等待条件变量的线程。
  下面是一个简单的示例,演示了如何使用 Condvar:use std::sync::{Arc, Mutex, Condvar}; use std::thread;  fn main() {     let pair = Arc::new((Mutex::new(false), Condvar::new()));     let pair2 = pair.clone();      let handle = thread::spawn(move || {         let &(ref lock, ref cvar) = &*pair2;         let mut started = lock.lock().unwrap();         *started = true;         cvar.notify_one();         // Do some work     });      let &(ref lock, ref cvar) = &*pair;     let mut started = lock.lock().unwrap();     while !*started {         started = cvar.wait(started).unwrap();     }     // Do some other work     handle.join().unwrap(); }
  这个示例创建了一个互斥锁和条件变量的元组,并使用 Arc 来在两个线程之间共享它们。第一个线程获取互斥锁并设置 started 标志为 true,然后调用条件变量的 notify_one 方法通知第二个线程可以开始执行了。第二个线程在 while 循环中等待 started 标志变为 true,并在条件变量上调用 wait 方法,这将释放互斥锁并进入等待状态,直到被 notify_one 唤醒。
  这个示例展示了如何使用条件变量来实现线程同步,这是一种在并发编程中常用的技术。通过使用条件变量,我们可以避免在循环中使用忙等待的方式来等待某些条件的满足。
  7.3.Semaphore
  Semaphore(信号量)是一种用于控制并发访问的同步原语。它可以维护一个计数器,用于表示可用资源的数量。每当一个线程需要使用一个资源时,它会尝试获取一个信号量。如果信号量计数器的值大于零,那么线程将获取该资源并将计数器减少。如果计数器的值为零,那么线程将被阻塞,直到有一个资源可用为止。当一个线程使用完一个资源后,它会将计数器增加,以使其他线程能够使用该资源。
  在 Rust 中,可以使用 std::sync::Semaphore 来创建一个信号量,并使用 acquire 和 release 方法来进行获取和释放操作。
  以下是一个简单的示例,演示了如何使用 Semaphore 来限制同时执行的线程数量:use std::sync::Arc; use std::sync::Semaphore; use std::thread;  fn main() {     // 创建一个计数器为 2 的 Semaphore     let sem = Arc::new(Semaphore::new(2));      // 启动 5 个线程进行计数器操作     for i in 0..5 {         let sem_clone = sem.clone();         thread::spawn(move || {             // 获取信号量             sem_clone.acquire();              println!("Thread {} started", i);             thread::sleep(std::time::Duration::from_secs(2));             println!("Thread {} finished", i);              // 释放信号量             sem_clone.release();         });     }      // 等待所有线程执行完毕     thread::sleep(std::time::Duration::from_secs(5)); }
  在上面的示例中,我们首先创建了一个计数器为 2 的 Semaphore,并使用 Arc 来将其包装在一个可共享的引用计数指针中。接下来,我们启动了 5 个线程来执行操作。每个线程首先会尝试获取信号量,如果有可用的资源,那么它将执行其操作,并在完成后释放信号量,以便其他线程可以获取该资源。由于我们的 Semaphore 计数器只有 2,因此在任何给定时刻,最多只有 2 个线程可以同时执行。
  最后,我们在主线程中等待所有线程执行完毕,以便观察其输出。Rust 并发编程实践并发编程案例实践
  下面是一个简单的 Rust 并发编程案例实践,通过多线程实现并发下载多个网页:use std::thread;  fn main() {     let urls = vec![         "https://www.example.com/page1",         "https://www.example.com/page2",         "https://www.example.com/page3",         "https://www.example.com/page4",         "https://www.example.com/page5",     ];     let mut handles = vec![];      for url in urls {         let handle = thread::spawn(move || {             // 这里是下载网页的代码,例如使用 reqwest 库             // 下载成功后可以将网页保存到本地或者打印网页内容             println!("Downloaded {}", url);         });         handles.push(handle);     }      for handle in handles {         handle.join().unwrap();     } }
  在这个示例中,我们首先定义了需要下载的网页链接地址,然后通过 for 循环创建了多个线程,每个线程负责下载一个网页。在创建线程时,我们使用了 thread::spawn 函数来创建新的线程,并将需要下载的网页地址传递给线程,使用 move 关键字来将 url 的所有权转移给线程。
  在线程内部,我们可以编写下载网页的代码,并在下载成功后打印一条提示信息。最后,我们在主线程中使用 join 函数来等待所有线程执行完毕,并通过 unwrap 方法处理可能的错误。
  这个示例演示了如何使用 Rust 的多线程功能来实现并发下载多个网页,同时也涉及到了 Rust 的所有权和闭包等基础概念。Rust 并发编程项目实战
  这里提供一个简单的 Rust 并发编程项目示例:一个基于多线程和通道实现的简单的任务调度器。
  任务调度器包含多个 worker 线程和一个任务队列,它从队列中获取任务并将其分配给 worker 线程。每个 worker 线程都会不断地从队列中获取任务并执行。
  下面是示例代码:use std::sync::{Arc, Mutex}; use std::thread;  type Job = Box;  struct Worker {     id: usize,     thread: Option>, }  impl Worker {     fn new(id: usize, receiver: Arc>>) -> Self {         let thread = thread::spawn(move || loop {             let job = receiver.lock().unwrap().recv().unwrap();             println!("Worker {} got a job; executing.", id);             job();         });         Self {             id,             thread: Some(thread),         }     } }  pub struct ThreadPool {     workers: Vec,     sender: crossbeam::channel::Sender, }  impl ThreadPool {     pub fn new(size: usize) -> Self {         assert!(size > 0);          let (sender, receiver) = crossbeam::channel::unbounded::();         let receiver = Arc::new(Mutex::new(receiver));          let mut workers = Vec::with_capacity(size);         for id in 0..size {             workers.push(Worker::new(id, receiver.clone()));         }          Self { workers, sender }     }      pub fn execute(&self, f: F)     where         F: FnOnce() + Send + "static,     {         let job = Box::new(f);         self.sender.send(job).unwrap();     } }  impl Drop for ThreadPool {     fn drop(&mut self) {         for worker in &mut self.workers {             println!("Sending terminate message to worker {}", worker.id);             self.sender.send(Box::new(|| {})).unwrap();             if let Some(thread) = worker.thread.take() {                 thread.join().unwrap();             }         }     } }  fn main() {     let pool = ThreadPool::new(4);      for i in 0..8 {         pool.execute(move || {             println!("Executing job {} in thread {:?}", i, thread::current().id());             thread::sleep(std::time::Duration::from_secs(1));         });     }      std::thread::sleep(std::time::Duration::from_secs(5)); }
  这个示例实现了一个简单的线程池,并使用通道来实现任务的调度和分配。主线程创建了一个线程池,然后提交了 8 个任务。线程池会将这些任务分配给 4 个 worker 线程,每个线程会执行任务并打印出执行的任务编号。每个任务都会 sleep 1 秒钟,以模拟一些耗时的工作。最后,主线程 sleep 5 秒钟,等待所有任务执行完毕。
  这个示例展示了如何使用 Rust 的并发编程机制来实现一个简单的任务调度器。在实际的项目中,任务调度器可以用于实现并发任务处理、消息处理、网络服务器等。Rust 并发编程最佳实践
  以下是一些 Rust 并发编程的最佳实践:使用 std::sync::mpsc 而不是 std::sync::Arc>,这样可以避免一些潜在的死锁问题。尽量使用不可变数据,避免使用可变数据,这样可以避免并发访问导致的数据竞争问题。使用 std::thread::spawn 创建新的线程,而不是 std::thread::scoped,因为 scoped 可能会导致线程泄露。使用 std::sync::Once 来实现一次性初始化,这样可以避免多个线程同时初始化相同的资源。使用 std::sync::Barrier 来等待多个线程同时到达一个点,这样可以避免死锁和活锁问题。
  以下是一个 Rust 并发编程的示例,使用最佳实践来避免潜在的问题:use std::sync::{mpsc, Arc, Barrier, Mutex}; use std::thread;  fn main() {     let (tx, rx) = mpsc::channel();     let barrier = Arc::new(Barrier::new(3));     let data = Arc::new(Mutex::new(0));      for i in 0..3 {         let tx1 = tx.clone();         let barrier1 = barrier.clone();         let data1 = data.clone();          thread::spawn(move || {             // 等待所有线程都到达此处             barrier1.wait();              // 递增数据             let mut num = data1.lock().unwrap();             *num += i;              // 发送数据             tx1.send(*num).unwrap();         });     }      // 等待所有线程都执行完毕     for _ in 0..3 {         rx.recv().unwrap();     } }
  在这个示例中,我们使用了 std::sync::mpsc 来进行线程间通信,避免了使用 std::sync::Arc> 带来的潜在死锁问题。我们使用了 std::sync::Barrier 来等待所有线程都到达一个点,避免了死锁和活锁问题。我们也使用了不可变数据,避免了并发访问导致的数据竞争问题。总结
  在本篇学习笔记中,我们学习了 Rust 并发编程的基础知识,包括并发模型、线程基础、共享状态与可变性、锁和互斥体、Atomics 和 Memory Ordering、通道和消息传递等。我们还深入了解了 Rust 并发编程的高级主题,如生命周期和并发、异步编程和 Futures、异步编程和 Tokio 框架、并发编程中的性能优化、错误处理和并发编程、原子引用计数和并发计数器、高级同步构造如 Barrier、Condvar 和 Semaphore。
  同时,我们也探讨了 Rust 并发编程的实践,包括并发编程案例实践、Rust 并发编程项目实战和 Rust 并发编程最佳实践。通过这些实践,我们可以更加深入地了解 Rust 并发编程的应用。
  在下一篇学习笔记中,我们将探讨 Rust 中的泛型和 trait,泛型和 trait 是 Rust 中非常重要的概念,也是 Rust 语言的核心特性之一。我们将介绍泛型和 trait 的概念、使用方法和一些高级特性。敬请期待!

大人带娃放加特林烟花烧了!1月4日12时38分江苏常州新北区一草坪突然起火火势蔓延迅速还引燃了周边一间店铺据现场群众描述火灾原因为附近的大人带着两三岁的孩子在这里燃放加特林烟花烟花火苗随即点燃了堆放在草坪上干咳不止,这两种口服液儿童如何用好?仅供医学专业人士阅读参考更多儿科常用药怎么用,上临床决策助手App!得了流感新冠等疾病以后,有时会出现干咳不止的症状,十分难受,尤其儿童患者,必须要服用一些止咳药缓解不适。针对干咳小克鲁伊夫虽打进了西超杯决赛,但巴萨还要继续提高直播吧1月13日讯北京时间1月13日凌晨,本赛季西班牙超级杯半决赛,巴萨通过互罚点球64击败贝蒂斯,小克鲁伊夫在赛后接受采访时表示虽然巴萨实现了打进西超杯决赛的目标,但我们还要继续影驰B760金属大师主板上架黑白双色,949元起IT之家1月13日消息,据影驰官方消息,影驰B760金属大师主板正式开售,黑白双色,黑金版首发价949元,白金版首发价999元。据介绍,影驰B760金属大师主板为黑白双版本,MAT大话西游2斩杀倭寇活动中,剑精灵霸气输出一挑十怀旧服中看到截图,一个斩杀倭寇活动中,最新打造的剑精灵,超高攻击,霸气输出一挑十一回合结束战斗!先看下剑精灵状态栏这技能打造得如何,完全为大力而生,觉醒技乱舞狂刀。并且天生技能有1下饭神器涪陵榨菜如今的涪陵榨菜已走进千家万户,成为人们喜爱的下饭神器。本报记者黄汉鑫摄光明图片国家地理标志产品探秘啥子?榨菜还曾经是奢侈品?!看着记者吃惊的表情,重庆涪陵榨菜产业发展中心副主任陈林我的二胎顺产记二宝悄悄来到我的肚子的时候,有纠结有开心,产检一路绿灯过,整个孕期自己做饭洗衣服干家务,接送大宝上下学,上下班都是步行就是想着老大是顺产,尽量老二能顺就顺,因为是二胎大家都说会提前好书推荐满月猫咪咖啡店翻阅2022书名满月猫咪咖啡店作者望月麻衣译者邱香凝绘者樱田千寻出版春天出版提要满月猫咪咖啡店是一家只在月圆之夜开店的行动式咖啡店,没有固定位置,店长和店员都是会说话与人类差不多大30年过去了,你对赵雅芝新白娘子传奇的影响力一无所知娱乐圈考古记多年后,当赵雅芝在节目中分享自己演白娘子时是怎样施法的,应该总会想起自己读介绍观音书籍的那段日子。导演对赵雅芝和陈美琪说,她俩要演的白蛇和青蛇是会法术的,但究竟要怎么施管彤与韩红身为好友,却被谣言断送前程,难怪现在很少看到她前言说起管彤,相信很多人对她都非常熟悉,管彤曾经可是家喻户晓的央视一姐,在主持人行业中,管彤的实力可是很强的要知道央视对于主持人的要求非常高,面对这个高门槛的行业,管彤不知道在背后自降身价的明星们,真的赚到钱了吗?2022年,影视寒冬之下,拍戏减少的明星们都去哪里了?直播间肯定是明星含量最多的地方之一。明星艺人下沉式直播带货,不仅能增加曝光量,也能快速的将流量变现。根据新抖数据显示,张柏芝在
哪个游戏的地图最让你感到震撼?最让人感到震撼的地图,第一时间想到的就是宫崎英高制作的魂like游戏中的地图,特别是黑暗之魂中地图非常巧妙游戏从来不设置任何地图,全屏玩家自己摸索,最开始会觉得非常复杂,因为路线众多少人为玩王者荣耀发愁?特别是孩子,说实话真担心误了孩子学习,你们怎么控制孩子玩?作为一名教师,我很明确告诉题主的是,几乎所有的负责任的家长和老师都在为孩子疯狂玩王者荣耀而发愁。因为无限沉迷于玩玩者荣耀除了耗费大量的时间和精力外,还有很多不好的表现一严重影响孩子王者荣耀微信区游戏环境感觉越来越差,混子太多,单排真心累,你认为呢?你好,很高兴回答你的问题,楼主说微信区游戏环境越来越差,混子太多,单排累,下面我说下我个人观点,首先,我感觉微信区游戏环境一般,算不上差,我也是是一个玩微信区的玩家,上图是我的游戏已交了三年平安福保险,最近收入压力山大想退保,对此你怎么看?有什么好的建议吗?谢邀!保险虽好,可不要多买呦!买多了不仅不能帮我们分摊风险,更是增加了我们的家庭负担,甚至影响我们的生活质量,更有甚者影响夫妻感情的和睦共处。所以,买保险也要量体裁衣,在自己的经济王者荣耀高端局代练为什么必选姜子牙?感谢邀请,在下老梅德,王者荣耀分析师,7个赛季王者我都这么努力吹了,你还不关注我吗?老梅德也是做过一段时间代练的,姜子牙这个英雄确实比较受代练青睐,原因且听我慢慢道来加快节奏受到代怀孕后你会选择提前休假吗?你们的产假有多长?说实在,没什么特殊情况,我愿意上到快生前两个星期,先说说怀孕期间上班真的可以调节心情,有同事可以聊天,自己的作息时间很有规律。如果是从一怀孕就要保胎的除外。其次怀孕时可以给自己挣点如何改掉孩子晚睡的坏习惯?谢谢邀请!和你分享一下!现在很多孩子晚上经常熬夜,一直熬到11点甚至12点多才上床休息,而第二天早晨却赖在床上不肯起来。孩子熬夜的危害孩子晚上迟睡的原因是多方面的,有的是家长迟睡孩男性到了什么年龄最好不要二胎?跟年龄无关,如今社会,关键看实力,经济实力。男的想生,以为穷有穷的过法,可是女的考虑的更多,生出来之后,怎么培养好。所以关键还真是看经济条件。如囊中羞涩,家徒四壁,何来养育二胎之资怀孕晚期,你家老公是怎么样的?半夜自己难受睡不着生不如死,自家那位在旁边呼呼大睡?我也是孕妈,现在37周了,每天晚上睡觉都很煎熬,频繁起夜,老是会失眠,你的心情作为孕妇来讲我是理解的,但我想说你真的不应该把这种气撒在老公身上,你可以白天跟他说说,睡前给你按按摩捶有哪些曾经是跑龙套的群众演员最终成了大明星?其实我是一个演员。来源电影喜剧之王截图就算你一定要叫我跑龙套的,也不要加个死在前面吧。来源电影喜剧之王片段看过电影喜剧之王的,肯定都对这两句经典台词印象深刻。片中,周星驰饰演的尹天如果将小鲁班被动趴下那一瞬间设置成霸体状态,射手里面会无敌吗?先说一点,貌似不是每个鲁班都能解锁被动下趴动作吧,我要没记错,只有电玩小子才有此专属特权。我猜题主的意思应该不是特指给电玩小子新增霸体扫射吧?言归正传,暂且按鲁班被动扫射期间处于霸
友情链接:快好知快生活快百科快传网中准网文好找聚热点快软件