【Rust中多线程同步机制】

Rust中多线程同步机制


多线程编程

多线程编程,在rust异步编程中我们提到过:

线程用于并行工作,异步用于并行等待

并行即一对一处理任务,并发即M对N轮换处理任务,看起来像是同步在运行的程序实际上在循环更替交替占用cpu核。


Rust中的多线程编程

thread::spawn

Spawns a new thread, returning a JoinHandle for it.

spawn方法用于创建一个新线程并将返回一个Join句柄

join句柄很好理解,即在多线程编程中,常见的我们一般会在主线程做子线程的等待回收,此返回值便是如此。

代码示例:

rust 复制代码
use std::thread;
fn main() {
    let handler = thread::spawn(|| {
        // thread code
        println!("this is a thread");
    });
    handler.join().unwrap();//显然join会返回一个Result结果
}

move

代码示例:

rust 复制代码
use std::thread;

fn main() {
    let vec = vec![4, 2, 3];
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", vec);
    });
    handle.join().unwrap();
}

move表示对vec所有权的转移。因为thread::spawn表示创建一个新的线程,我们无法获知新线程的生命周期,所以当子线程使用到主线程的中的变量,通常情况下需要将所有权也进行转移。

Rust中的线程间同步机制

与其他语言一样,Rust也提供了诸如互斥锁,读写锁,条件变量,信号量等的线程间数据同步机制,下面一一进行举例说明:

为保证程序性能,一尽量避免线程间需要的同步,二如果一定需要同步,选择合适的同步机制以及细粒度。

Rust线程间同步机制之Mutex

代码示例:

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(10));
    let mut handles = vec![];
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num -= 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Result: {}", *counter.lock().unwrap());
}

Arc是线程安全的是不是意味着可以直接让其携带数据?--不是的,Arc的线程安全保证的是这个智能指针的计数在多线程中是完全保证其原子性的,但是我发保证其所指的数据是安全的,所以在Rust的多线程编程中,Arc常与Mutex共同使用,除非所指数据并不需要修改。

上述代码中我们使用了Vec来存储joinhander,并在后续的for循环中依次收集等待了子线程的运行结束,如果不这样做而让其自行结束会发生什么?
由于创建线程更加的耗时,所以会出现打印结果大于0的情况,因为一部分子线程并没有等到运行结束而是跟随主线程一同消亡了。

简而言之,如果开发者需要一个在多线程间同步的数据,并且需要在线程间修改,那么使用Arc+Mutex是没有问题的。

Rust线程间同步机制之读写锁

Rust中的读写锁与其他语言中的并无二致,读写锁允许同时读,但同一时刻有且只能有一个写。在编程时为了避免死锁以及非规范写法带来的程序风险,使用TryLockxx是很好的。

代码示例:

rust 复制代码
use std::sync::RwLock;

fn main() {
    let lock = RwLock::new(5);

    // many reader locks can be held at once
    {
        let r1 = lock.read().unwrap();
        let r2 = lock.read().unwrap();
        assert_eq!(*r1, 5);
        assert_eq!(*r2, 5);
    } // read locks are dropped at this point

    // only one write lock may be held, however
    {
        let mut w = lock.write().unwrap();
        *w += 1;
        assert_eq!(*w, 6);
    } // write lock is dropped here
}

Rust线程同步机制之条件变量

条件变量一般和锁配合使用,既等待条件变量的线程会在等待出获得锁在进入条件变量等待后释放锁等待唤醒,而唤醒线程会使用notice,broadcast等方法通知到等待线程,进而线程恢复执行态,带着锁向下执行。这是很经典的生产者消费者模型。

代码示例:

rust 复制代码
use std::sync::{Arc, Condvar, Mutex};
use std::thread;

fn main() {
    let pair = Arc::new((Mutex::new(false), Condvar::new()));  //创建锁和条件变量
    let pair2 = Arc::clone(&pair);

    // Inside of our lock, spawn a new thread, and then wait for it to start.
    thread::spawn(move || {
        let (lock, cvar) = &*pair2;
        let mut started = lock.lock().unwrap();
        *started = true;
        // We notify the condvar that the value has changed.
        cvar.notify_one();//通知线程
    });

    // Wait for the thread to start up.
    let (lock, cvar) = &*pair;
    let mut started = lock.lock().unwrap();
    while !*started {
        started = cvar.wait(started).unwrap();//等待线程
    }
}

Rust中的信号量

信号量一般指系统层面的控制资源的如C接口sem_init,sem_wait,sem_post等,信号量本身就是原子性的,通过PV操作,既增加或减持来保证数据的统一性和资源的同步性,一般用在共享内存的同步机制,如共享内存的数据队列等,在Rust中,原理是一样的,都是通过PV操作来保证数据的一致性。Rust中的信号量关键字是Semaphore。

