rust语言的多线程编程

百思 百思     2022-08-04     327

关键词:

Concurrency并发

在计算机科学上,并发Concurrency 和并行 parallelism是非常重要的话题,也是软件产业一个热门的话题。电脑有了越来越多的的核,但喝多程序员没有准备好充分利用它们。

Rust的内存安全特性也应用于并发。Rust程序必须内存安全,没有数据竞争。Rust的类型系统 很胜任这工作,很容易让你理解在编译时的并行代码。

在谈论Rust并发特色之前,了解一些东西很重要:Rust是一门足够低级的语言,所有这些都由标准库提供,而不是语言本身。这意味着如果你你不喜欢Rust处理并发的某些方面,你可以用其它方式来实现。 mio 就是这方面实践的一个真实的例子。

背景: Send  Sync

并发很难解释清楚。在Rust中,我们由一个强大的静态的类型系统来帮助我们理解我们的代码。Rust本身给我们两个特性,帮助我们实现并发编程。

Send

第一个类是 Send. 当类型 T 实现了 Send, 它告诉编译器这个类型的实例的所有权可以在多个线程之间安全传递。

实施强制的限制条件是很重要的。例如,如果我们由一个通道在两个线程之间,我们可能会想在两个线程之间传递数据。因此,我们要保证传递的数据类型要实现 Send 特性。

相反,如果我我们用FFI包裹了一个线程不安全的类库,我们不会去实现 Send, 编译器会帮助我们确保它不会离开当前线程。

Sync

第二个特性是 Sync. 当一个类型实现了 Sync, 它向编译器表明这个类型的数据在多线程并发是不可能导致内存的不安全。例如,由原子引用计数的不可变数据的共享是线程安全的。Rust提供了一个类型 Arc<T>, 它实现了 Sync, 因此它可以在线程之间共享。

这两个特性运行你使用类型系统来保证你代码在并发情况下的所有权。在解释为什么之前,让我们先创建一段并行的Rust代码。

Threads

Rust标准库中的线程,允许你并行的运行Rust代码。下面是一个使用了 std::thread的例子:

usestd::thread;

fnmain() {
    thread::spawn(|| {
        println!("Hello from a thread!");
    });
}

 

 thread::spawn() 方法接受一个在新线程运行的闭包。spawn方法返回一个线程的处理对象,可以用来等待子线程结束和取得线程返回结果:

usestd::thread;

fnmain() {
    lethandle=thread::spawn(|| {
        "Hello from a thread!"
    });

    println!("{}", handle.join().unwrap());
}

 

很多语言有执行多线程的能力,但是非常不安全。有很多关于如何防止共享状态数据导致错误的书籍。Rust通过在编译时防止数据竞争来解决这个问题。让我们谈论一下怎样真正地在线程之间安全地共享数据。

安全地共享可变状态

归功于Rust的类型系统,我们与一个看似谎言的概念:安全地共享可变状态。很多程序员都认同共享可变状态是非常非常糟糕的。

有人曾经说过:

共享可变状态是万恶的根源。大多数语言尝试从“可变”这个方向来解决这个问题,但Rust通过“共享”这方面来解决这个问题。

 ownership system 帮助我们防止错误地使用指针,同样也帮助我们排除数据竞争。数据竞争是并发编程中最恐怖地bug之一。

举例说明,下面是一个Rust程序,里面有一个其它语言经常会出现地数据竞争。但是在Rust中是无法编译通过地:

usestd::thread;

fnmain() {
    let mut data=vec![1u32, 2, 3];

    for i in 0..3 {
        thread::spawn(move|| {
            data[i] +=1;
        });
    }

    thread::sleep_ms(50);
}

 

编译时,提示错误如下:

8:17 error: capture of moved value: `data`
        data[i] += 1;
        ^~~~

在这种情况,从代码我们知道我们的代码应该是安全的,但是Rust不确定。事实上是不安全地,如果我们在多个线程中有 data 的引用,线程拿走了引用的所有权,我们就有了三个拥有者了。这是不行的。我们可以通过 Arc<T> 来改正,它是一个原子引用计数器指针。原子意思是在多线程共享是安全的。

Arc<T> 假定一个它的内容有多个所有权,但是仍然可以安全地共享。它假定它地内容是线程同步的。但在我们这中情况,我们向改变里面的数据。我们需要一个可以保证一次只能由一个线程改数据的类型。这个类型就是 Mutex<T> 。下面老师第二个版本的代码。虽然依然编译不通过,但是是不同原因:

usestd::thread;
usestd::sync::Mutex;

fnmain() {
    letmutdata=Mutex::new(vec![1u32, 2, 3]);

    foriin0..3 {
        let data=data.lock().unwrap();
        thread::spawn(move|| {
            data[i] +=1;
        });
    }

    thread::sleep_ms(50);
}

 