代码示例:

rust 复制代码
use tokio::sync::{Semaphore, TryAcquireError};
#[tokio::main]
async fn main() {
    let semaphore = Semaphore::new(3);

    let a_permit = semaphore.acquire().await.unwrap();
    let two_permits = semaphore.acquire_many(2).await.unwrap();

    assert_eq!(semaphore.available_permits(), 0);

    let permit_attempt = semaphore.try_acquire();
    assert_eq!(permit_attempt.err(), Some(TryAcquireError::NoPermits));
}

Rust中的Atomic

标准库中的Atomic:

rust 复制代码
AtomicBool	A boolean type which can be safely shared between threads.
AtomicI8	An integer type which can be safely shared between threads.
AtomicI16	An integer type which can be safely shared between threads.
AtomicI32	An integer type which can be safely shared between threads.
AtomicI64	An integer type which can be safely shared between threads.
AtomicIsize	An integer type which can be safely shared between threads.
AtomicPtr	A raw pointer type which can be safely shared between threads.
AtomicU8	An integer type which can be safely shared between threads.
AtomicU16	An integer type which can be safely shared between threads.
AtomicU32	An integer type which can be safely shared between threads.
AtomicU64	An integer type which can be safely shared between threads.
AtomicUsize	An integer type which can be safely shared between threads.

Atomic仅支持内置类型的原子性操作,一般情况开发者使用不到,只有在编写库的时候可能会用到,其使用CAS机制保证了数据的线程安全性。

代码示例:

rust 复制代码
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{hint, thread};

fn main() {
    let spinlock = Arc::new(AtomicUsize::new(1));

    let spinlock_clone = Arc::clone(&spinlock);

    let thread = thread::spawn(move || {
        spinlock_clone.store(0, Ordering::Release);
    });

    // Wait for the other thread to release the lock
    while spinlock.load(Ordering::Acquire) != 0 {
        hint::spin_loop();
    }

    if let Err(panic) = thread.join() {
        println!("Thread had an error: {panic:?}");
    }
}

Rust中线程间的数据传递

一般情况下,我们在使用C++多线程时,会使用同步机制保证数据的一致性,以及共享数据的安全性。在Rust中不仅仅可以使用线程间同步的方式,如加锁,信号量,条件变量等,一般的在C++中提及数据传递一般发生在进程间,如管道,队列等等,rust对线程间的数据传递 也进行了友好的支持如Module std::sync::mpsc。

Module std::sync::mpsc 即Multi-producer, single-consumer FIFO queue communication primitives.

发送与接收在第一次使用时便可确定数据类型,并在使用期间不可变化,如果想发送不同的数据类型,则需要使用enum包裹。

代码示例:

rust 复制代码
use std::sync::mpsc::channel;
use std::thread;

fn main() {
    // Create a shared channel that can be sent along from many threads
    // where tx is the sending half (tx for transmission), and rx is the receiving
    // half (rx for receiving).
    let (tx, rx) = channel();
    for i in 0..10 {
        let tx = tx.clone();
        thread::spawn(move || {
            tx.send(i).unwrap();//无接收者时,send将会返回错误
        });
    }

    for _ in 0..10 {
        let j = rx.recv().unwrap();//发送者全部消失时,recv将会返回错误(阻塞模式,由发送者recv就会一直等待),try------recv仅做接收尝试,可以接收不到数据(既不阻塞)
        assert!(0 <= j && j < 10);
    }
}

只有实现了Sync+Send的数据类型方为线程安全的,对于自组织类型,如果其中一个没有实现,那么整个都无法是线程安全的。并且一般情况下开发者无需 手动实现Sync+Send,当程序报错时首先考虑是不是自己的定义有问题


总结

Rust语言的线程间同步机制大概就这么多,需要不断的熟悉才能掌握。

"不确定的未来才是最确定的"

相关推荐
心仪悦悦2 分钟前
Scala的Array(2)
开发语言·后端·scala
Oak Zhang22 分钟前
sharding-jdbc自定义分片算法,表对应关系存储在mysql中,缓存到redis或者本地
redis·mysql·缓存
yqcoder25 分钟前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_8827275734 分钟前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
baivfhpwxf202335 分钟前
C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)
开发语言·c#
许嵩6638 分钟前
IC脚本之perl
开发语言·perl
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
直裾1 小时前
Scala全文单词统计
开发语言·c#·scala
心仪悦悦1 小时前
Scala中的集合复习(1)
开发语言·后端·scala
JIAY_WX1 小时前
kotlin
开发语言·kotlin