下面是错误信息:

<anon>:9:9: 9:22 error: the trait `core::marker::Send` is not implemented for the type `std::sync::mutex::MutexGuard<‘_, collections::vec::Vec<u32>>` [E0277]
<anon>:11         thread::spawn(move || {
                  ^~~~~~~~~~~~~
<anon>:9:9: 9:22 note: `std::sync::mutex::MutexGuard<‘_, collections::vec::Vec<u32>>` cannot be sent between threads safely
<anon>:11         thread::spawn(move || {
                  ^~~~~~~~~~~~~

你看看, Mutex 由一个 lock 方法,方法的签名是:

fn lock(&self) ->LockResult<MutexGuard<T>>

因为   MutexGuard<T>没有实现Send,我们不可以不能在线程之间传输这个对象,所以报错。

我们可以使用 Arc<T> 来修正这个错误。下面是可以编译通过的版本:

usestd::sync::{Arc, Mutex};
usestd::thread;

fnmain() {
    letdata=Arc::new(Mutex::new(vec![1u32, 2, 3]));

    foriin0..3 {
        let data=data.clone();
        thread::spawn(move|| {
            let mut data=data.lock().unwrap();
            data[i] +=1;
        });
    }

    thread::sleep_ms(50);
}

 

我们调用了Arc的 clone() 方法 ,增加了内部的引用计数。它们返回只移动到了一个新的线程。我们细看一下线程的主体:

thread::spawn(move|| {
    let mut data=data.lock().unwrap();
    data[i] +=1;
});

 

首先,我们调用 lock(), 取得了一个互斥锁。因为可能会失败,它返回一个结果Result<T, E>, 因为只是举例说明,我们直接调用 unwrap() 来获取data的一个引用。真实代码可能要写更全面的代码来作错误处理。因为我们现在有一个互斥锁了,所以可以自由地改变数据。

最后,当线程运行,我们等一段时间,但是这是有点不切实际:等多久合适呢,很难猜测,这是程序运行CPU执行情况决定的。

一种更加精准的计时器是使用Rust标准库提供的机制来实现线程同步。让我们讲一下这中机制: channels.

通道Channels

下面代码是使用channel来同步,而不是漫无目的地等待:

 1 usestd::sync::{Arc, Mutex};
 2 usestd::thread;
 3 usestd::sync::mpsc;
 4 
 5 fnmain() {
 6     let data=Arc::new(Mutex::new(0u32));
 7 
 8     let (tx, rx) =mpsc::channel();
 9 
10     for _ in0..10 {
11         let (data, tx) = (data.clone(), tx.clone());
12 
13         thread::spawn(move|| {
14             letmutdata=data.lock().unwrap();
15             *data+=1;
16 
17             tx.send(());
18         });
19     }
20 
21     for _ in0..10 {
22         rx.recv();
23     }
24 }

 

我们使用mpsc::channel() 方法类构造一个channel。我们用10个线程分别向通道发送一个简单地() 然后在主线程接收。

send方法是泛型地,我们可以向通道发送任何类型地数据。

usestd::thread;
usestd::sync::mpsc;

fnmain() {
    let (tx, rx) =mpsc::channel();

    for _ in0..10 {
        let tx=tx.clone();

        thread::spawn(move|| {
            let answer=42u32;

            tx.send(answer);
        });
    }

   rx.recv().ok().expect("Could not receive answer");
}

 

一个 u32 数据被发送,因为我们可以复制一份。因此我们可以创建一个线程,叫它计算答案,然后通过将答案通过channel发送给我们。

Panics致命异常

一个 panic! 会使执行中地线程崩溃。你可以这样写:

usestd::thread;

letresult=thread::spawn(move|| {
    panic!("oops!");
}).join();

assert!(result.is_err());

 

我们的线程返回了一个结果,我们可以通过这个返回结果检查线程师父抛异常。

rust编程语言入门之无畏并发(代码片段)

...式通过调用OS的API来创建线程:1:1模型需要较小的运行时语言自己实现的线程(绿色线程):M:N模型需要更大的运行时Rust:需要权衡运行时的支持Rust标准库仅提供1:1模型的线程通过spawn创建新线程通过thread::spawn函数可以创建新... 查看详情

rust入坑指南:齐头并进(下)(代码片段)

...行编程。今天我们继续学习并发编程,原子类型许多编程语言都会提供原子类型,Rust也不例外,在前文中我们聊了Rust中锁的使用,有了锁,就要小心死锁的问题,Rust虽然声称是安全并发,但是仍然无法帮助我们解决死锁的问题... 查看详情

rust编程语言入门之最后的项目:多线程web服务器(代码片段)

最后的项目:多线程Web服务器构建多线程Web服务器在socket上监听TCP连接解析少量的HTTP请求创建一个合适的HTTP响应使用线程池改进服务器的吞吐量优雅的停机和清理注意:并不是最佳实践创建项目~/rust➜cargonewhelloCreatedbinary(applica... 查看详情

rust初识及rust的esapi(代码片段)

RustRust是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似,但是设计者想要在保证性能的同时提供更好的内存安全。Rust致力... 查看详情

在c语言的多线程编程中一般volatile应该用在啥地方?

比如如果我的变量是全局变量,还需要用volatile吗?或者变量本身是数组或指针,若指针地址值不改变,指针指向的值有可能改变,不使用volatile会出现问题吗?一般用在多线程程序中,由于某个变量可能被多个线程修改,因此... 查看详情

rust编程语言入门(代码片段)

Rust编程语言入门Rust简介为什么要用Rust?Rust是一种令人兴奋的新编程语言,它可以让每个人编写可靠且高效的软件。它可以用来替换C/C++,Rust和他们具有同样的性能,但是很多常见的bug在编译时就可以被消灭。Rust是一种通用的... 查看详情

rustvspython:为啥越来越流行,取代榜一python?

参考技术A2021年,Python又获得了TIOBE年度编程语言,排名已经是第一。而Rust依然在20名以外。但依然有人认为,Rust甚至可能取代Python。不过这不重要,认清两者的优缺点,进而合适的地方使用合适的语言,这才最重要。在这个指... 查看详情

java编程:java的多线程是怎么实现的?高手进吧

...,则想提高其效率】【问题详述】我们知道现在的每一种语言都是先实现其核心,然后再领用语言本身来开发出额外功能,也就是我们常说的“库”,这种思想来源于lisp的Macro,也就是说java的“多线程”,也是用java本身实现的... 查看详情

autosar汽车软件编程语言rust

        Rust是一种多范式的通用编程语言,他的使用可以保障性能与安全,而且能够同时实现两者。        Rust在语法上与C++相似,但可以在没有回收站的情况下保证储存记忆安全。Rust被称为系统编程... 查看详情

autosar汽车软件编程语言rust

        Rust是一种多范式的通用编程语言,他的使用可以保障性能与安全,而且能够同时实现两者。        Rust在语法上与C++相似,但可以在没有回收站的情况下保证储存记忆安全。Rust被称为系统编程... 查看详情

rust备忘清单_开发速查表分享

...,为开发人员分享快速参考备忘单。Rust是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似,但是设计者想要在保证性能的同时提供更好的内存安... 查看详情

rust编程语言〇

Rust编程语言〇Rust官方:rust官方支持的环境我这里使用idea进行配置idea安装Rust插件需要安装两个插件intellij-rust和intellij-toml,intellij-rust是Rust语言插件,intellij-toml是为Toml语言的插件,是为cargo的配置文件cargo.toml使用。... 查看详情

一天一门编程语言rust语言程序设计极简教程(代码片段)

文章目录Rust语言程序设计极简教程介绍安装RustHello,World基础语法变量及数据类型控制结构`if`语句`while`语句`for`语句函数泛型泛型的语法泛型的应用TraitTrait的定义Trait的实现Trait的继承模式匹配模式匹配的语法... 查看详情

第73课qt中的多线程编程

1.QThread类(1)QThread是一个跨平台的多线程解决方案(2)QThread以简洁易用的方式实现多线程编程 2.QThread中的关键成员函数(1)virtualvoidrun():线程函数,用于定义线程功能(执行流)。(2)voidstart():启动函数,将线程入口... 查看详情

单核中的多线程与异步编程

】单核中的多线程与异步编程【英文标题】:MultithreadedvsAsynchronousprogramminginasinglecore【发布时间】:2021-12-1403:24:41【问题描述】:如果CPU一次只实时执行一项任务,那么多线程与单处理器系统中的异步编程(在效率方面)有何不... 查看详情

qt中的多线程编程(代码片段)

    QThread编程示例classMyThread:publicQThread//创建线程类protected:voidrun()//线程入口函数for(inti=0;i<5;i++)qDebug()<<objectName()<<":"<<i;sleep(1)//暂停1s; 多线程编程初 查看详情

(73课)qt中的多线程编程

...d直接支持多线程    1、QThread是一个跨平台的多线程解决方案    2、QThread以简洁易用的方式实现多线程编程注意:1、Qt中的线程以对象的形式被创建和使用     2、每一个线程对应着... 查看详情

rust编程语言入门之rust的面向对象编程特性(代码片段)

Rust的面向对象编程特性一、面向对象语言的特性Rust是面向对象编程语言吗?Rust受到多种编程范式的影响,包括面向对象面向对象通常包含以下特性:命名对象、封装、继承对象包含数据和行为“设计模式四人帮”在《设计模型... 查看详情