Rust Concurrency Cookbook
Rust Concurrency Cookbook
Version 0.14
December 27 , 2023
Machine Translated by Google
Machine Translated by Google
Table of contents
. . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3 Current thread . 1.4 Concurrency number and current thread number . 1.5 . . . . . . . . . . . . . . . . . . . . . . . . 13
. . . . . . . . . . . . . . . . . . . . . . . 15
. . . . . . . . . . . . . . . . . . . . . . . 17
1.7 ThreadLocal . . . . . . . . . . . . . . . . . . . . . . . . . 18
. . . . . . . . . . . . . . . . . . . . . . . 21
. . . . . . . . . . . . . . . . . . . . . . . . 23
1.12 Panic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
. . . . . . . . . . . . . . . . . . 25
1.13 crossbeam scoped thread .
. . . . . . . . . . . . . . . . . . . . 25
1.14 Rayon scoped thread . 1.15 send_wrapper .
. . . . . . . . . . . . . . . . . . . . . . . 26
. . . . . . . . . . . . . . . . . . . . . . . . 29
2.2 threadpool library . 2.3 rusty_pool library . 2.4 fast_threadpool library . 2.5
. . . . . . . . . . . . . . . . . . . . . . . 33
scoped_threadpool library . 2.6 scheduled_thread_pool library . 2.7 poolite library . 2.8
. . . . . . . . . . . . . . . . . . . . . . . 34
executor_service library . 2.9 threadpool_executor library .
. . . . . . . . . . . . . . . . . . . . . 37
. . . . . . . . . . . . . . . . . . . . 38
. . . . . . . . . . . . . . . . . . 39
. . . . . . . . . . . . . . . . . . . . . . . . . 40
. . . . . . . . . . . . . . . . . . . . . 42
. . . . . . . . . . . . . . . . . . . 44
49
3 async/await asynchronous programming 3.1 Overview of asynchronous programming .
. . . . . . . . . . . . . . . . . . . . . . . 49
3.2 Asynchronous programming model in Rust . 3.3 async/await syntax and usage . . . . . . . . . . . . . . . . . . . . 49
. . . . . . . . . . . . . . . . . . . 52
Machine Translated by Google
3.4 Such . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
. . . . . . . . . . . . . . . . . . . . . . . . 56
try_joinÿjoinÿselect ÿ zip .
. . . . . . . . . . . . . . . . . . . . . . . . . 57
. . . . . . . . . . . . . . . . . . . . . . . . . . . 57
. . . . . . . . . . . . . . . . . 58
4.1 cow . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.2 box . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
. . . . . . . . . 64
4.3 CellÿRefCellÿOnceCellÿLazyCell ÿ LazyLock .
4.3.1 Cell . . . . . . . . . . . . . . . . . . . . . . . . . 64
4.3.2 RefCell. . . . . . . . . . . . . . . . . . . . . . . . 65
4.3.3 OnceCell . . . . . . . . . . . . . . . . . . . . . . . 65
. . . . . . . . . . . . . . . . . 66
4.3.4 LazyCellÿLazyLock .
4.4 rc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
. . . . . . . . . . . . . . . . . . . . . . . 72
5.2.1 Lock . . . . . . . . . . . . . . . . . . . . . . . . . 73
. . . . . . . . . . . . . . . . . . . . . . 74
5.2.2 try_lock .
. . . . . . . . . . . . . . . . . . . . . 75
5.2.3 Poisoning. . 5.2.4 Faster release of mutex locks . 5.3 Read-write lock RWMutex .
5.4 Once initialization . 5.5 Barrier/ Barrier . 5.6 Condition variable Condvar. . . . . . . . . . . . . . . . . . . . 76
. . . . . . . . . . . . . . . . . . . . . . 78
. . . . . . . . . . . . . . . . . . . . . . 83
. . . . . . . . . . . . . . . . . . . . . . 85
. . . . . . . . . . . . . . . . . . . . . 87
. . . . . . . . . . . . . . . . . . . . 88
5.7 LazyCell and LazyLock .
5.8 Exclusive . . . . . . . . . . . . . . . . . . . . . . . . . . 89
. . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.9 mpsc . 5.10 Semaphore . 5.11 Atomic
. . . . . . . . . . . . . . . . . . . . . 92
operation atomic 5.11.1 Ordering of atomic operation .
. . . . . . . . . . . . . . . . . . . . . . . 92
. . . . . . . . . . . . . . . . . 95
. . . . . . . . . . . . . . . . . . 96
5.11.2 Ordering::Relaxed .
. . . . . . . . . . . . . . . . . . 96
5.11.3 Ordering::Acquire .
. . . . . . . . . . . . . . . . . . 97
5.11.4 Ordering::Release .
. . . . . . . . . . . . . . . . . . 98
5.11.5 Ordering::AcqRel .
. . . . . . . . . . . . . . . . . . 99
5.11.6 Ordering::SeqCst .
. . . . . . . . . . . . . . . . . . . . 102
. . . . . . . . . . . . . . . . . . . . . . . . . 104
Machine Translated by Google
. . . . . . . . . . . . . . . . . . . . . . . . . . 105
6.6 evmap . 6.7 arc-swap .
. . . . . . . . . . . . . . . . . . . . . . . . . 106
process . 7.2 Wait for the process to end . 7.3 Configure input and . . . . . . . . . . . . . . . . . . . . . . . . . 109
output . 7.4 Environment variables . 7.5 Set the working directory . 7.6 Set the UID and . . . . . . . . . . . . . . . . . . . . . . . 109
GID of the process . 7.7 Pass to the file opened by the child process . 7.8 Control the child . . . . . . . . . . . . . . . . . . . . . . . 110
process . 7.8.1 Wait for the child process to end . 7.8.2 Send signals . . . . . . . . . . . . . . . . . . . . . . . . . 110
to the child process . 7.8.3 Interact with the child process through standard input and . . . . . . . . . . . . . . . . . . . . . . . 111
output . 7.9 Implement pipes . 7.10 Interact with I/O of the child process . . . . . . . . . . . . . . . . . . . . 111
. . . . . . . . . . . . . . . . . . . 112
. . . . . . . . . . . . . . . . . . . . . . . . 113
. . . . . . . . . . . . . . . . . . . 113
. . . . . . . . . . . . . . . . . . 114
. . . . . . . . . . . . . 114
. . . . . . . . . . . . . . . . . . . . . . . . . 114
. . . . . . . . . . . . . . . . . . . . . 115
. . . . . . . . . . . . . . . . . . . . . . . . . . . 119
. . . . . . . . . . . . . . . . . . . . . 123
. . . . . . . . . . . . . . . . . . . . . . . 130
8.4 async_channel . 8.5 futures_channel . 8.6 crossfire .
. . . . . . . . . . . . . . . . . . . . . . 131
. . . . . . . . . . . . . . . . . . . . . . . . . 133
. . . . . . . . . . . . . . . . . . . . 140
. . . . . . . . . . . . . . . . . . 141
. . . . . . . . . . . . . . . . . . . . . . . 142
. . . . . . . . . . . . . . . . . . . . . 142
. . . . . . . . . . . . . . . . . . . . . 142
9.1.6 async-timer . 9.1.7 timer-kit .
. . . . . . . . . . . . . . . . . . . . . . 142
. . . . . . . . . . . . . 142
9.1.8 hierarchical_hash_wheel_timer .
145
10parking_lot concurrent library
11crossbeam concurrency library 11.1 Atomic operations . 11.2 Data structure . 11.2.1 155
. . . . . . . . . . . . . . . . . . . . . . . . . 157
. . . . . . . . . . . . . . . . . . . 157
. . . . . . . . . . . . . . . . . . . . . 159
11.2.2 ArrayQueue .
. . . . . . . . . . . . . . . . . . . . . 159
11.2.3 SegQueue . 11.3 Memory management . 11.4 Thread synchronization .
. . . . . . . . . . . . . . . . . . . . . . . . . 160
. . . . . . . . . . . . . . . . . . . . . . . . . 160
. . . . . . . . . . . . . . . . . . . . . . 165
11.4.2 Parking .
. . . . . . . . . . . . . . . . . . . . . 167
11.4.4 WaitGroup . 11.5 Utilities .
. . . . . . . . . . . . . . . . . . . . . . . . . 167
. . . . . . . . . . . . . . . . . . . . . . . 170
11.5.3 Scope . 11.6 crossbeam-skiplist .
. . . . . . . . . . . . . . . . . . . . . 170
173 . 173
12rayon library 12.1 Parallel collections . 12.2
scope . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . 174
. . . . . . . . . . . . . . . . . . . . . . . . . . . 176
. . . . . . . . . . . . . . . . . . . . . . . . . 181
. . . . . . . . . . . . . . . . . . . . . . . 184
13.2.4 Notify .
. . . . . . . . . . . . . . . . . . . . . 186
13.2.5 Semaphore .
. . . . . . . . . . . . . . . . . . . . . . . . . . . 187
. . . . . . . . . . . . . . . . . . . . . . . 188
. . . . . . . . . . . . . . . . . . . . . . 188
. . . . . . . . . . . . . . . . . . 189
13.3.3 broadcast (mpmc) . 13.3.4 watch (spmc) .
. . . . . . . . . . . . . . . . . . . . 190
Machine Translated by Google
. . . . . . . . . . . . . . . . . . . . . . . 193
13.4.1 Sleep .
13.4.3 Timeout . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . 197
. . . . . . . . . . . . . . . . . . . . . . . . . . . 200 . 203
14.3 map . 14.4 Some synchronization primitives .
14.5 Event notification . 14.6 Queue . 14.7 scc . 14.8 Semaphore . 14.9 singleflight . . . . . . . . . . . . . . . . . . . . . . .
14.10arc_swap . . . . . . . . . . . . . . . . . . . . . . . . . . 206
. . . . . . . . . . . . . . . . . . . . . . . . . . . 208
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 . 210
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . 211
. . . . . . . . . . . . . . . . . . . . . . . . . 212
Machine Translated by Google
Machine Translated by Google
thread
Thread (English: thread) is the smallest unit that the operating system can perform calculations and scheduling. In most cases, it is included in the process and is the actual
operating unit in the process. Therefore, the program is actually run in thread units. Multiple threads can run concurrently in a process, and each thread executes different tasks
in parallel. task.
Threads are the basic unit of independent scheduling and dispatch, and multiple threads in the same process will share all system resources in the process, such as virtual
address space, file descriptors, signal processing, etc. But multiple threads in the same process have their own call stack , their own register context , and their own thread-local
storage.
A process can have many threads to process it, with each thread performing different tasks in parallel. If a process has many tasks to complete, which requires many threads
and calls many cores, using multi- thread programming on multi-core or multi -CPU, or a CPU that supports Hyper-threading can improve the execution throughput of the
program. On a computer with a single CPU and a single core, multi-threading technology can also be used to separate the parts of the process that are responsible for I/O
processing and human-computer interaction that are often blocked from the intensive calculation parts, thereby improving CPU utilization.
Threads differ from traditional multitasking operating system processes in the following ways:
• Processes carry much more state information than threads, and multiple threads in a process share process state and memory
• Processes have separate address spaces while threads share their address space
• Processes interact only through the inter-process communication mechanism provided by the system
• Context switches between threads within the same process usually occur faster than context switches between processes
9
Machine Translated by Google
1 thread
• Threads consume fewer resources: Using threads, an application can use fewer resources than when using multiple processes.
source to run.
• Threads simplify sharing and communication: with processes that require message passing or shared memory mechanisms to perform inter-process communication
Differently, threads can communicate through data, code, and files that they already share.
• Threads can crash a process: Since threads share the same address space, illegal operations performed by a thread can crash
the entire process; therefore, a misbehaving thread can interrupt the processing of all other threads in the application.
There are also some programming languages, such as SmallTalk, Ruby, Lua, Python, etc., which also have smaller scheduling units
called coroutines . Coroutines are very similar to threads. But coroutines are cooperative multitasking, while threads are typically
preemptive multitasking. This means that coroutines provide concurrency but not parallelism. Coroutines can also be implemented
using threads that are preemptively scheduled, but some benefits will be lost. The Go language implements the minimum scheduling
unit of Goroutine . Although it is not officially equated with coroutine because goroutine implements a unique scheduling and execution
mechanism, you can roughly think of it as the same thing as coroutine.
There is also a smaller type of scheduling unit called fiber (English: Fiber), which is the most lightweight thread. It is a user thread that
allows applications to independently decide how their threads should operate. The operating system kernel cannot see it and does not
schedule it. Just like regular threads, fibers have their own addressing space. But fibers adopt cooperative multitasking , while threads
(Pre-emptive multitasking). An application can create multiple fibers in a thread environment and then execute it manually. Fibers will
not be automatically executed, and the application must specify it to execute, or switch to the next fiber. Compared with threads, fibers
require less operating system support. In fact, some people also have task fibers that also belong to coroutines, because there is no
strict definition of these two, or the meanings are different for different people in different scenarios, so different people have different
understandings, such as recently The feature finally released in Java 19 , some call it fiber, some call it coroutine.
In any case, the basic unit of concurrency in Rust is the thread, although there are also some third-party libraries, such as Huang
Xudong of PingCAP implemented the Stackful coroutine library (may) and coroutine, there is even a RFC (RFC 2033: Experimentally
add coroutines to Rust) Pay attention to it, but the current mainstream implementation of Rust concurrency is still implemented using
threads, including the recently implemented async/await feature. The runtime still runs in the form of threads and thread pools.
Therefore, as the first chapter of concurrent programming in Rust , our focus is on the use of threads.
10
Machine Translated by Google
Rust standard library std::thread The crate provides thread-related functions. As mentioned above, a Rust program
Execution will start a process, which will contain one or more threads. Threads in Rust are pure operations.
The thread of the system has its own stack and state. Communication between threads can be through channels, like Go slang
Like the channel in the language , you can also use some synchronization primitives). We will do this in later chapters
introduce.
1 pub fn start_one_thread() {
2 let handle = thread::spawn(|| {
3 println!("Hello from a thread!");
4 });
5
6 handle.join().unwrap();
7}
In this code, we start a new thread in the current thread through thread.spawn . The new thread simply
Output Hello from a thread text.
If you call this start_one_thread function in the main function , you will see this output normally in the console.
Text, but if you comment out the sentence handle.join.unwrap();, the expected text may
will not be output because when the main program exits, even these newly opened threads will be forced to exit, so sometimes
You need to wait for these threads to complete by joining . If you ignore the JoinHandle returned by thread::spawn
value, then the newly created thread is called detached . By calling the join method of JoinHandle , the
The user will have to wait for the thread to complete.
The caller can even get the final return value of the thread:
1 pub fn start_one_thread_result() {
2 let handle = thread::spawn(|| {
3 println!("Hello from a thread!");
4 200
5 });
6
7 match handle.join() {
8 Ok(v) => println!("thread result: {}", v),
9 Err(e) => println!("error: {:?}", e),
10 }
11 }
1 pub fn start_two_threads() {
2 let handle1 = thread::spawn(|| {
3 println!("Hello from a thread1!");
4 });
5
11
Machine Translated by Google
1 thread
10 handle1.join().unwrap();
11 handle2.join().unwrap();
12 }
But what if N threads are started? You can use a Vector to save the handle of the thread :
1 pub fn start_n_threads() {
2 const N: isize = 10;
3
15 }
Wait a minute.
1 pub fn start_one_thread_by_builder() {
2 let builder = thread::Builder::new()
3 .name("foo".into()) // set thread name
4 .stack_size(32 * 1024); // set stack size
5
12 handler.join().unwrap();
13 }
It provides spawn to start a thread, and also provides spawn_scoped to start a scoped thread.
(discussed below), an experimental method spawn_unchecked, provides a looser declaration cycle binding, calling
The user should ensure that the thread's join must be called before the referenced object is discarded , or use the 'static statement'
Period, because it is an experimental method, we will not introduce it too much. A simple example is as follows:
12
Machine Translated by Google
1 #![feature(thread_spawn_unchecked)]
2 use thread;
3
6 let x = 1;
7 let thread_x =
8 &x;
9 let handler = unsafe {
10 builder.spawn_unchecked(move || {
11 println!("x = {}", *thread_x);
12 }).unwrap()
13 };
14
How to get the current thread? It can be obtained through thread::current() , which will return a Thread
Object through which you can obtain the ID and name of the thread:
1 pub fn current_thread() {
2 let current_thread = thread::current();
3 println!(
4 "current thread: {:?},{:?}",
5 current_thread.id(),
6 current_thread.name()
7 );
8
24 handler.join().unwrap();
25 }
13
Machine Translated by Google
1 thread
Even, you can wake up the blocked (parked) thread through its unpark method:
1 use std::thread;
2 use std::time::Duration;
3
12 thread::sleep(Duration::from_millis(10));
13
17 parked_thread.join().unwrap();
Park and unpark are methods used to block and wake up threads. They can effectively utilize the CPU and make it temporarily unavailable.
Concurrency capability is a resource. A machine can provide a concurrent capability value. This value is generally equivalent to the number of times the computer has
There is a certain number of CPUs (number of logical cores), but in the environment of virtual machines and containers, the number of CPU cores that a program can use
May be restricted. You can get the current number of concurrency through available_parallelism :
7 Ok(())
8 }
affinity ( MacOS is not supported) The crate can provide the current number of CPU cores:
In more scenarios, we use num_cpus to obtain the number of CPU cores (logical cores):
1 use num_cpus;
2 let num = num_cpus::get();
If you want to get the number of threads of the current process, for example when collecting indicators for some performance monitoring, you can use
Using the num_threads crate, the actual test num_threads does not support windows, so you can use
14
Machine Translated by Google
thread-amount instead. (The Rust ecosystem is like this. There are many crates with the same or similar functions . You can
It takes time to evaluate and compare. Unlike the Go ecosystem, packages from the standard library are preferred. If not, the ecosystem
There are generally one or several high-standard libraries that are recognized by everyone in the circle and can be used. Relatively speaking, the Rust ecosystem
It's quite divisive, which is quite obvious when choosing an asynchronous runtime or network library. )
spinlock, or if you want to execute certain services regularly, such as a cron program, we can call
thread::sleep function:
1 pub fn start_thread_with_sleep() {
2 let handle1 = thread::spawn(|| {
3 thread::sleep(Duration::from_millis(2000));
4 println!("Hello from a thread3!");
5 });
6
12 handle1.join().unwrap();
13 handle2.join().unwrap();
14 }
It guarantees that the current thread sleeps for at least the specified time. Because it will block the current thread, do not use asynchronous
Call it in your code. If the time is set to 0, different platforms handle it differently. Unix- like platforms will immediately
That is, returning, the nanosleep system call will not be called, and the Windows platform will always call the underlying Sleep
System calls. If you just want to transition out of the time slice, you don't need to set the time to 0, but call the yield_now function
Just count:
15
Machine Translated by Google
1 thread
1
pub fn start_thread_with_yield_now() {
2
let handle1 = thread::spawn(|| {
3
thread::yield_now();
4
println!("yield_now!");
5
});
6
7
let handle2 = thread::spawn(|| {
8
thread::yield_now();
9
println!("yield_now in another thread!");
10
});
11
12
handle1.join().unwrap();
13
handle2.join().unwrap();
14
}
If we want a thread to sleep when the sleep time is uncertain, after a certain event occurs in the future,
If we actively wake it up, we can use the park and unpark methods we introduced earlier.
You can think of each thread as having a token that initially does not exist:
• thread::park will block the current thread until the thread's token is available.
At this point it uses the token atomically. thread::park_timeout performs the same operation but allows
Allows you to specify the maximum time to block a thread. Unlike sleep , it can be awakened before the timeout is reached.
• The thread.upark method atomically makes the token available if it is not already available. Since the token does not exist initially
Now, unpark will cause the following park call to return immediately.
1
pub fn thread_park2() {
2
let handle = thread::spawn(|| {
3
thread::sleep(Duration::from_millis(1000));
4
thread::park();
5
println!("Hello from a park thread in case of unpark first!");
6
});
7
8
handle.thread().unpark();
9
10
handle.join().unwrap();
11 }
Is it okay if I call unpark multiple times in advance and then call park all at once , as shown below:
1 ```rust
2
let handle = thread::spawn(|| {
3
thread::sleep(Duration::from_millis(1000));
4
thread::park();
5
thread::park();
16
Machine Translated by Google
6 thread::park();
7 println!("Hello from a park thread in case of unpark first!");
8 });
9 handle.thread().unpark();
10 handle.thread().unpark();
11 handle.thread().unpark();
12 handle.join().unwrap();
```
13
The answer is no. Because a thread has only one token, this token either exists or there is only one, multiple calls
Unpark is also an operation performed on a token. The above code will cause the newly created thread to always be in
parked status.
According to the official documentation, calling the park function does not guarantee that the thread will always remain parked .
The thread::scope function provides the possibility to create scoped threads . scoped thread no
Similar to the thread we created above , it can borrow non- 'static' data outside the scope . use
The Scope parameters provided by the thread::scope function can create (spawn) scoped threads. create
If the created scoped thread does not call join manually , it will automatically join before this function returns .
1 pub fn wrong_start_threads_without_scoped() {
2 let mut a = vec![1, 2, 3];
3 let mut x = 0;
4
5 thread::spawn(move || {
6 println!("hello from the first scoped thread");
7 dbg!(&a);
8 });
9 thread::spawn(move || {
10 println!("hello from the second scoped thread");
11 x += a[0] + a[2];
12 });
13 println!("hello from the main thread");
14
15 // After the scope, we can modify and access our variables again:
16 a.push(4);
17 assert_eq!(x, a.len());
18 }
This code cannot be compiled, because a outside the thread cannot be moved to two threads , even if it is moved
To a thread, external threads can no longer use it. To solve this problem we can use
scoped thread:
1 pub fn start_scoped_threads() {
2 let mut a = vec![1, 2, 3];
3 let mut x = 0;
17
Machine Translated by Google
1 thread
5 thread::scope(|s| {
6 s.spawn(|| {
7 println!("hello from the first scoped thread");
8 dbg!(&a);
9 });
10 s.spawn(|| {
11 println!("hello from the second scoped thread");
12 x += a[0] + a[2];
13 });
14 println!("hello from the main thread");
15 });
16
17 // After the scope, we can modify and access our variables again:
18 a.push(4);
19 assert_eq!(x, a.len());
20 }
Here we call the thread::scope function and use the s parameter to start two scoped threads .
We use external variables a and x . Because we only read a and write x in a single thread, there is no need to take the test.
Consider concurrency issues. After thread::scope returns, the two threads have completed execution, so the external thread can
The variable is accessed. The scope function of the standard library has not been extended further. In fact, we can see that in the new
scoped thread, can we also start a new scope thread? This achieves something similar to Java
Fork-Join parent-child thread. However, if you have this requirement, it can be achieved through a third-party library.
1.7 ThreadLocal
ThreadLocal provides an implementation of thread-local storage for Rust programs .
TLS (thread-local storage) can store data in global variables. Each thread has this storage.
A copy of the stored variable. Threads will not share this data. The copy is unique to the thread, so access to it does not require
Synchronous control. There are similar data structures in Java , but Go officials do not recommend implementing goroutine-local.
storageÿ
The thread-local key owns its value, and the value is destroyed when the thread exits. We use thread_local!
The macro creates a thread-local key, which can contain 'static' values. It uses the with access function to access the value. like
If we want to modify the value, we also need to combine the two types Cell and RefCell, which
, we will discuss later in the synchronization primitives chapter.
Reintroducing, currently you can understand that they provide internal modifiability for immutable variables.
1 pub fn start_threads_with_threadlocal() {
2 thread_local!(static COUNTER: RefCell<u32> = RefCell::new(1));
3
4 COUNTER.with(|c| {
5 *c.borrow_mut() = 2;
6 });
7
18
Machine Translated by Google
1.8 Move
10 *c.borrow_mut() = 3;
11 });
12
13 COUNTER.with(|c| {
14 println!("Hello from a thread7, c={}!", *c.borrow());
15 });
16 });
17
23 COUNTER.with(|c| {
24 println!("Hello from a thread8, c={}!", *c.borrow());
25 });
26 });
27
28 handle1.join().unwrap();
29 handle2.join().unwrap();
30
31 COUNTER.with(|c| {
32 println!("Hello from main, c={}!", *c.borrow());
33 });
34 }
In this example, we define a Thread_local key: COUNTER. in external thread and two sub
COUNTER is modified using with in the thread , but modifying COUNTER will only affect this thread. can watch
At the end, the value of COUNTER output by the external thread is 2, although the two child threads modified the value of COUNTER .
for 3 and 4.
1.8 Move
In the previous example, we can see that sometimes when calling thread::spawn , sometimes we use
Make the corresponding closure not used move depend on whether to obtain ownership of external variables. If you do not get the external variable
common:
1 pub fn start_one_thread_with_move() {
2 let x = 100;
3
8 handle.join().unwrap();
9
19
Machine Translated by Google
1 thread
There is a question here. Doesn’t move transfer the ownership of x to the first sub-thread? Why does the second sub-thread
This is because the x variable is of type i32 , which implements the Copy trait, and actually copies it when moving .
value, if we replace x with a type that does not implement Copy , similar code will not compile because
Ownership of x has been transferred to the first child thread:
1 pub fn start_one_thread_with_move2() {
2 let x = vec![1, 2, 3];
3
8 handle.join().unwrap();
9
20 }
From all the examples above, it seems that we have no way to control the created child thread, we can only wait for its execution or
The operator ignores its execution and has no way to stop it midway, or tell it to stop. The goroutine created by Go also has
Similar question, but Go provides Context.WithCancel and channel , the parent goroutine can pass
Pass the signal to the child goroutine . Rust can also implement a similar mechanism. We can use the mpsc mentioned later.
Or similar synchronization primitives such as spsc or oneshot can be used for control. You can also use this crate:thread-control:
20
Machine Translated by Google
1 pub fn control_thread() {
2 let (flag, control) = make_pair();
3 let handle = thread::spawn(move || {
4 while flag.alive() {
5 thread::sleep(Duration::from_millis(100));
6 println!("I'm alive!");
7 }
8 });
9
10 thread::sleep(Duration::from_millis(100));
11 assert_eq!(control.is_done(), false);
12 control.stop(); // Also you can ‘control.interrupt()‘ it
13 handle.join().unwrap();
14
15 assert_eq!(control.is_interrupted(), false);
16 assert_eq!(control.is_done(), true);
17
Generate a pair of objects flag and control through make_pair , just like the center of two mirrors that have been broken and reunited.
Cherish each other, or more like two quanta in an entangled state, where one quantum changes and the other quantum remains unchanged
Horse perception. Here control is given to the parent process for control. You can call the stop method to trigger the message.
No., at this time flag.alive() will become false. If the child thread panicled, you can pass
Because Rust 's threads are pure operating system priorities, modern operating system threads have the concept of priority.
concept, so the priority can be set through system calls and other methods. The only disadvantage is that the platform of each operating system
The priority numbers and ranges of stations are different. Currently this library supports the following platforms:
• Linux
• Android
• DragonFly
• FreeBSD
• OpenBSD
• NetBSD
• macOS
• Windows
21
Machine Translated by Google
1 thread
1 pub fn start_thread_with_priority() {
2
let handle1 = thread::spawn(|| {
3
assert!(set_current_thread_priority(ThreadPriority::Min).is_ok());
4
println!("Hello from a thread5!");
5 });
6
7
let handle2 = thread::spawn(|| {
8
assert!(set_current_thread_priority(ThreadPriority::Max).is_ok());
9
println!("Hello from a thread6!");
10 });
11
12
handle1.join().unwrap();
13
handle2.join().unwrap();
14 }
1
use thread_priority::*;
2
use std::convert::TryInto;
3
4 //
5
assert! (set_current_thread_priority(ThreadPriority::Crossplatform(0.try_into()
.unwrap())).is_ ok());
1
use thread_priority::*;
2
3 fn main() {
4
assert! (set_current_thread_priority(ThreadPriority::Os(
WinAPIThreadPriority::Lowest.into())). is_ok());
5 }
It also provides a ThreadBuilder, which is similar to the ThreadBuilder of the standard library , but adds setting priority.
Ability:
1 pub fn thread_builder() {
2 let thread1 = ThreadBuilder::default()
3
.name("MyThread")
4
.priority(ThreadPriority::Max)
5
.spawn(|result| {
6
println!("Set priority result: {:?}", result);
7 assert!(result.is_ok());
8 })
9
.unwrap();
10
22
Machine Translated by Google
1.11Set affinity _
19 thread1.join().unwrap();
20 thread2.join().unwrap();
21 }
You can also get the priority of the current thread through get_priority :
1 use thread_priority::*;
2
3 assert!(std::thread::current().get_priority().is_ok());
4 println!("This thread's native id is: {:?}", std::thread::current().
get_native_id());
1.11Set affinity _
You can bind threads to one core or to several cores. There is an older crate core_affinity, but it
A thread can only be bound to one core. If you want to bind it to multiple cores, you can use crate affinity:
1 #[cfg(not(target_os = "macos"))]
2 pub fn use_affinity() {
3 // Select every second core
4 let cores: Vec<usize> = (0..affinity::get_core_num()).step_by(2).collect();
5 println!("Binding thread to cores : {:?}", &cores);
6
7 affinity::set_thread_affinity(&cores).unwrap();
8 println!(
9 "Current thread affinity : {:?}",
10 affinity::get_thread_affinity().unwrap()
11 );
12 }
However, it currently does not support MacOS, so it cannot be used on Apple laptops.
In the above example, we bind the current thread to an even number of cores.
Bundling cores is one of the effective ways to improve performance in extreme situations. Only using certain cores for our applications can make
These cores are specially provided for our business services, which not only provides CPU resource isolation but also improves performance.
23
Machine Translated by Google
1 thread
1.12 Panic
Fatal logic errors in Rust will cause threads to panic. When panic occurs , the thread will perform stack rollback and run the solution.
constructor and release owned resources, etc. Rust can use catch_unwind to implement try/catch -like capture
Panic function, or resume_unwind to continue execution. If panic is not caught, then the thread
1 pub fn panic_example() {
2 println!("Hello, world!");
3 let h = std::thread::spawn(|| {
4 std::thread::sleep(std::time::Duration::from_millis(1000));
5 panic!("boom");
6 });
7 let r = h.join();
8 match r {
9 Ok(r) => println!("All is well! {:?}", r),
10 Err(e) => println!("Got an error! {:?}", e),
11 }
12 println!("Exiting main!")
13 }
1 pub fn panic_caught_example() {
2 println!("Hello, panic_caught_example !");
3 let h = std::thread::spawn(|| {
4 std::thread::sleep(std::time::Duration::from_millis(1000));
5 let result = std::panic::catch_unwind(|| {
6 panic!("boom");
7 });
8 println!("panic caught, result = {}", result.is_err()); // true
9 });
10
11 let r = h.join();
12 match r {
13 Ok(r) => println!("All is well! {:?}", r), // here
14 Err(e) => println!("Got an error! {:?}", e),
15 }
16
17 ÿaÿaÿaÿaprintln!("Exiting main!")
18 }
If any thread panics through the scope thread generated by the scope , if it is not captured, the scope returns
If yes, this error will be returned.
24
Machine Translated by Google
1 pub fn crossbeam_scope() {
2 let mut a = vec![1, 2, 3];
3 let mut x = 0;
4
5 crossbeam_thread::scope(|s| {
6 s.spawn(|_| {
7 println!("hello from the first crossbeam scoped thread");
8 dbg!(&a);
9 });
10 s.spawn(|_| {
11 println!("hello from the second crossbeam scoped thread");
12 x += a[0] + a[2];
13 });
14 println!("hello from the main thread");
15 })
16 .unwrap();
17
18 // After the scope, we can modify and access our variables again:
19 a.push(4);
20 assert_eq!(x, a.len());
21 }
Here we create two sub-threads. When the sub-thread is spawned , a scope value is passed to it , using
This scope value
rayonscope in rayon - Rust (docs.rs) also provides and Crossbeam- like mechanism used to create Sun
Threads, descendant threads:
1 pub fn rayon_scope() {
2 let mut a = vec![1, 2, 3];
3 let mut x = 0;
4
5 rayon::scope(|s| {
6 s.spawn(|_| {
7 println!("hello from the first rayon scoped thread");
8 dbg!(&a);
9 });
10 s.spawn(|_| {
11 println!("hello from the second rayon scoped thread");
12 x += a[0] + a[2];
13 });
14 println!("hello from the main thread");
25
Machine Translated by Google
1 thread
15 });
16
17 // After the scope, we can modify and access our variables again:
18 a.push(4);
19 assert_eq!(x, a.len());
20 }
At the same time, rayon also provides another function: fifo 's scope thread.
1 rayon::scope_fifo(|s| {
2 s.spawn_fifo(|s| { // task s.1
3 s.spawn_fifo(|s| { // task s.1.1
4 rayon::scope_fifo(|t| {
5 t.spawn_fifo(|_| ()); // task t.1
6 t.spawn_fifo(|_| ()); // task t.2
7 });
8 });
9 });
10 ÿaÿaÿaÿas.spawn_fifo(|s| { // task s.2
11 ÿaÿaÿaÿa});
12 ÿaÿaÿaÿa// point mid
13 ÿaÿaÿaÿa
The order of concurrent execution of its threads is similar to the following order:
1 | (start)
2 |
12 :
| + <-+------------+ (scope `t` ends)
13 :
||
14
15 |
16 | (end)
1.15 send_wrapper
Cross-thread variables must implement Send, otherwise they are not allowed to be used across threads, such as the following code:
1 pub fn wrong_send() {
26
Machine Translated by Google
6 let _t = thread::spawn(move || {
7 sender.send(counter).unwrap();
8 });
9
Because Rc does not implement Send, it cannot be used directly between threads. Because the Rc pointer used by the two threads
to the same reference count value, they update this reference count at the same time, and no atomic operations are used, possibly
Can cause unexpected behavior. The Rc type can be replaced by the Arc type , or a third party can be used
Sender: Send .
1 pub fn send_wrapper() {
2 let wrapped_value = SendWrapper::new(Rc::new(42));
3
6 let _t = thread::spawn(move || {
7 sender.send(wrapped_value).unwrap();
8 });
9
Have you learned about Go language? If you have looked at the Go language for a while, you will find that it opens a new goroutine
The method is very simple. It starts a goroutine through go func() {...}() , which looks like a synchronous code.
The code is executed asynchronously.
There is a third-party library go-spawn that can provide Similar convenience methods in Go :
1 pub fn go_thread() {
2 let counter = Arc::new(AtomicI64::new(0));
3 let counter_cloned = counter.clone();
4
27
Machine Translated by Google
1 thread
9 }
10 }
11
12 assert!(join!().is_ok());
13 assert_eq!(counter.load(Ordering::SeqCst), 100);
14 }
Start a thread through the macro go!, and use join! to join the threads recently created by go_spawn . It looks like
It's also very simple. Although it doesn't get much attention, I think it is a very interesting library.
28
Machine Translated by Google
2
Thread Pool
Thread pool is a design pattern for concurrent programming that consists of a group of pre-created threads used to perform multiple tasks. The main function of the thread pool is
to reuse the created threads when tasks arrive to avoid frequent creation and destruction of threads, thereby improving system performance and resource utilization. Thread pools
are typically used in applications that need to handle a large number of short-term tasks or concurrent requests.
• Reduce the overhead of thread creation and destruction: Thread creation and destruction is an expensive operation, and the thread pool
Reusing threads reduces this overhead and improves system responsiveness and efficiency.
• Controlling concurrency: The thread pool can limit the number of threads executing at the same time, thereby effectively controlling the concurrency of the system and
• Task scheduling and load balancing: The thread pool uses task queues and scheduling algorithms to manage and allocate tasks, ensuring that tasks are allocated to
available threads in a reasonable manner to achieve load balancing and optimal resource utilization.
Rayon is a parallel computing library in Rust that makes it easier to write parallel code to take advantage of multi-core processors. Rayon provides a simple API that allows you to
parallelize iterative operations, thereby accelerating the ability to process large data sets. In addition to these core features, it also provides the ability to build thread pools.
rayon::ThreadPoolBuilder is a structure in the Rayon library that is used to customize and configure the behavior of the Rayon
thread pool. The thread pool is the core part of Rayon and manages the execution of parallel tasks. By using ThreadPoolBuilder,
you can customize the behavior of Rayon thread pools according to your needs to better suit your parallel computing tasks.
After creating the thread pool, you can use the methods provided by Rayon to execute tasks in parallel and take advantage of
the performance advantages of multi-core processors.
29
Machine Translated by Google
2 thread pool
ThreadPoolBuilder is designed with the builder pattern in the design pattern. Here are some
Main methods of ThreadPoolBuilder :
1 use rayon::ThreadPoolBuilder;
2
3 fn main() {
4 let builder = ThreadPoolBuilder::new();
5 }
2. num_threads() method: Set the number of threads in the thread pool. You can specify the thread pool through this method
The number of threads in to control the degree of parallelism. By default, Rayon automatically sets based on the number of CPU cores
Threads.
1 use rayon::ThreadPoolBuilder;
2
3 fn main() {
4 let builder = ThreadPoolBuilder::new().num_threads(4); //
4
5 }
3. thread_name() method: Set a name for the thread in the thread pool, which can help you when debugging
1 use rayon::ThreadPoolBuilder;
2
3 fn main() {
4 let builder = ThreadPoolBuilder::new().thread_name(|i| format!("
worker-{}", i));
5 }
4. build() method: Create a thread pool through the build method. This method will apply the previous configuration to
1 use rayon::ThreadPoolBuilder;
2
3 fn main() {
4 let pool = ThreadPoolBuilder::new()
5 .num_threads(4)
6 .thread_name(|i| format!("worker-{}", i))
7 .build()
8 .unwrap(); // unwrap()
9 }
5. The build_global method creates a global thread pool through the build_global method. Not recommended to you
Actively call this method to initialize the global thread pool, just use the default configuration, remember the global thread
1 rayon::ThreadPoolBuilder::new().num_threads(22).build_global().unwrap();
30
Machine Translated by Google
6. Other methods: ThreadPoolBuilder also provides some other methods for configuring the behavior of the thread pool.
For example, stack_size() is used to set the size of the thread stack.
7. It also provides some callback function settings. start_handler() is used to set the callback when the thread starts.
functions etc. spawn_handler implements customized functions to generate threads. panic_handler provides support for
Callback function for panic handling. exit_handler provides a callback when the thread exits.
The following example demonstrates using the rayon thread pool to calculate the Fibonacci sequence:
6 return a + b;
7}
9 pub fn rayon_threadpool() {
10 let pool = rayon::ThreadPoolBuilder::new()
11 .num_threads(8)
12 .build()
13 .unwrap();
14 let n = pool.install(|| fib(20));
15 println!("{}", n);
16 }
• rayon::join is used to execute two functions in parallel and wait for their results. It allows you to execute two
separate tasks and wait for them all to complete so that their results can be merged together.
By passing in the fib recursive task in join , parallel calculation of the fib sequence is achieved
Compared with spawning threads directly , using rayon 's thread pool has the following advantages:
• The number of threads is configurable, generally set according to the number of CPU cores
8 rayon::ThreadPoolBuilder::new()
31
Machine Translated by Google
2 thread pool
9 .build_scoped( \/
10 \/ Borrow pool_data in TLS for each thread. |thread|
11 POOL_DATA.set(&pool_data, || thread.run()), // Do some work that
12 needs the TLS data. |pool| pool.install(|| assert!
13 (POOL_DATA.is_set())), ).unwrap();
14
15
18
19
20 }
This Rust code uses some Rust libraries to demonstrate the use of thread pools and how to share thread- local storage (TLS,
1. scoped_tls::scoped_thread_local!(static POOL_DATA: Vec<i32>); This line of code uses the scoped_tls library macro
scoped_thread_local! to create a static thread local storage variable POOL_DATA, whose type is Vec<i32>. This
means that each thread can have its own POOL_DATA value, and these values are independent between different
threads.
2. let pool_data = vec![1, 2, 3]; In the main function, a variable pool_data of type Vec<i32> is created , which contains the
integers 1, 2 and 3.
3. assert!(!POOL_DATA.is_set()); This line of code is used to check whether POOL_DATA has been set in thread local
storage . At this initial stage, we haven't assigned a value to any of its threads yet, so it should return false.
5. .build_scoped After the thread pool is established, the .build_scoped method is used here to define threads
Pool behavior. This method requires two closures as parameters.
• The first closure |thread| POOL_DATA.set(&pool_data, || thread.run()) is used to define what each thread will do
when it starts. It sets the reference to pool_data to
The thread local storage value of POOL_DATA is run thread.run() in a new thread . The purpose of this closure
is to set the thread local storage data for each thread.
• The second closure |pool| pool.install(|| assert!(POOL_DATA.is_set())) defines the operations to be performed
after the thread pool is started. It uses the pool.install method to ensure that each thread in the thread pool can
access the thread-local storage value, and performs an assertion to verify that POOL_DATA has been set in
this thread's thread-local storage.
6. drop(pool_data); This line of code is used to release the pool_data variable after the scope of the thread pool ends . This
is because values in thread local storage are managed per thread, so after this scope ends, we need to manually
32
Machine Translated by Google
Process pool is a mechanism for managing threads. It can reuse threads in the application to reduce the cost of thread creation and destruction.
overhead and allows you to manage parallel tasks efficiently. The following is some basic introduction about the threadpool library:
1. Create a thread pool: threadpool allows you to easily create a thread pool and you can specify the size of the thread pool (i.e.
number of threads running simultaneously). This ensures that you don't create too many threads and thus avoid unnecessary
pin.
2. Submit tasks: Once the thread pool is created, you can submit tasks to the thread pool for execution. This can be
Any closure that implements the FnOnce() trait, typically used to represent a unit of work that you want to execute in parallel.
3. Task scheduling: The thread pool will automatically distribute tasks to available threads and recycle threads after the tasks are completed to ensure
It can be used for other tasks. This kind of task scheduling can reduce the overhead of thread creation and destruction and better
4. Wait for tasks to complete: You can wait for all tasks in the thread pool to complete to ensure that subsequent code continues to be executed.
Previously, all tasks were completed. This is useful for situations where you need to wait for the results of parallel tasks.
5. Error handling: threadpool provides some error handling mechanisms so that you can detect and handle tasks
Here is a simple example that demonstrates how to use the threadpool library to create a thread pool and submit tasks:
1 use std::sync::mpsc::channel;
2 use threadpool::ThreadPool;
3
4 fn main() {
5 // 4
8 //
9 let (sender, receiver) = channel();
10
11 //
12 for i in 0..8 {
13 let sender = sender.clone();
14 pool.execute(move || {
15 let result = i * 2;
16 sender.send(result).expect("");
17 });
18 }
19
20 //
21 for _ in 0..8 {
22 let result = receiver.recv().expect("");
23 println!(": {}", result);
24 }
25 }
33
Machine Translated by Google
2 thread pool
The above example creates a thread pool with 4 threads and submits 8 tasks to the thread pool, each task
Calculates twice a number and sends the result to the channel. Finally, it waits for all tasks to complete and prints the results.
Next let's look at an example of threadpool + barrier . Execute multiple tasks concurrently and use
The barrier waits for all tasks to complete. Note that the number of tasks must not be greater than the number of workers , otherwise it will cause
Deadlock:
9 // barrier, let
10 barrier = Arc::new(Barrier::new(n_jobs + 1));
11 for _ in 0..n_jobs {
12 let barrier = barrier.clone();
13 let an_atomic = an_atomic.clone();
14
15 pool.execute(move || {
16 //
17 an_atomic.fetch_add(1, Ordering::Relaxed);
18
19 //
20 barrier.wait();
21 });
22 }
23
24 //
25 barrier.wait();
26 assert_eq!(an_atomic.load(Ordering::SeqCst), /* n_jobs = */ 23);
• Core threads continue to survive, and additional threads have idle recycling mechanisms
• Create threads only when submitting tasks for the first time to avoid resource occupation
• Additional threads will be created only when the core thread pool is full
– spawn and try_spawn are used to submit the future, which will automatically poll
– Otherwise, you can directly block the execution of future through complete
34
Machine Translated by Google
The thread pool implements functions such as automatic expansion and contraction, idle recycling, and asynchronous task support.
Its adaptive control and asynchronous task support enable it to cope with sudden large traffic and save resources in normal times.
From the implementation point of view, the author uses authentic Rust concurrent programming methods such as crossbeam channels , and the code quality is very high.
So this is a very advanced and practical thread pool implementation, worthy of in-depth study and reference. It can be a good choice for us to write elastically
1 pub fn rusty_pool_example() {
2 let pool = rusty_pool::ThreadPool::default();
3
4 for _ in 1..10
5 { pool.execute(|| { println!
6 ("Hello from a rusty_pool!");
7 });
8 }
9
10 pool.join();
11 }
This example shows how to use another thread pool, rusty_pool, to achieve concurrency.
printing tasks to the thread pool in a loop • Call join in the main
thread and wait for all tasks in the thread pool to be completed
Similar to the previous threadpool , rusty_pool also provides a convenient thread pool abstraction that is simpler to use.
The following code is an example of submitting a task to the thread pool for running and then waiting for the result to be returned:
4 }); 5
let result = handle.await_complete(); 6 assert_eq!
(result, 4);
The following example shows how to execute asynchronous tasks in the rusty_pool thread pool.
• You can use await in an async block to run asynchronous functions •complete will
35
Machine Translated by Google
2 thread pool
b2. Call join in the main thread and wait for the asynchronous task to complete.
Through the combination of complete and spawn , Future tasks can be flexibly executed synchronously or asynchronously in the thread pool.
service.
rusty_pool well supports Future based asynchronous programming through the built-in async runtime .
We can use this method to implement complex asynchronous business without needing to manage threads and Futures ourselves.
1 pub fn rusty_pool_example2() {
2 let pool = rusty_pool::ThreadPool::default();
3
Next is an example of waiting for timeout and closing the thread pool:
1 pub fn rusty_pool_example3() {
2 let pool = ThreadPool::default();
3 for _ in 0..10 {
4 pool.execute(|| thread::sleep(Duration::from_secs(10)))
5 }
6
7 // join
8 pool.join_timeout(Duration::from_secs(5));
9
36
Machine Translated by Google
19 // ThreadPool
worker
20 pool.shutdown_join();
21 assert_eq!(count.load(Ordering::SeqCst), 15);
22 }
Pay the cost of thread generation. New threads are created only during the worker thread's ''idle time'' (e.g. after returning job results)
generated during.
The only situation that can cause delays is when there are not enough 'available' worker threads. To minimize the chance of this happening,
This thread pool constantly maintains a certain number of available worker threads (configurable).
This implementation allows you to asynchronously wait for the execution result of a task, so you can use it as an alternative async runtime
6 Ok(())
7 }
The following example is an example of asynchronous execution of tasks. Here we use tokio 's asynchronous runtime:
1 let rt = tokio::runtime::Runtime::new().unwrap();
2 rt.block_on(async {
37
Machine Translated by Google
2 thread pool
3
let threadpool = fast_threadpool::ThreadPool::start(ThreadPoolConfig::
default(), ()).into_async_handler();
4
assert_eq!(4, threadpool.execute(|_| { 2 + 2 }).await.unwrap());
5
});
• The life cycle of a thread is limited to a code block and stops automatically when it leaves the scope.
• Threads can directly access external state without the need for channels or mutexes
• Borrow checker automatically ensures thread safety
1
pool.scoped(|scope| {
2
scope.execute(|| {
3
4
// });
5
}); // , Join
Scoped threads provide a safer and more convenient multi-threading mode in Rust , which is worthy of our attention in multi-thread programming.
Consider using.
1 pub fn scoped_threadpool() {
2
let mut pool = scoped_threadpool::Pool::new(4);
3
4
let mut thing = thing![0, 1, 2, 3, 4, 5, 6, 7];
5
6
// Use the threads as scoped threads that can reference anything outside this
closure
7
pool.scoped(|s| {
8
// Create references to each element in the vector ...
9
for e in &mut vec {
10
// ... and add 1 to it in a seperate thread
38
Machine Translated by Google
11 s.execute(move || {
12 *e += 1;
13 });
14 }
15 });
16
This example shows how to create a scoped thread pool using the scoped_threadpool library .
• Start the thread in pool.scoped and access the external state vec in the closure
• Each thread reads an element of vec and modifies it within the thread
• When the thread pool scope ends, automatically wait for all threads to complete
Compared with the global thread pool, the advantages of the scoped thread pool are:
The scoped thread pool provides a safer and more convenient concurrency mode, which is very suitable for use in Rust .
• Based on the thread pool model to avoid repeated creation and destruction of threads
1 pub fn scheduled_thread_pool() {
2 let (sender, receiver) = channel();
3
39
Machine Translated by Google
2 thread pool
10
11 let _ = handle;
12 receiver.recv().unwrap();
13
14 }
This example shows how to create a schedulable thread pool using the scheduled_thread_pool crate .
• Print a message in the task and send a completion signal to the channel
• The main thread receives the signal in the channel and blocks waiting for the task to be completed.
• Adopt the worker thread pool model to avoid repeated creation and destruction of threads
Compared with ordinary thread pools, the advantages of scheduled thread pools are:
• Tasks can be delayed or executed regularly without having to implement a timer yourself
• Scheduling function has built-in thread pool, no need to manage threads yourself
• Scheduling semantics can be used directly, making the code more concise
1 pool.scoped(|scope| {
2 scope.push(|| println!("hello"));
3 });
40
Machine Translated by Google
Poollite also provides good support for accessing our common shared resources . The following example calculates Fibonacci
1 use spool::Spool;
2
3 use std::collections::BTreeMap;
4 use std::sync::{Arc, Mutex};
5
1 fn main() {
2 let pool = Builder::new()
3 .min(1)
4 .max(9)
5 .daemon(None) // Close
6 .timeout(None) //Close
7 .name("Worker")
41
Machine Translated by Google
2 thread pool
8 .stack_size(1024*1024*2) //2Mib
9 .build()
10 .unwrap();
11
12 for i in 0..38 {
13 pool.push(move || test(i));
14 }
15
The entire poolite library only has about 500 lines of code, which is very streamlined.
Poolite provides a simple and practical thread pool implementation, suitable for applications that do not have high performance requirements but require stability and ease of use.
If you need a small and sophisticated Rust thread pool, poolite is a very good choice.
executor_service is a Rust library that provides thread pool abstraction . Its main features are as follows:
1 //
2 let pool = Executors::new_fixed_thread_pool(4)?;
3
4 //
5 let pool = Executors::new_cached_thread_pool()?;
A thread pool with a fixed number of threads, as its name implies, creates a fixed number of threads, and the number of threads does not change.
The cache thread pool will create threads on demand, and the new threads created will be cached. 10 threads are initialized by default .
150 more threads. The maximum thread value is a constant and cannot be modified. However, the initial number of threads can be initialized.
1 //
2 pool.execute(|| println!("hello"));
3
4 // future
5 pool.spawn(async {
42
Machine Translated by Google
6 // ...
7 });
submit_sync can submit tasks synchronously and get the return value:
1 ThreadPoolExecutor::builder()
2 .core_threads(4)
3 .max_threads(8)
4 .build()?;
This example shows how to use the executor_service thread pool library:
1 pub fn executor_service_example() {
2 use executor_service::Executors;
3
10 for _ in 0..10 {
11 let counter = counter.clone();
12 executor_service.execute(move || {
13 thread::sleep(Duration::from_millis(100));
14 counter.fetch_add(1, Ordering::SeqCst);
15 });
16 }
17
18 thread::sleep(Duration::from_millis(1000));
19
20 assert_eq!(counter.load(Ordering::SeqCst), 10);
21
27 sleep(Duration::from_secs(5));
43
Machine Translated by Google
2 thread pool
2. Submit 10 tasks, pause each task for a period of time and then add 1 to the counter.
6. The main thread uses submit_sync to execute the task synchronously and obtain the return value
The parameters of all aspects of the thread pool can be customized through the builder:
1 ThreadPool::builder()
2 .core_threads(4)
3 .max_threads(8)
4 .keep_alive(Duration::from_secs(30))
5 .build();
1 //
2 pool.execute(|| println!("hello"));
3
4 //
5 pool.execute(async {
6 // ...
7 });
44
Machine Translated by Google
6 assert!(res.is_err());
7 if let Err(err) = res {
8 matches!(err.kind(), threadpool_executor::error::ErrorKind::TimeOut);
9 }
threadpool_executor provides a complete and controllable thread pool implementation, suitable for scenarios with high thread management requirements.
Its configuration capabilities are very powerful and worthy of in-depth study and use.
This example shows how to use the threadpool_executor thread pool library:
1 pub fn threadpool_executor_example() {
2 let pool = threadpool_executor::ThreadPool::new(1);
3 let mut expectation = pool.execute(|| "hello, thread pool!").unwrap();
4 assert_eq!(expectation.get_result().unwrap(), "hello, thread pool!");
5
13 pool.execute(|| {
14 std::thread::sleep(std::time::Duration::from_secs(3));
15 })
16 .unwrap();
17 let mut exp = pool.execute(|| {}).unwrap();
18 exp.cancel();
19 }
1. Create a single-threaded thread pool, submit a task and get the results
45
Machine Translated by Google
2 thread pool
threadpool_executor provides a fully functional thread pool implementation, suitable for scenarios that require fine-grained control.
46
Machine Translated by Google
My book "In-depth Understanding of Go Concurrent Programming" will be released soon, comprehensively analyzing the concurrent programming of Go language, so stay tuned!
https://cpgo.colobu.com/
47
Machine Translated by Google
Machine Translated by Google
3
async/await asynchronous programming
Asynchronous programming is a concurrent programming model that improves the concurrency and responsiveness of the system by not blocking threads during task execution. Compared with
traditional synchronous programming, asynchronous programming can better handle I/O -intensive tasks and concurrent requests, improving system throughput and performance.
time and improve resource utilization • Can handle a large number of concurrent requests
-intensive tasks: such as file operations, database access, etc. • User interface and graphics rendering:
keep the user interface smooth and responsive • Parallel computing: accelerate the execution of complex
computing tasks
As a modern system-level programming language, Rust aims to provide efficient, safe and reliable asynchronous programming capabilities.
The goal of Rust asynchronous programming is to implement high-performance, security-free asynchronous applications while providing concise syntax and rich asynchronous libraries.
49
Machine Translated by Google
The most worth reading is Rust ’s official Rust asynchronous programming book
Since concurrent programming is very important in modern society, every mainstream language has made trade-offs and carefully designed its own concurrency
model, and the Rust language is no exception. The following list can help you understand the trade-offs of different concurrency models:
• OS threads, which are the simplest and do not require any changes to the programming model (business/code logic), are therefore very suitable as the
native concurrency model of the language. We also mentioned in the multi-threading chapter that Rust I chose to natively support thread-level
concurrent programming. However, this model also has shortcomings. For example, synchronization between threads will become more difficult, and
context switching between threads will cause greater loss. Using a thread pool can improve performance to a certain extent, but for IO- intensive
• Event driven (Event driven), this term may be unfamiliar to you. If event driven is often used together with callback (Callback) , I believe everyone will
suddenly realize it. The performance of this model is quite good, but the biggest problem is the risk of callback hell: non-linear control flow and result
processing make data flow and error propagation difficult to control, and also lead to code maintainability and readability. has been significantly
• Coroutines may be the most popular concurrency model at present. The coroutine design of Go language is very excellent. This is also one of the killer
features of Go language that can quickly become popular around the world. Coroutines are similar to threads and do not need to change the
programming model. At the same time, they are also similar to async and can support a large number of tasks to run concurrently. However, the
coroutine abstraction level is too high, resulting in users being unable to access the underlying details, which is unacceptable for system programming
asynchronous runtimes. The actor model is one of Erlang ’s killer features. It divides all concurrent calculations into one A unit, these units are called actor
model is very close to reality, it is relatively easy to implement, but once it encounters flow control, failure retry and other scenarios, it becomes less
easy to use • async/await, this model has high performance and also It can support low-level programming, and at the same time, like threads
and coroutines, there is no need to change the programming model too much, but there are gains and losses. The problem with the async model is that
the internal implementation mechanism is too complex, and it is difficult for users to understand and use it. Threads and coroutines are simple.
Fortunately, developers have helped us encapsulate the complexity of the former. However, it is not simple enough to understand and use, which is
In short, after making trade-offs, Rust finally chose to provide both multi-threaded programming and async programming:
• The former is implemented through the standard library. When you don’t need such high concurrency, such as parallel computing, you can choose it. The
advantage is that the code in the thread executes more efficiently and the implementation is more intuitive and simpler. This content has already been
and will not be repeated . The latter is implemented through language features + standard library + third-party library. When you need high concurrency and asynchronous I/O ,
Just choose it
The asynchronous runtime is a runtime environment in Rust that supports asynchronous programming and is responsible for managing the execution and
scheduling of asynchronous tasks. It provides infrastructure such as task queues, thread pools, and event loops, and supports concurrent execution of
asynchronous tasks and event-driven programming models. Rust does not have a built-in runtime necessary for asynchronous calls. The main Rust asynchronous runtimes include:
• Tokio - The first choice for Rust asynchronous runtime, with strong performance and ecosystem. Tokio provides async
50
Machine Translated by Google
• async-std - Newer but full-featured runtime that provides asynchronous abstractions similar to Tokio . The code is simpler
ÿÿ
• futures/futures-lite
There is also futuresust, a basic abstract library for asynchronous programming. Most runtimes rely on futures to provide asynchronous primitives.
Toutiao is one of the well-known companies in China that uses the Rust language. They have also open sourced one of their runtimes, bytedance/monoio.
The Rust asynchronous programming model encompasses some key components and concepts, including:
• Asynchronous functions and asynchronous blocks: Asynchronous functions and asynchronous code blocks defined using the async keyword.
The biggest difference between async statement blocks and async fn is that the former cannot explicitly declare the return value. This is not a
problem most of the time, but when used together with ?, the problem is different:
};
}
51
Machine Translated by Google
14 | | Ok(1)
ˆˆ
cannot infer type for type parameter `E` declared on the enum `Resu
The reason is that the compiler cannot infer the type of E in Result<T, E> , and don’t foolishly believe the compiler’s prompt consider
giving fut a type , then try for a long time, and finally give up: There is currently no way to async The statement block specifies the return
type.
Since the compiler cannot infer the type, let's give it more hints. You can use ::< ... to add type annotations: > way to increase
};
• await keyword: Use the await keyword inside an asynchronous function to wait for the asynchronous operation to complete.
async/.await is part of the Rust syntax. When encountering a blocking operation ( such as IO) , it will give up the ownership of
the current thread instead of blocking the current thread, thus allowing the current thread to continue executing other code,
async is lazy and will not start running until it is polled or .awaited by the executor . The latter is the most commonly used method
of running Future . When .await is called, it will try to run the Future until completion, but if the Future blocks, it will give up control
of the current thread. When the Future is ready to be run again (for example, data is read from the socket ), the executor will be
• Future Trait: represents the Future Trait of asynchronous tasks and provides execution and status management of asynchronous tasks.
// Required method fn
poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
async and await are keywords in Rust for asynchronous programming. async is used to define asynchronous functions, indicating that the function body
contains asynchronous code. await is used to wait for an asynchronous operation to complete and return the result of the asynchronous operation.
• Asynchronous functions are defined using the async keyword and return types that implement Future Trait . asynchronous function
52
Machine Translated by Google
3.4 Such
You can use the await keyword in other asynchronous functions to wait for the asynchronous operation to complete. When calling an asynchronous function, it
will return an object that implements Future Trait , and you can wait for the result by calling the .await method.
• An async block is a temporary asynchronous context created inside an asynchronous function and can be created using the async keyword. Asynchronous
closures are a way of encapsulating asynchronous code in a closure, which can be created using the async keyword. Asynchronous blocks and asynchronous
closures allow waiting for asynchronous operations in a synchronized context using the await keyword.
The return type of an asynchronous function is usually a type that implements Future Trait . Future Trait represents an asynchronous task and provides execution and
status management of asynchronous tasks. The Rust standard library and third-party libraries provide many types that implement Future Trait to represent various
asynchronous operations.
fn get_two_sites() { // let
//
thread_one.join().expect("thread one panicked");
thread_two.join().expect("thread two panicked");
}
If you are simply downloading files in a small project, there is no problem in writing this way. However, once there are more concurrent requests to download files, the mode
of one download task occupying one thread will be too heavy, and it will easily become a bottleneck of the program. . Fortunately, we can use async to solve it:
async fn get_two_sites_async() {
`future` `future`
// // `future` let
future_one = download_async("https://www.foo.com"); let future_two =
download_async("https://www.bar.com");
// `future`
join!(future_one, future_two);
}
Note that the above code must be run in an asynchronous runtime, so that the asynchronous runtime uses a certain number of threads to schedule the execution of these
codes.
Next, we will learn various asynchronous runtime libraries and asynchronous runtime methods.
3.4 Such
Tokio is the most important runtime library for Rust asynchronous programming, providing asynchronous IO, asynchronous task scheduling, synchronization primitives and
other functions.
53
Machine Translated by Google
You can see that the Tokio library contains a lot of functions, including asynchronous network programming, concurrency primitives, etc. We will
spend an entire chapter introducing it later. In this section we will introduce the use of its asynchronous runtime.
You can define the main function as follows, which automatically supports runtime startup:
#[tokio::main]
async fn main() { //
tokio::spawn(async { // do
work });
//
other_task.await;
}
In this example, the async keyword must be added before the main function , and the #[tokio::main] attribute must be added, then the main will run
asynchronously.
rt.spawn(async
{ println!("Hello from a tokio task!"); println!("in
spawn")
}) .await .unwrap();
});
First it creates a Tokio runtime rt. The block_on method performs an asynchronous task in the runtime context. Here we simply print a sentence.
54
Machine Translated by Google
3.5 futures
Then use rt.spawn to execute another task asynchronously in the runtime. This task also prints a few sentences. spawn returns a JoinHandle, so .await is called
Finally, use spawn_blocking to perform a normal blocking task at runtime. This task will run in the thread pool without blocking the runtime.
• Use block_on to execute asynchronous tasks in the Tokio runtime • Use spawn to
to execute blocking tasks in the thread pool • You can awaitJoinHandle to wait for the end
The Tokio runtime provides all the functionality needed to execute and schedule asynchronous tasks. By correctly combining block_on, spawn and
spawn_blocking, Tokio 's powerful capabilities can be used to realize various asynchronous scenarios.
3.5 futures
The futures library futures is the basic abstract library for Rust asynchronous programming, providing core traits and types for writing asynchronous code.
• Future trait - represents an abstraction for asynchronous computation, whose results can be obtained with .await . • Stream
trait - represents an asynchronous data stream whose elements can be obtained iteratively through .await . • Sink trait - represents a target
that can receive data asynchronously. • Executor - the runtime environment for executing
pub fn futures_async() {
let pool = ThreadPool::new().expect("Failed to build pool"); let (tx, rx) =
mpsc::unbounded::<i32>();
fut_values.await
};
55
Machine Translated by Google
println!("Values={:?}", values);
}
This example shows how to use futures and thread pools for asynchronous programming:
Stream with map through rx , which will multiply the received number by 2. 5. Use collect to collect the results of
the Stream into a Vec. 6. block_on executes this asynchronous task in the main
This code demonstrates the combined use of futures and channels to process streams of data concurrently through a thread pool.
It's also convenient for block_on to run futures without requiring an explicit runtime.
Futures can implement non-blocking concurrent programs by asynchronously processing data streams , which is very useful in network server programming,
for example . Compared to threads, futures abstractions are generally more lightweight and efficient.
3.6 futures_lite
This library is a subset of futures that compiles an order of magnitude faster, fixes some minor issues in the futures API , fills in some obvious gaps,
In short, this library aims to be more usable than futures while still being fully compatible with them.
Let's start by creating a simple Future . In Rust , Future is a trait that represents asynchronous computation . Here is an example:
use futures_lite::future;
fn main() {
future::block_on(hello_async());
}
In this example, we use the future::block_on function in futures-lite to run the asynchronous
function hello_async.
56
Machine Translated by Google
3.7 async_std
3.7 async_std
async-std is a library that provides an asynchronous standard library for Rust . It extends the standard library to make operations such as file I/
It provides all the interfaces you're used to, but in an asynchronous form and ready for Rust 's async/await syntax.
characteristic
• Modern: Built from scratch for std::future and async/await , compilation is extremely fast. • Fast: Our reliable allocator and
thread pool design provides ultra-high throughput and predictable low latency. • Intuitive: Full equivalence with the standard
library means you only need to learn the API once. • Clear: Detailed documentation and
accessible guides mean using asynchronous Rust has never been easier.
use async_std::task;
fn main()
{ task::block_on(hello_async());
}
In the main function, use task::block_on to execute this asynchronous function. block_on will block the current thread until the incoming future
is completed.
The effect is that although the hello_async function is asynchronous, we can call it synchronously without manually handling the future.
The async/await syntax hides the details of future and brings great convenience to asynchronous programming. With async_std, we can use
3.8 pitches
smol is an ultra-lightweight async runtime library designed to simplify writing asynchronous Rust code. It provides a concise and efficient way
characteristic
• Lightweight: One of the design goals of smol is to be lightweight, allowing for fast startup and low resource overhead.
• Simple API: Provides a simple API, making the creation, combination and running of asynchronous tasks intuitive and simple.
57
Machine Translated by Google
• Zero configuration: No complex configuration is required and can be used directly in existing Rust projects.
• Asynchronous I/O operations: Supports asynchronous file I/O, network operations, etc., making asynchronous programming more flexible.
The following example demonstrates using the smol asynchronous runtime to execute an asynchronous code block:
pub fn smol_async() {
smol::block_on(async { println!("Hello from smol") });
}
The select! macro can wait for multiple futures at the same time and only process the future that completes first :
The join! macro can wait for multiple futures at the same time and process the results of all futures :
Both macros require the futures crate, making the code more concise. If you don't use macros, you need to manually create a Poll to combine multiple futures.
So select and join are very convenient when dealing with multiple futures . Select is used to process only the first completed future, and
join can process all futures at the same time.
The try_join! macro can also be used to wait for multiple futures at the same time. It is similar to join!, but has one difference:
try_join! When any future returns an error, it will return the error in advance without waiting for other futures.
For example:
58
Machine Translated by Google
use futures::try_join;
Because future2 returned an error here , try_join! will also return this error and will not wait for future1 to complete.
This is different from join!, which will wait for all futures to complete.
So the purpose of try_join! is to start multiple futures at the same time, but return immediately when encountering any error to avoid
unnecessary waiting. This is useful in scenarios where concurrency is required but any failures cannot be tolerated.
When you need to wait for all futures to obtain all results regardless of success or failure , use join!.
So both try_join! and join! can combine multiple futures, but the error handling strategies are different. Which one to choose depends on
actual needs.
The zip function joins two futures and waits for them to complete. The try_zip function will join two functions,
but it will wait for both futures to complete or one of them to return Err :
pub fn smol_zip()
{ smol::block_on(async {
use smol::future::{try_zip, zip, FutureExt};
59
Machine Translated by Google
Machine Translated by Google
4
Container synchronization primitives
Rust has some powerful primitives for concurrent programming that allow you to write safe and efficient concurrent code. One of the most
notable primitives is the ownership system, which allows you to manage memory access without locks. In addition, Rust also provides some
concurrent programming tools and standard libraries, such as threads, thread pools, message communication (mpsc , etc.), atomic operations,
etc. However, we will not introduce these tools and libraries in this chapter. They will be divided into separate chapters. Go talk. In this chapter,
we specifically talk about some methods and libraries that ensure sharing between threads.
Concurrency primitives have a lot of content and are divided into two chapters. This chapter introduces Cow, beef::Cow, Box, Cell, RefCell,
OnceCell, LazyCell, LazyLock and Rc. I call them container class concurrency primitives, mainly based on their behavior. They mainly wrap ordinary
4.1 cow
Cow (Copy-on-write) is a technology that optimizes memory and improves performance, and is usually used in resource sharing scenarios.
The basic idea is that when multiple callers request the same resource at the same time, they will all share the same resource. The system will not
actually make a copy to the caller until a caller attempts to modify the resource content. , while other callers still use the original resources.
Types such as String and Vec in Rust take advantage of COW. For example:
s2.push_str(" world"); // s2 , s2
This can avoid repeated allocation and copying of a large number of unmodified strings, vectors, etc., and improve memory utilization and performance.
61
Machine Translated by Google
The advantages of cow are: - high memory utilization, copying only when writing - high read performance, multiple callers share the
same resource
The disadvantages are: -Copying is required when writing, resulting in a certain performance loss -The implementation is more complex
It needs to be weighed according to the actual scenario. But for shared situations where there are a large number of identical or similar resources, using cow can
The std::borrow::Cow type in the standard library is a smart pointer that provides the clone-on-write function: it can encapsulate and
provide immutable access to borrowed data when modification or acquisition is required. It can lazily clone the data when taking
ownership.
Cow implements Deref, which means you can call immutable methods directly on the data it encapsulates. If changes need to be made,
to_mut will obtain a mutable reference to the owned value and clone it if necessary.
The following code wraps the origin string into a cow. You can borrow it into a &str. In fact, you can also directly call the &str method in
cow , because Cow implements Deref and can automatically dereference, such as directly calling len and into:
assert_eq!(s.len(), cow.len());
Next we have an example of cloning while writing . The following example changes all characters in a string to uppercase letters:
Here we use to_mut to get a mutable reference. Once s is modified, it will clone a copy from the original data and modify it on the cloned
data.
62
Machine Translated by Google
4.2 box
Furthermore, the beef library provides a faster and more compact Cow type. Its usage is similar to the standard library's Cow usage:
The first half of this example demonstrates the three methods of generating beef::Cow : Cow::borrowed, Cow::from,
Cow::owned, the standard library Cow also has these three methods, their differences are: - borrowed: borrow existing resources
- from: copy and create Owned from existing resources - owned: provide resource content yourself
The second half of this example compares the memory size of the standard library Cow with beef::Cow and the more compact
beef::lean::Cow . It can be seen that for Cow whose data is of str type , the current Cow of the standard library occupies three
WORDs, which is equivalent to beef::Cow , while the further compressed beef::lean::Cow only occupies two Words.
cow-utils has been optimized for the Cow of strings and has better performance.
4.2 box
Box<T>, often just called box, provides the simplest form of heap allocation in Rust . Box provides ownership of this allocation and
releases its contents when it goes out of scope. Box also ensures that they do not allocate more memory than isize::MAX bytes.
Its use is very simple. The following example moves the value val from the stack to the heap:
So how do you do the opposite? The following example moves a value from the heap to the stack via dereference:
If we want to define a recursive data structure, such as a linked list, the following method will not work, because the size of the List
is not fixed and we do not know how much memory to allocate to it:
#[derive(Debug)]
enum List<T> {
63
Machine Translated by Google
Cons(T, List<T>),
Nil,
}
#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
Currently, Rust also provides an experimental type ThinBox, which is a thin pointer, regardless of the type of the internal element:
Both Cell and RefCell are shareable mutable containers. Shareable mutable containers exist to allow mutability in a controlled manner,
even in the presence of alias references. Both Cell and RefCell allow doing this in a single-threaded environment. However, neither
4.3.1 Cell
Cell<T> allows the value it contains to be modified without violating the borrowing rules : - The value in the Cell no longer has ownership
and can only be accessed through the get and set methods. - The set method can be modified without obtaining a mutable reference
64
Machine Translated by Google
Cell value. - Suitable for simple single-value containers such as integers or characters.
The following example creates a Cell and assigns it to the variable x. Note that x is immutable, but we can modify its value through the set
use std::cell::Cell;
x.set(10); //
4.3.2 RefCell
RefCell<T> provides more flexible internal mutability, allowing borrowing rules to be checked at runtime, through runtime borrow checking:
- Immutable and mutable borrowing through the borrow and borrow_mut methods. -The borrow must be returned before the end of the
scope, otherwise it will panic. - Suitable for containers with multiple fields.
use std::cell::RefCell;
let x = RefCell::new(42);
If you enable #![feature(cell_update)], you can also update it: c.update(|x| x + 1);.
4.3.3 OnceCell
OnceCell is a type in the Rust standard library that provides cells that can be written once. It allows putting
values into cells at runtime, but only once. Once a value has been written, further write attempts will be ignored.
65
Machine Translated by Google
Main features and uses: - Once-write: OnceCell ensures that its internal value can only be written once. Once a value is
written, subsequent write operations are ignored. - Lazy initialization: OnceCell supports lazy initialization, which means it
will only be initialized when needed. This is useful in situations where you need to determine at runtime when a value is
initialized. - Thread safety: OnceCell provides thread-safe one-time writes. In a multi-threaded environment, it ensures that
only one thread can successfully write the value, and other threads' write attempts are ignored.
The following example demonstrates the use of OnceCell . Before it is initialized, its value is None. Once it is initialized to
Hello, World!, its value is fixed:
pub fn once_cell_example() {
let cell = OnceCell::new(); assert!
(cell.get().is_none()); // true
4.3.4 LazyCellÿLazyLock
Sometimes we want to achieve the effect of lazy (lazy) initialization. Of course, the lazy_static library can achieve this effect,
but the Rust standard library also provides a function, but it is still in an unstable state. You need to set #![feature(lazy_cell) ]
enable it.
#![feature(lazy_cell)]
use std::cell::LazyCell;
});
println!("ready"); println!
("{}", *lazy); // 46 println!("{}", *lazy); //
46
Note that it is lazy initialized, that is, it will only call the initialization function for initialization when you access it for the first
time.
But it is not thread-safe. If you want to use the thread-safe version, you can use std::sync::LazyLock:
use std::collections::HashMap;
use std::sync::LazyLock;
66
Machine Translated by Google
4.4 rc
});
fn main()
{ println!("ready");
std::thread::spawn(|| {
println!("{:?}", HASHMAP.get(&13)); }).join().unwrap();
println!("{:?}",
HASHMAP.get(&74));
}
4.4 rc
Rc is a smart pointer type in the Rust standard library. The full name is std::rc::Rc, which stands for "reference counting ".
It is used for ownership through reference counting when sharing the same data in multiple places. manage.
• Rc uses reference counting to track the number of references pointing to data. When the reference count drops to zero, the data is automatically
Release automatically.
• Rc allows multiple Rc pointers to share the same data without worrying about transfer of ownership.
• The data stored inside Rc is immutable. If you need variability, you can use internal variability mechanisms such as RefCell
attention when dealing with circular references, because circular references will cause the reference count to fail to drop to zero,
This results in a memory leak. To solve this problem, Weak types can be used.
The following example demonstrates the basic use of Rc . We can obtain new shared references through clone .
use std::rc::Rc;
// data 3
// reference1 reference2
Note that Rc allows immutable data to be shared in multiple places, with ownership managed through reference counting.
67
Machine Translated by Google
If you still want to modify the data, you can use the Cell- related types in the previous section. For example, in the following
Note that Rc is not thread-safe. For the above, if you want to implement a thread-safe type, you can use Arc, but we will
introduce this type in the next chapter.
68
Machine Translated by Google
5
Basic synchronization primitives
Synchronization is an important concept in multi-threaded programs. In a multi-threaded environment, multiple threads may access a shared resource at the same time, which
may lead to data competition or data inconsistency. In order to ensure data security, synchronization operations are required.
Common synchronization requirements include: - Mutual exclusion: When a thread uses a shared resource, only one thread is allowed to access the shared resource at the
same time. When one thread uses it, other threads need to wait and cannot access it at the same time. Mutually exclusive access is required. -Limit the number of threads
accessing at the same time: For some shared resources, it may be necessary to limit the number of threads accessing at the same time. -Inter - thread communication: One
thread needs to be able to continue execution based on the processing results of another thread, which requires inter-thread communication. -Ordered access: Access to
In order to achieve these synchronization requirements, synchronization primitives need to be used. Common synchronization primitives include mutex locks, semaphores,
Mutex locks can ensure that only one thread can access shared resources at the same time. A semaphore can limit the number of threads accessing it at the same time.
Condition variables enable communication and coordination between threads. The use of these synchronization primitives can avoid synchronization problems and help us
5.1 Arc
Arc has been moved to the previous chapter and will be added in this chapter. The classification I introduce here is not necessarily precise. It is just for the
convenience of introducing various libraries and concurrency primitives to everyone. There is no need to pursue the accuracy of classification.
Rust 's Arc representation (Atomic Reference Counting) is a smart pointer for multi-threaded environments. It allows sharing data in multiple places while ensuring thread
safety. The full name of Arc is std::sync::Arc, which is part of the standard library.
In Rust , usually variables are owned and managed, but sometimes we need to share them in multiple places
69
Machine Translated by Google
data. This is where Arc comes in. It allocates memory on the heap and uses reference counting to keep track of the number of owners of the data,
ensuring that resources are released correctly when they are no longer needed.
fn main() { //
let
data = Arc::new(46);
// data
let thread1 = { let
data = Arc::clone(&data);
thread::spawn(move || { // data
println!("Thread 1: {}",
data);
})
};
})
};
//
thread1.join().unwrap();
thread2.join().unwrap();
}
Arc (atomic reference counting) and Rc (reference counting) are both smart pointers for multi-ownership in Rust , but they have some key
differences.
• Thread Safety: –
Arc is thread safe and can be safely shared in multi-threaded environments. It uses atomic operations to update
New reference counting to ensure thread safety during concurrent access.
- Rc is not thread safe. It is only suitable for single-threaded environments because its reference counting operation is not original
sub, may lead to race conditions and unsafe behavior in multithreading. • Performance
overhead: – Because
Arc uses atomic operations to update reference counts, Arc has a greater performance overhead compared to Rc . Atomic
70
Machine Translated by Google
5.1 Arc
operations. •
Variability: – Arc cannot be used with variable data. If you need to share mutable data in a multi-threaded environment, you usually make
– When Arc ’s reference count decreases to zero, since it is atomic, it correctly releases the underlying resource
(such as data on the heap).
- Rc correctly releases resources when the reference count decreases to zero in single thread, but may be problematic in multi-
In short, just remember to use Arc in multi-threaded situations and Rc in single-threaded situations .
Arc and Mutex are often used together when you need to share mutable data in a multi-threaded environment . Mutex ( mutex
lock) is used to ensure that only one thread can access the locked data at any time. Here is a simple example that
demonstrates how to use Arc and Mutex to share mutable data among multiple threads:
fn main() { //
let
counter = Arc::new(Mutex::new(0));
//
let mut handles = vec![];
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.lock().unwrap());
}
71
Machine Translated by Google
The scenario where Arc and RefCell are used together usually occurs when multi-threads need to share mutable state but
do not need a mutex lock. RefCell allows borrow checking at runtime, so when used in a single-threaded environment it
does not introduce locking overhead like a Mutex .
The following is a simple example using Arc and RefCell to demonstrate sharing mutable state in a multi-threaded environment. Note that this example is only for
demonstration. We do not expect the final result of num to be the same as the above example:
fn main() { //
let
counter = Arc::new(RefCell::new(0));
//
let mut handles = vec![];
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.borrow());
}
Mutex is a mutex lock in Rust , used to solve race conditions that may occur when multiple threads access shared data concurrently.
Mutex provides a mechanism so that only the thread that owns the lock can access the locked data, and other threads must wait for the
lock to be released.
72
Machine Translated by Google
5.2.1 Lock
In the standard library, Mutex is located under the std::sync module. Here is a simple example demonstrating how to use Mutex:
fn main() { //
let
counter = Arc::new(Mutex::new(0));
//
let mut handles = vec![];
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.lock().unwrap());
}
In this example, counter is a Mutex- protected (and wrapped) mutable integer, which is then shared across multiple threads using
Arc . In each thread, acquire the lock through counter.lock().unwrap() to ensure that only one thread can modify the counter value
at a time. This ensures that race conditions do not occur in concurrent situations.
It should be noted that the lock method returns a MutexGuard, which is a smart pointer that implements the Deref and Drop traits.
When MutexGuard is destroyed, the lock is automatically released, ensuring that the lock is released correctly under any
circumstances.
Pay attention to three knowledge points here: - For cross-thread support, Mutex is generally used in combination with Arc , so that
Mutex objects can be safely accessed in each thread - The lock method returns a MutexGuard object that implements the Deref
trait , so it will automatically resolve Reference, you can directly call the method on the protected object - MutexGuard also
implements Drop, so the lock will be unlocked automatically. Generally, you do not need to actively call drop to unlock it.
73
Machine Translated by Google
The current nightly version of rust provides an experimental method unlock, which has the same function as drop and also releases the
mutex lock.
5.2.2 try_lock
The try_lock method of Mutex attempts to acquire the lock. If the lock is already held by another thread, Err is returned immediately
instead of blocking the thread. This is useful to avoid thread blocking when trying to acquire the lock.
fn main() { //
let
counter = Arc::new(Mutex::new(0));
//
let mut handles = vec![];
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.lock().unwrap());
}
In this example, the try_lock method is used to try to acquire the lock. If the acquisition is successful, the thread can modify the counter
value, otherwise it will print a message indicating that the lock was not acquired.
It should be noted that the try_lock method returns a Result. If the lock is acquired successfully, it returns Ok containing
74
Machine Translated by Google
MutexGuard, otherwise Err is returned. This allows you to perform different logic based on the results of acquiring the lock.
5.2.3 Poisoning
In Rust , poisoning is a mechanism used to deal with unrecoverable states caused by thread panics . This
concept is usually associated with Mutex and RwLock . When a thread panics while holding a lock , this
causes the lock to enter an inconsistent state because the lock's internal state may have been modified without
a chance to clean up. To avoid this situation, Rust 's standard library uses a poisoning mechanism (figurative
metaphor). Specifically , in Mutex and RwLock , when a thread panics while holding a lock , the lock will be
marked as poisoned. Any subsequent thread that attempts to acquire this lock will get a PoisonError, which
contains a flag indicating whether the lock is poisoned . In this way, the thread can detect the previous panic
and handle it accordingly.
Mutex represents this situation by wrapping PoisonError in a LockResult . Specifically, the Err
branch of LockResult is a PoisonError, which contains a MutexGuard. You can get MutexGuard
through the into_inner method and then continue.
Here is a simple example that demonstrates lock "poisoning" and how to deal with it:
fn main() { //
let
counter = Arc::new(Mutex::new(0));
//
let mut handles = vec![];
//
match result
{ Ok(mut num) =>
{ *num +=
1; // panic if *num
== 3 { panic!
("Simulated panic!");
}
Err(poisoned) => {
75
Machine Translated by Google
// "poisoned" println!
("Thread encountered a poisoned lock: {:?}", poisoned);
// MutexGuard
let mut num = poisoned.into_inner(); *num += 1;
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.lock().unwrap());
}
In this example, one thread intentionally panics when the counter reaches 3 , and other threads will get a PoisonError when trying to acquire
the lock . In the error handling branch, we print the error message and then use the into_inner method to obtain the MutexGuard to ensure
that the lock is released correctly. This way other threads can continue to use the lock normally.
As mentioned earlier, because MutexGuard implements Drop , the lock can be released automatically. However, if the scope of the lock is
too large and we want to release it as soon as possible, what should we do?
The first way you can achieve an effect similar to manually releasing Mu-tex is by creating a new internal scope . In the new scope,
MutexGuard will automatically release the lock when leaving the scope. This is an implementation of the Drop trait that is triggered by
fn main() { //
let
counter = Arc::new(Mutex::new(0));
//
let mut handles = vec![];
76
Machine Translated by Google
for _ in 0..5 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// !!!!!!!!!!!!!!
{
//
let mut num = counter.lock().unwrap();
*num += 1;
// MutexGuard
}
//
//
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.lock().unwrap());
}
The second method is to actively drop or unlock. The following is an example demonstrating manual release of Mutex :
fn main() {
//
let counter = Arc::new(Mutex::new(0));
//
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
//
let mut num = counter.lock().unwrap();
*num += 1;
77
Machine Translated by Google
// !!!!!!!!
drop(num);
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
//
println!("Final count: {}", *counter.lock().unwrap());
}
Is Mutex a reentrant lock? Probably not, but the official documentation marks it as undefined behavior, so don't try to acquire the lock twice in the same thread. If
you want to use reentrant locks, please use a third-party concurrency library that I will introduce in the future. Also noteworthy is the read-write lock RWMutex.
RWMutex is a read-write lock (Read-Write Lock) in Rust , which allows multiple threads to obtain read access to shared data at
the same time, but will be exclusive when writing. This means that multiple threads can read data at the same time, but only
one thread can write data, and no other threads are allowed to read or write while writing.
• Situations with more reads and less writes: When multiple threads need to read shared data at the same time and there are few write operations,
using RWMutex can improve concurrency performance. Multiple threads can acquire read locks simultaneously, while write operations occur
exclusively.
• Situations where read-only access and write access do not conflict: If in the logic of the program, read operations and write
operations are independent and there is no conflict, then using RWMutex can better utilize concurrency
performance. • Resource allocation and release phase: when required to allow only reads for a period of time and then only for another period of time
fn main() { //
let RwLock
counter = Arc::new(RwLock::new(0));
78
Machine Translated by Google
//
let mut read_handles = vec![];
//
let write_handle = thread::spawn(move || {
//
let mut num = counter.write().unwrap(); *num += 1;
println!("Writer
{}: Incremented counter to {}", thread::current().id(), *num);
});
//
for handle in read_handles
{ handle.join().unwrap();
}
//
write_handle.join().unwrap();
}
Its use is similar to a mutex lock, except that you need to call the read() method to obtain the read lock and the write() method to obtain the write lock.
Read-write locks have the following properties: - Multiple threads can acquire read locks at the same time to achieve concurrent reading - Only one thread can acquire write
locks, and the lock will be exclusive when writing - If a read lock has been acquired, a write lock cannot be acquired, preventing Data competition - if the write lock has been
acquired, the read lock or write lock can no longer be acquired. Prevent concurrent reading and writing when writing exclusive
If a thread already holds a read lock and another thread requests a write lock, it must wait for the read lock to be released. This ensures that no other thread can hold the
read lock while the write operation is in progress. Write locks ensure that writes to shared data are exclusive.
fn main() {
79
Machine Translated by Google
// RwLock
let counter = Arc::new(RwLock::new(0));
//
let read_handle = {
let counter = Arc::clone(&counter);
thread::spawn(move || {
//
let num = counter.read().unwrap();
println!("Reader {}: {}", thread::current().id(), *num);
//
thread::sleep(std::time::Duration::from_secs(10));
})
};
//
let write_handle = {
let counter = Arc::clone(&counter);
thread::spawn(move || {
//
thread::sleep(std::time::Duration::from_secs(1));
//
read_handle.join().unwrap();
write_handle.join().unwrap();
}
Furthermore, after the write lock request, a new read lock request comes in. Is it waiting for the write lock to be released? Or get it directly
Need a read lock? The answer is to wait for the write lock to be released, see the following example:
// RwLock
let counter = Arc::new(RwLock::new(0));
//
let read_handle = {
80
Machine Translated by Google
//
thread::sleep(std::time::Duration::from_secs(10));
})
};
//
let write_handle = {
let counter = counter.clone();
thread::spawn(move || {
//
thread::sleep(std::time::Duration::from_secs(1));
//
let mut num = counter.write().unwrap();
*num += 1;
println!("Writer : Incremented counter to {}", *num);
})
};
//
let read_handle_2 = {
let counter = counter.clone();
thread::spawn(move || {
//
thread::sleep(std::time::Duration::from_secs(2));
//
let num = counter.read().unwrap();
println!("Reader#2: {}", *num);
})
};
//
read_handle.join().unwrap();
write_handle.join().unwrap();
read_handle_2.join().unwrap();
Deadlock is a common problem in concurrent programming that may occur when RwLock is used improperly. a typical
81
Machine Translated by Google
The deadlock scenario is that one thread tries to acquire a write lock while holding a read lock, while other threads hold a write lock and try to acquire a write lock.
Try to acquire the read lock, causing each other to wait for each other.
The following is a simple example that demonstrates a situation that may lead to a RwLock deadlock:
fn main() {
// RwLock
let counter = Arc::new(RwLock::new(0));
//
let read_and_write_handle = {
let counter = Arc::clone(&counter);
thread::spawn(move || {
//
let num = counter.read().unwrap();
println!("Reader {}: {}", thread::current().id(), *num);
//
let mut num = counter.write().unwrap();
*num += 1;
println!("Reader {}: Incremented counter to {}", thread::current().id()
})
};
//
let write_and_read_handle = {
let counter = Arc::clone(&counter);
thread::spawn(move || {
//
let mut num = counter.write().unwrap();
*num += 1;
println!("Writer {}: Incremented counter to {}", thread::current().id()
//
let num = counter.read().unwrap();
println!("Writer {}: {}", thread::current().id(), *num);
})
};
//
read_and_write_handle.join().unwrap();
82
Machine Translated by Google
write_and_read_handle.join().unwrap();
}
Like Mutex , RwLock will also become poisoned when panic occurs . But please note that RwLock will only be poisoned if it panics when
it is locked by an exclusive write lock . If a panic occurs in any reader , the lock will not be poisoned.
The reasons are: - RwLock allows multiple readers to acquire read locks at the same time, and reading is non-exclusive. -If any reader
panics, other readers still hold read locks, so the status cannot be marked as poisoned. -A panic occurs only when the current thread
exclusively holds the write lock . Since no other thread holds the lock, the status can be safely marked as poisoned.
So to sum up, RwLock will only be poisoned when panic occurs during exclusive writing . Reader panic will not cause poisoning. This is
determined by the RwLock read-write lock semantics.
This mechanism can avoid unnecessary poisoning, because non-exclusive read locks will not affect each other, and panic of any lock
holder should not affect other readers. Only exclusive write locks require special handling.
std::sync::Once is a concurrency primitive in Rust that is used to ensure that an operation is executed only once during the entire
program life cycle. Once is mainly used to execute initialization code in a multi-threaded environment, ensuring that the code is only
executed once, even if multiple threads try to execute it at the same time.
fn main() { //
call_once
INIT.call_once(|| { // println!
// call_once
INIT.call_once(|| { println!
("This won't be printed.");
});
}
Usage scenarios: -Global initialization: Perform some global initialization operations when the program starts, such as initializing global
variables, loading configuration, etc. -Lazy loading: One-time initialization when needed, such as lazy loading of global configuration.
-Singleton mode: The thread-safe singleton mode can be implemented through Once , ensuring that an object is only initialized once
83
Machine Translated by Google
The following example is an example with a return value to implement lazy loading of global configuration scenarios:
fn init_global_config() { unsafe
{ GLOBAL_CONFIG.as_ref().unwrap()
}
fn main() {
println!("{}", get_global_config()); println!("{}",
get_global_config()); //
}
In this example, the get_global_config function uses Once to ensure that the init_global_config function will only be called
once, thus achieving lazy loading of global configuration.
In the previous chapter, we also introduced OnceCell and OnceLock, which are both single-initialization concurrency
primitives of the same family. The main differences are: - Once is a primitive used to ensure that an operation is only
executed once during the entire program life cycle. . It is suitable for scenarios such as global initialization, lazy loading
and singleton mode. - OnceCell is a lazy loading container that wraps a certain data type. It can perform one-time
initialization when needed and provide access to the initialized value later. - OnceLock is a thread-safe lazy loading
primitive, similar to OnceCell, but simpler and can only store Copy type data.
OnceCell is not thread-safe, while OnceLock is thread-safe, but OnceLock can only store Copy type data, while OnceCell
There is also a widely used third-party library once_cell, which provides two types of OnceCell, thread-safe and non-thread-
safe. For example, the following is a thread-safe example:
use once_cell::sync::OnceCell;
84
Machine Translated by Google
std::thread::spawn(|| {
let value: &String = CELL.get_or_init(|| { "Hello,
World!".to_string()
});
assert_eq!(value, "Hello, World!"); }).join().unwrap();
allows multiple threads to wait at a certain point until all threads have reached that point, and then they can continue executing simultaneously.
fn main() { //
let Barrier
barrier = Arc::new(Barrier::new(3)); // 3
//
let mut handles = vec![];
//
barrier.wait();
//
println!("Thread {} resumed", id);
});
handles.push(handle);
}
85
Machine Translated by Google
//
for handle in handles {
handle.join().unwrap();
}
In this example, a Barrier is created and the number of threads participating in synchronization is specified as 3. Then, three threads are
created, each thread simulates some work, and then calls barrier.wait() to wait for the other threads. After all threads have called wait ,
Usage scenario - Parallel computing: Barrier can be used when you need to ensure that multiple threads are synchronized at a certain
point in order to perform certain calculations or tasks . -Synchronization of iteration steps: In some algorithms, multiple steps may be
required, and the results of each step depend on the completion of other steps. Barrier can be used to ensure that all threads complete
the current step before continuing to the next step. -Phases of collaborative work: In multi-stage tasks, Barrier can be used to synchronize
various stages.
Barrier 's flexibility makes it very useful for coordinating the execution flow of multiple threads.
So, can Barrier be used repeatedly? Once all threads have reached the synchronization point through the wait method, the Barrier is
When all threads call the wait method, the internal state of the Barrier will be reset. The next time the wait method is called, the thread
will be blocked again until all threads reach the synchronization point again. In this way, Barrier can be used cyclically for multiple rounds
of synchronization.
The following is a simple example that demonstrates the looping use of Barrier :
//step1
barrier.wait(); println!
("after wait1");
thread::sleep(time::Duration::from_secs(1));
//step2
barrier.wait(); println!
("after wait2"); }));
86
Machine Translated by Google
Condvar is a condition variable (Condition Variable) in the Rust standard library , used for inter-thread coordination and communication
between multiple threads. Condition variables allow a thread to wait for a specific condition to be true. When the condition is met, the
fn main() { //
let Mutex Condvar
mutex = Arc::new(Mutex::new(false)); let condvar =
Arc::new(Condvar::new());
//
let mut handles = vec![];
// true
while !*guard { guard
= condvar.wait(guard).unwrap();
}
//
println!("Thread {} woke up", id);
});
handles.push(handle);
}
87
Machine Translated by Google
//
thread::sleep(std::time::Duration::from_secs(2));
//
{
let mut guard = mutex.lock().unwrap(); *guard = true;
condvar.notify_all();
//
for handle in handles {
handle.join().unwrap();
}
In this example, a Mutex and Condvar are created , where Mutex is used to protect shared state (conditions), and
Condvar is used to wait and wake up threads. After multiple threads lock the Mutex , they wait for the conditions to be
met through the condvar.wait() method, then modify the conditions in the main thread, and wake up all waiting threads
through condvar.notify_all() .
Usage scenarios - synchronization between threads: Condvar can be used for synchronization between threads, allowing threads to wait for a
certain condition to be established instead of polling for checks. -Producer -consumer model: In a multi-threaded environment, the producer
thread can notify the consumer thread that new data is generated through condition variables. -Thread pool: In the thread pool, task threads can
wait for condition variables and be awakened for execution when new tasks arrive.
It should be noted that when using Condvar , you usually need to use it with Mutex to ensure thread safety when waiting
and modifying conditions.
Condvar can send out signals by calling notify_one() method. When the notify_one() method is called ,
Condvar randomly selects a thread that is waiting for the signal and releases the thread. Condvar can also
send signals by calling notify_all() method. When the notify_all() method is called, Condvar releases all
threads waiting for the signal.
LazyCell and LazyLock in Rust are both tools for lazily initializing objects. LazyCell is used to lazily initialize values, and
LazyLock is used to lazily initialize resources.
88
Machine Translated by Google
5.8 Exclusive
LazyLock lazily initializes resources and acquires the lock for the first time
OnceCell lazily initializes the singleton value and calls the get_or_init() method for the first time
OnceLock lazily initializes the mutex and calls the lock() method for the first time
5.8 Exclusive
Exclusive in Rust is a tool used to ensure that a resource is only accessed by one thread. Exclusive can be used by
importing std::sync::Exclusive .
What is the difference between it and Mutex ? Exclusive provides only mutable access to the underlying value, also known as exclusive access to the
underlying value . It does not provide immutable or shared access to the underlying value.
While this may not seem very useful, it allows Exclusive to implement Sync unconditionally . In fact, the security
requirement of Sync is that for Exclusive to be safe, it must be safely shared across threads. That is to say, &Exclusive
must be safe when crossing thread boundaries. By design, &Exclusive does not have any API, making it useless and
therefore harmless and therefore memory safe.
This type is still a nightly experimental feature, so we might as well wait for it to stabilize before learning and using it.
5.9 mpsc
mpsc is a module in the Rust standard library that provides a message passing channel for multiple producers and single
consumers . mpsc is the abbreviation of multiple-producer, single-consumer . This module is based on channel -based
messaging communication and specifically defines three types: - Sender: sender, used to send messages asynchronously.
- SyncSender: Sync sender, used to send messages synchronously. - Receiver: Receiver, used to receive messages from
synchronous channel or asynchronous channel , can only be accessed by one thread.
Sender or SyncSender is used to send data to Receiver . Both senders are cloneable (multi-producer), so multiple threads
can send to a receiver (single consumer) at the same time .
There are two types of these channels: - Asynchronous, infinite-buffer channels. The channel function will return a
(Sender, Receiver) tuple, where all sends will be asynchronous (never blocking). The channel has a conceptually infinite
buffer. -Synchronized , bounded channels. The sync_channel function will return a (SyncSender, Receiver) tuple, and the
storage area for the message to be sent is a fixed-size pre-allocated buffer. All sends will be synchronized, via
89
Machine Translated by Google
Block until there is free buffer space. Note that a binding size of 0 is also allowed, which will make the channel a "contract" channel, with
Usage scenario - Concurrent messaging: Suitable for scenarios where multiple threads (producers) send messages to one thread
(consumer). -Task coordination: used to coordinate the execution process of multiple concurrent tasks.
Whenever I see rust 's mpsc, I always compare it with Go 's channel . In fact, rust 's channel is also very simple to use.
tx.send(10).unwrap();
});
assert_eq!(rx.recv().unwrap(), 10);
// 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();
});
}
90
Machine Translated by Google
5.9 mpsc
for _ in 0..3 { // It
would be the same without thread and clone here // since there will
still be one `tx` left. let tx = tx.clone(); // cloned tx dropped
within thread
thread::spawn(move || tx.send("ok").unwrap());
// Unbounded receiver waiting for all senders to complete. while let Ok(msg)
= rx.recv() { println!("{msg}");
println!("completed");
use std::sync::mpsc::channel;
// The call to recv() will return an error because the channel has already // hung up (or been
deallocated) let (tx, rx) = channel::<i32>();
drop(tx); assert!(rx.recv().is_err());
In the Rust standard library, there is currently no native MPMC (Multiple Producers, Multiple Consumers )
channel provided. The std::sync::mpsc module provides a single consumer channel, mainly for design and
performance considerations.
By design, MPSC channels are relatively simple to implement, can more easily meet specific performance requirements, and are
sufficient in many cases. At the same time, the usage scenarios of MPSC channels are more common. For example, there is a task
queue in a thread pool, multiple producers push tasks to the queue, and a single consumer is responsible for executing these tasks.
In the future, I will introduce more channels and similar synchronization primitives provided by third-party libraries in special chapters ,
According to this mpmc introduction, the previous The rust standard library should implement mpmc, which is extracted
from the old standard library.
91
Machine Translated by Google
5.10 Semaphore _
There is no implementation of Semaphore in the standard library . This is a very common concurrency primitive and should theoretically be introduced here.
But there is a lot of content in this chapter, and I will also introduce two signals in Tokio , in a special special concurrency primitive (Chapter 14 or more),
This chapter still focuses on the introduction of the concurrency primitives of the standard library.
Atomic Operation in Rust is a special operation that can be performed atomically in a multi-threaded environment, that is, it will not be interrupted by the
operations of other threads. Atomic operations can ensure the thread safety of data and avoid data competition.
In Rust , the std::sync::atomic module provides a set of types and functions for atomic operations. Atomic operations are special operations that can be
Atomic can be used in various scenarios, such as: - Guaranteeing the consistency of a certain value. -Prevent multiple threads from modifying a value at
Currently Rust atomic types follow the same rules as C++20 atomic, specifically atomic_ref. Basically , creating a shared reference of a Rust atomic type
is equivalent to creating an atomic_ref in C++ ; when the life cycle of the shared reference ends, the atomic_ref is also destroyed. (A Rust atomic type that
is owned exclusively or behind a mutable reference does not correspond to an "atomic object" in C++ , because it can be accessed through non-atomic
operations.)
This module defines atomic versions for some basic types , including AtomicBool, AtomicIsize,
AtomicUsize, AtomicI8, AtomicU16, etc. Atomic types provide operations that, when used correctly, can be updated synchronously across threads.
Each method has an Ordering parameter, Indicates the strength of the memory barrier for this operation. These orders are identical to C++20 atomic orders. See
92
Machine Translated by Google
Atomic variables are safely shared between threads (implementing Sync), but it does not provide a sharing mechanism itself and
follows Rust 's thread model. The most common way to share an atomic variable is to put it into an Arc ( an atomic reference-
counted shared pointer).
Atomic types can be stored in static variables, initialized using a constant initializer like AtomicBool::new . Atomic static variables
are often used for lazy global initialization.
As we have already said, this module defines atomic versions for some basic types, including AtomicBool,
AtomicIsize, AtomicUsize, AtomicI8, AtomicU16, etc. In fact, each similar method is relatively
similar, so we introduce it with AtomicI64 . This can be done via pub const fn new(v: i64)
-> AtomicI64 gets an AtomicI64 object. AtomicI64 defines some methods for operating on atomic
variables, for example:
//
pub fn load(&self, order: Ordering) -> i64 pub fn
store(&self, val: i64, order: Ordering) pub fn swap(&self, val:
i64, order: Ordering) -> i64 pub fn compare_and_swap(&self, current:
i64, new: i64, order: Ordering) -> i64 // pub fn compare_exchange( &self, current: i64, new: i64, success:
Ordering, failure: Ordering
93
Machine Translated by Google
pub fn fetch_or(&self, val: i64, order: Ordering) -> i64 pub fn fetch_xor(&self,
val: i64, order: Ordering) -> i64 pub fn fetch_update<F>( &self, set_order:
Ordering, fetch_order: Ordering,
f: F
If you have some basic knowledge of atomic operations, it is not difficult to understand these atomic operations and their variants: - store: atomic write - load:
atomic read - swap: atomic exchange - compare_and_swap: atomic compare and exchange - fetch_add: atomic Return old value after addition
//
let num = atomic_num.load(Ordering::Relaxed);
//
let old = atomic_num.fetch_add(10, Ordering::SeqCst);
//
atomic_num.compare_and_swap(old, 100, Ordering::SeqCst);
//
let swapped = atomic_num.swap(200, Ordering::Release);
//
atomic_num.store(1000, Ordering::Relaxed);
The above examples are: - load: atomic load - fetch_add: atomic addition and return the old value - compare_and_swap: atomic comparison and exchange -
These atomic operations can ensure thread safety and no data competition will occur.
Different Ordering represents memory ordering barriers with different strengths, which can be selected according to needs.
AtomicI64 provides a rich set of atomic operations that can implement lock-free concurrent algorithms and data structures.
94
Machine Translated by Google
In Rust , the Ordering enumeration is used to specify memory ordering during atomic operations . This has some similarities with the sequentiality of
atomic operations in C++ 's memory model, but there are also some differences. The following are the three main members of Ordering and their
1. Ordering::Relaxed
• Rust (Ordering::Relaxed): The most lightweight memory barrier without enforcing execution order
Sort. Allows compilers and processors to rearrange instructions around atomic operations.
• C++ (memory_order_relaxed): Has similar semantics, allowing compilers and processors to perform
lightweight instruction reordering around atomic operations.
2. Ordering::Acquire
• Rust (Ordering::Acquire): Insert an acquire memory barrier to prevent subsequent read operations from
being reordered before the current operation. Ensures that all read operations before the current operation
are performed before the current operation. • C++ (memory_order_acquire): In C++ , memory_order_acquire means acquisition
Fetch operations ensure that all read operations before the current operation are executed before the current operation.
3. Ordering::Release
• Rust (Ordering::Release): Inserts a release memory barrier to prevent previous write operations from being
reordered after the current operation. Ensures that all write operations following the current operation are
performed after the current operation. • C++ (memory_order_release): In C++ , memory_order_release represents a
release operation, ensuring that previous write operations are executed after the current operation.
4. Ordering::AcqRel
• Rust (Ordering::AcqRel): Inserts an acquire-release memory barrier that both ensures that all reads before the current operation are performed
before the current operation and that all previous writes are performed after the current operation. This memory barrier provides a balance that
is suitable for certain scenarios where acquisition and release operations alternate.
• C++ (memory_order_acq_rel): Also represents an acquire-release operation, which is a combination of acquire and release . Ensures
that all read operations before the current operation are performed before the current operation, and that all previous write operations are
5. Ordering::SeqCst
• Rust (Ordering::SeqCst): Insert a fully ordered memory barrier to ensure that all threads can see a
consistent sequence of operations. It is the strongest memory order and is used to achieve global synchronization.
Reasonable selection of Ordering can maximize performance while ensuring the required memory order constraints.
However, how to make a reasonable choice depends on the developer's basic accounting skills. When using atomic operations, you need to be careful to
ensure that the appropriate Ordering is correctly selected, and to avoid race conditions and data competition.
95
Machine Translated by Google
Like the Go language, it directly uses Ordering::SeqCst as its default memory barrier, so developers have no mental burden
when using it. However, if you want to use Ordering in a more refined way, please make sure you clearly understand your
code. The meaning of logic and Ordering .
5.11.2 Ordering::Relaxed
Ordering::Relaxed is the most lightweight memory ordering, allowing compilers and processors to rearrange instructions around atomic operations
without providing a forced execution order. This is usually used when there are no strict requirements on the order of program execution, in order
fn main() { //
let
atomic_bool = AtomicBool::new(false);
// true
let producer_thread = thread::spawn(move || {
// Ordering::Relaxed atomic_bool.store(true, Ordering::Relaxed);
});
//
let consumer_thread = thread::spawn(move || {
// Ordering::Relaxed
let value = atomic_bool.load(Ordering::Relaxed); println!("Received
value: {}", value);
});
//
producer_thread.join().unwrap();
consumer_thread.join().unwrap();
}
5.11.3 Ordering::Acquire
Ordering::Acquire in Rust means inserting an acquisition memory barrier to ensure that all read operations before the current
operation are executed before the current operation. This memory ordering is often used to synchronize shared data to ensure
that threads can correctly observe previous writes.
96
Machine Translated by Google
use std::thread;
fn main() {
//
let atomic_bool = AtomicBool::new(false);
// true
let producer_thread = thread::spawn(move || {
// true
atomic_bool.store(true, Ordering::Release);
});
//
let consumer_thread = thread::spawn(move || {
// true
while !atomic_bool.load(Ordering::Acquire) {
Acquire
// //
}
//
producer_thread.join().unwrap();
consumer_thread.join().unwrap();
}
5.11.4 Ordering::Release
Ordering::Release in Rust means inserting a release memory barrier to ensure that all previous write operations
All operations are executed after the current operation. This memory order is often used to synchronize shared data to ensure that previous write operations
fn main() {
//
let atomic_bool = AtomicBool::new(false);
// true
let producer_thread = thread::spawn(move || {
97
Machine Translated by Google
// true
atomic_bool.store(true, Ordering::Release);
});
//
let consumer_thread = thread::spawn(move || {
// true
while !atomic_bool.load(Ordering::Acquire) {
Release
// //
}
//
producer_thread.join().unwrap();
consumer_thread.join().unwrap();
}
In this example, the producer thread uses the store method to set the boolean value to true, while the consumer thread uses
The load method waits and reads the status of the Boolean value. Due to the use of Ordering::Release, in the producer thread
After setting the boolean value, a release memory barrier is inserted to ensure that all previous write operations are performed after the current operation.
OK. This ensures that the consumer thread correctly observes the producer thread's writes.
5.11.5 Ordering::AcqRel
Ordering::AcqRel means inserting an acquire-release memory barrier in Rust , which includes both acquisition and release.
Put the operation. It ensures that all read operations before the current operation are performed before the current operation, and that all previous
Any write operations are performed after the current operation. This memory order is usually used to synchronize shared data and also provides
Some balance, suitable for scenarios where acquisition and release operations need to be performed simultaneously.
fn main() {
//
let atomic_bool = AtomicBool::new(false);
// true
let producer_thread = thread::spawn(move || {
// true
atomic_bool.store(true, Ordering::AcqRel);
});
98
Machine Translated by Google
//
let consumer_thread = thread::spawn(move || {
// true
while !atomic_bool.load(Ordering::AcqRel) {
AcqRel
// //
}
//
producer_thread.join().unwrap();
consumer_thread.join().unwrap();
}
In this example, the producer thread uses the store method to set the boolean value to true, while the consumer thread uses
The load method waits and reads the status of the Boolean value. Due to the use of Ordering::AcqRel, setting
After setting the Boolean value, a get-release memory barrier will be inserted to ensure that all previous read operations are before the current operation.
Execute, while ensuring that all previous write operations are executed after the current operation. This ensures that the consumer thread can
5.11.6 Ordering::SeqCst
Ordering::SeqCst means inserting a total order memory barrier in Rust to ensure that all threads can see a
consistent sequence of operations. This is the strongest memory order and is often used to achieve global synchronization.
fn main() {
//
let atomic_bool = AtomicBool::new(false);
// true
let producer_thread = thread::spawn(move || {
// true
atomic_bool.store(true, Ordering::SeqCst);
});
//
let consumer_thread = thread::spawn(move || {
// true
99
Machine Translated by Google
while !atomic_bool.load(Ordering::SeqCst) {
SeqCst
// //
}
//
producer_thread.join().unwrap();
consumer_thread.join().unwrap();
}
In this example, the producer thread uses the store method to set the Boolean value to true, while the consumer thread uses the load
method to wait and read the status of the Boolean value. Due to the use of Ordering::SeqCst, after the producer thread sets the
Boolean value, a total order memory barrier is inserted to ensure that all threads can see a consistent order of operations. This ensures
that the consumer thread correctly observes the producer thread's writes. SeqCst is the strongest memory order and provides the
highest level of synchronization guarantee.
Ordering::Acquire and Ordering::Release form a happens-before relationship, which can achieve synchronization between different
threads.
Its typical usage is: - When a thread writes to a variable using Ordering::Release , this establishes a release barrier for the write
operation. -When other threads use Ordering::Acquire to read this variable, this creates an acquisition barrier for the read operation.
-The acquisition barrier ensures that the read operation must occur after the release barrier.
This allows: - the writing thread to ensure that the write occurs before any previous reading - the reading thread to see the latest
written value
If you use happens-before to describe these five memory orders, then: - Relaxed: There is no happens-before relationship - Release:
For a given write operation A, the release operation happens-before the read operation B, when B reads A The latest value written.
Used in conjunction with Acquire . - Acquire: For a given read operation A, the acquisition operation happens-after write operation B,
when A reads the latest value written by B. Used in conjunction with Release . - AcqRel: satisfies the happens-before relationship of
Acquire and Release at the same time . - SeqCst: There is a happens-before relationship between all SeqCst operations , forming a
total order.
The happens-before relationship means that given two operations A and B: - If A happens-before B, then A is visible to all threads and
must be executed before B. -If there is no happens-before relationship, there may be reordering and visibility issues between A and B.
Release establishes the happens-before relationship before writing , and Acquire establishes the relationship after reading. Combining
the two can make writes visible to other threads. , SeqCst forces a total order, and all operations are ordered.
100
Machine Translated by Google
6
concurrent collection
Collection types are commonly used data types in our programming. Rust provides some collection types, such as
Vec<T>ÿHashMap<K, V>ÿHashSet<T>ÿVecDeque<T>ÿLinkedList<T>ÿBTreeMap<K,
• Vec - This is a variable size array that allows efficient addition and removal of elements at the head or tail. Other categories
• HashMap<K,V> - This is a hash map that allows fast lookup of values by key. It is similar to C++ 's unordered_map or Java 's HashMap.
• LinkedList - This is a linked list data structure that allows quick addition and removal of elements from the head or tail. •
BTreeMap<K,V> - This is an ordered map that allows fast lookup by key while keeping the elements sorted. It uses B- tree as the
the elements are automatically sorted. It uses B- tree as the underlying data structure
structure.
Unfortunately, these types are not thread-safe and cannot be shared among threads. Fortunately, we can use the concurrency primitives
Arc allows multiple threads to share ownership of the same data, while Mutex is used to synchronize when accessing data, ensuring that only
101
Machine Translated by Google
6 concurrent collections
fn main() { //
let Arc Mutex A thing
shared_vec = Arc::new(Mutex::new(Vec::new()));
// Object
vec.push(i);
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
// A thing
In this example, shared_vec is a Mutex- wrapped Arc that enables multiple threads to share ownership of the Vec .
Each thread needs to acquire the lock before modifying Vec to ensure that only one thread can modify data at the
same time.
102
Machine Translated by Google
fn main() { //
Arc Mutex HashMap
let shared_map = Arc::new(Mutex::new(HashMap::new()));
let HashMap //
mut handles = vec![]; for i in 0..5
{ let shared_map =
Arc::clone(&shared_map); let handle = thread::spawn(move
|| {
//
let mut map = shared_map.lock().unwrap();
HashMap //
map.insert(i, i * i);
});
handles.push(handle);
}
//
for handle in handles {
handle.join().unwrap();
}
// HashMap let
final_map = shared_map.lock().unwrap(); println!("Final
HashMap: {:?}", *final_map);
}
You find that the processing routines are the same, which is to use Arc<Mutex<T>> . Using Arc<Mutex<T>> composition is a common
way to implement thread-safe collection types, but it is not the only option. The basic idea of this combination is to use Arc (atomic
reference counting) to achieve ownership sharing among multiple threads, while Mutex provides a mutex lock to ensure that only one
In some scenarios, you may use Arc<RwLock<T>> , which allows multiple threads to read data at the same time, but only one thread
can write data. Suitable for scenarios with frequent read operations and few write operations.
103
Machine Translated by Google
6 concurrent collections
6.3 dashmap
dashmap is an extremely fast concurrent map implementation in Rust .
DashMap attempts to implement a simple and easy-to-use API similar to std::collections::HashMap , with some
minor changes to handle concurrency.
DashMap aims to be very simple to use and can directly replace RwLock<HashMap<K, V>>. To achieve these goals, all methods use &self instead of
modifying methods using &mut self. This allows you to put a DashMap into an Arc and share it between threads while still being able to modify it.
6.4 lockfree
There is also a lockfree library that also provides a rich set of thread-safe collection classes. Since it has not been maintained for five years, I don't want to
introduce it.
• Queue
104
Machine Translated by Google
6.5 cuckoofilter
6.5 cuckoofilter
Cuckoo Filter is a hash-based data structure used to implement efficient approximate set membership checking. Its
design is inspired by Cuckoo Hashing, but has better performance and memory footprint. Cuckoo Filter is mainly used
to solve some problems of Bloom filters, such as high memory usage and not supporting deletion operations.
// Create cuckoo filter with default max capacity of 1000000 items let mut cf =
CuckooFilter::new();
// Test and add to the filter (if data does not exists then add) let success =
cf.test_and_add(value).unwrap(); assert!(!success);
6.6 evmap
evmap (eventual map) is a Rust library that provides a concurrent, event-based mapping (Map) implementation. It allows multiple
threads to read and write maps concurrently, and supports the observer pattern, allowing event listeners to be registered on
changes to the map.
Here are some key features and concepts of evmap : - Concurrent reads and writes: evmap allows multiple threads to read and
write to the map concurrently without using locks. This is achieved by dividing the map into multiple fragments, each of which can
be read and written independently. -Event triggering: evmap allows registering event listeners on changes to the mapping.
Registered listeners are triggered when the mapping changes, allowing users to perform custom logic. -Keys and values: The
keys and values in the map can be of any type, as long as they implement the Clone and Eq traits. This allows users to use
custom types as keys and values. -Asynchronous event triggering: evmap supports asynchronous event triggering. This makes it
possible to perform some asynchronous tasks when an event occurs.
105
Machine Translated by Google
6 concurrent collections
let w = w.clone();
std::thread::spawn(move || {
let mut w = w.lock().unwrap(); w.insert(i,
true); w.refresh();
})
}) .collect();
6.7 arc-swap
arc-swap is a Rust library that provides Arc- and Atomic -based data structures for atomically exchanging
data between multiple threads. It is designed to provide an efficient way to update shared data between
threads and avoid the overhead of locks. You can think of it as Atomic<Arc<T>> or RwLock<Arc<T>>.
In many cases, you may need some data structures that are read frequently and updated rarely. Some examples might be a service's
configuration, routing tables, snapshots of certain data that are updated every few minutes, etc.
In all these cases, there is a need for: - Fast, frequent and concurrent reading of the current value of the data structure from multiple
threads. - Use the same version of the data structure over a longer period of time - queries should be answered by a consistent version of
the data, and packets should be routed by the old or new version of the routing table, not by a combination. -Perform updates without
interrupting processing.
The first idea is to use RwLock<T> and keep the read lock for the entire processing time. But the update pauses all processing until
completed. A better option is to use RwLock<Arc<T>>. The lock can then be acquired, the Arc cloned and unlocked. This is subject to
CPU -level contention (locks and Arc 's reference counting) and thus is relatively slow. Depending on the implementation, a steady influx
106
Machine Translated by Google
6.7 arc-swap
ArcSwap can be used as an alternative, which solves the above problems and has better performance characteristics in both competitive and non-competitive scenarios.
RwLockÿ
use arc_swap::ArcSwap;
fn main() {
let ArcSwap //
data = ArcSwap::new(1);
//
println!("Initial Value: {}", data.load());
//
data.store(Arc::new(2));
//
println!("New Value: {}", data.load());
}
In this example, ArcSwap contains an integer and is swapped with a new Arc via an atomic store operation . this
Enables multiple threads to safely share and update Arcs.
107
Machine Translated by Google
Machine Translated by Google
7
process
In Rust , you can use the std::process module in the standard library to perform process operations. This module provides functionality for
You can use the std::process::Command structure to create a new process. For example, to run an external command, do this:
use std::process::Command;
fn main() { let
output = Command::new("ls")
.arg("-
This example runs the ls -l command and prints the output of the command.
You can use the wait method to wait for process execution to complete. This will block the current thread until the process completes.
109
Machine Translated by Google
7 processes
use std::process::Command;
fn main() {
let mut child =
You can configure the standard input, standard output, and standard error streams of a process through the stdin, stdout , and stderr
methods.
fn main() {
let output = Command::new("echo") .arg("Hello,
In this example, stdout is configured as a pipe, and we read the output of the process.
You can use the env method to set environment variables for a process.
use std::process::Command;
fn main() {
let output = Command::new("printenv")
.env("MY_VAR",
110
Machine Translated by Google
When it comes to process operations in Rust , you may want to set the process's working directory, that is, have the process perform
operations in a specific local folder. In std::process::Command , you can use the current_dir method to set the working directory.
Here is a simple example showing how to set the working directory of a process in Rust :
use std::process::Command;
fn main() { // let
output = Command::new("ls")
.arg("-
l") .current_dir("/path/to/your/directory") .output() .expect("Failed
to execute
command");
In this example, the current_dir method is used to set the working directory. The process will be executed in the specified folder, not the
current Rust program's working directory.
Make sure to replace /path/to/your/directory with the path to the local folder you actually want to use.
This is useful for ensuring that processes execute in the correct environment, especially when operations that rely on relative paths are
involved.
In Rust , to set the user ID (UID) and group ID (GID) of a process , you can use the uid and gid methods of the std::process::Command
structure .
Here is a simple example showing how to set the UID and GID of a process in Rust :
use std::process::Command;
fn main() { // let
UID GID
output =
UID
GID
Command::new("whoami") .uid(1000) // .gid(1000) // .output()
111
Machine Translated by Google
7 processes
In this example, the uid and gid methods are used to set the UID and GID of the process. You need to replace them
Note that setting UID and GID usually requires privileges, so you may need to run your Rust as administrator
program or make sure your program has sufficient permissions to change the process's identity.
Be sure to use these features with caution as they may involve system-level permissions and security issues.
The code to implement the child process to restore the socket involves the child process parsing the command line arguments and using the nix library's dup2 function
Copy the file descriptor to the correct location. Here is a simple example:
fn main() {
// TCP
//
let (stream, _) = listener.accept().expect("Failed to accept connection");
//
let socket_fd = stream.as_raw_fd();
//
let backup_fd = dup2(socket_fd, 10).expect("Failed to duplicate file descriptor
//
match unsafe { nix::unistd::fork() } {
Ok(ForkResult::Parent { child }) => {
//
close(backup_fd).expect("Failed to close backup file descriptor");
println!("Parent process. Child PID: {}", child);
}
Ok(ForkResult::Child) => {
//
112
Machine Translated by Google
//
close(backup_fd).expect("Failed to close backup file descriptor");
Err(_) =>
{ eprintln!("Fork failed");
}
In this example, use dup2 to copy the socket's file descriptor to the backup file descriptor (here 10 is used as the backup file
descriptor, you can choose an unused file descriptor according to the actual situation). Then create a child process through fork , the
parent process closes the backup file descriptor, and the child process copies the backed-up file descriptor to the socket's file
descriptor location through dup2 .
In Rust , you can use the std::process::Child type to control child processes. The Child type is returned by the spawn method of
std::process::Command . It provides some methods to interact with the child process, wait for its end, and send signals.
use std::process::Command;
fn main() {
let mut child = Command::new("echo") .arg("Hello,
Rust!") .spawn() .expect("Failed
to start
command");
113
Machine Translated by Google
7 processes
fn main() {
let mut child =
//
child.kill().expect("Failed to send signal");
}
7.8.3 Interacting with child processes through standard input and output
fn main() {
let mut child =
echo "Hello, Rust!" | grep Rust This command will create two child processes, one as a producer and one as a
consumer. The producer process writes data to the pipe, and the consumer process reads data from the pipe.
pub fn pipe() { //
//
114
Machine Translated by Google
//
let consumer = Command::new("grep")
//
let output = String::from_utf8_lossy(&consumer.stdout); println!("Output: {:?}",
output);
}
Of course, you can also execute echo "Hello, Rust!" | grep Rust:
fn main() {
let command = "echo \"Hello, Rust!\" | grep Rust";
In this example, we create a shell command string containing a pipe operation and use Command::new("sh").arg("-
c").arg(command) to execute it. The execution results will be included in output , and we output the standard output of the
child process.
Please note that executing shell commands in this way may involve some security risks, as it allows arbitrary command strings
to be executed in the shell . Make sure your program is not vulnerable to malicious input and handle user input with care.
Stdio::piped() is a member of the std::process::Stdio enumeration, which indicates that a pipe is created when the child
process is created and used for standard input, standard output, or standard error.
Specifically, when you use Command::new("command") to create a child process, you can pass stdin,
115
Machine Translated by Google
7 processes
The stdout and stderr methods specify the standard input, standard output, and standard error of the child process. Stdio::piped() is a way to specify that
fn main() { //
let
mut child = Command::new("echo") .arg("Hello,
//
let mut output = String::new();
child.stdout.unwrap().read_to_string(&mut output).expect("Failed to read from s
In this example, Stdio::piped() is used to create a pipe and connect it to the standard output of the child process. Then, we
read the data from the standard output of the child process.
This is a way of establishing communication between parent and child processes so that the parent process can obtain data from the output of the child
process.
In Unix-like systems, a null device is usually represented as the special file /dev/null, any data written to it is discarded, and any attempt to read from it
In Rust , Stdio::null() can be used to connect standard input, standard output, or standard error to a null device, that is,
ignoring the relevant input or output. This may be useful in certain situations, such as when you want to disable the output or
input of a child process.
fn main() { //
let
mut child = Command::new("echo") .arg("Hello,
Rust!") .stdout(Stdio::null())
116
Machine Translated by Google
//
let status = child.wait().expect("Failed to wait for command"); println!("Command
exited with: {:?}", status);
}
In this example, the child process's standard output is connected to a null device, so the output will be discarded. The parent process waits for the
child process to end and outputs the exit status of the child process.
117
Machine Translated by Google
Machine Translated by Google
8
channelchannel _
A channel in Rust is a mechanism for passing messages between different threads. It mainly has the following
characteristics:
• Channels provide a safe way to pass data between threads. Sending data to a channel does not cause race conditions or deadlocks. =
Channels use Rust 's ownership system to ensure that messages are only received by one recipient. When a value is sent over a
• Channels can be set up to be synchronous or asynchronous. Synchronous channels block the sender when no receiver is ready.
background. • Channels can be bounded or unbounded. Bounded means that the channel has a fixed length buffer, and sending will
block when the buffer fills up. Borderless channels have no such limitation. • Channels are
generic and can pass any data that implements the Send and Sync traits .
Channels are best suited for passing larger data between different threads, or as a thread-safe task distribution mechanism. For situations
where only a small amount of data is passed, an atomic type or Mutex may be more efficient. Channels are widely used in various multi-
threading and concurrency scenarios in Rust . Proper use can greatly simplify the complexities and risks of multi-threaded programming.
8.1 mpsc
Rust 's standard library provides a std::sync::mpsc module for implementing multi-producer, single-consumer channels.
mpsc (multiple producer, single consumer) is a specific type of channel used to deliver messages between multiple senders and a single
• Only one receiver is allowed on the mpsc channel. This simplifies ownership transfer as each message can only be uniquely
Get it once.
119
Machine Translated by Google
8 channels
• Multiple senders can send messages to an mpsc channel simultaneously. The channel automatically handles synchronous concurrent write access
ask.
• The mpsc channel supports both synchronous and asynchronous channels. Synchronous channels need to set
boundaries (buffer size). • Values sent via mpsc must implement the Send trait. This ensures that the type sent is safe between threads
move.
• The receiving end can receive messages by polling or waiting. try_recv will not block, recv will block until there is
Message available.
• After the mpsc channel is closed at the sender, the receiver will receive a None message indicating the end of the channel's life cycle.
bundle.
• mpsc channels are often used to build thread-safe producer-consumer patterns. Multiple producers send through channels
Send a message and a consumer receives and processes it.
• Since there is only a single receiver, the mpsc channel is one of the most efficient channel implementations. Their throughput can reach
Specifically, this module provides message-based communication, specifically defining three types: - Sender -
SyncSender - Receiver
Sender or SyncSender is used to send data to Receiver . Both senders are cloneable (multi- producer ), so multiple threads can send to a receiver (single
There are two types of these channels: - Asynchronous, infinitely buffered channels. The channel function returns a (Sender, Receiver) tuple, in which all
sending is asynchronous (never blocking). This channel has a conceptually infinite buffer. -Synchronized , bounded channels. The sync_channel function
returns a (SyncSender, Receiver) tuple, and the storage for pending messages consists of a fixed-size pre-allocated buffer. All sends are synchronous
and block until buffer space is available. Note that the boundary size can be set to 0, which turns the channel into a "contracted" channel, with each sender
In short, the mpsc module provides different forms of FIFO queue communication mechanisms such as multi-producer single-consumer, asynchronous
and synchronous, infinite buffering and bounded buffering through three types of channels: Sender, SyncSender and Receiver .
fn main() { //
let
(sender, receiver) = mpsc::channel();
//
thread::spawn(move || {
let message = "Hello from the producer!";
sender.send(message).expect("Failed to send message");
});
//
120
Machine Translated by Google
8.1 mpsc
In this example, we use std::sync::mpsc::channel() to create a channel, where the sender is used to send
messages and the receiver is used to receive messages. We start the producer in a new thread, which sends
a message, and the main thread receives and outputs the message as a consumer.
Channels are a thread-safe way to ensure that message passing between multiple threads does not cause problems such as data competition. In practical
applications, you can use channels to achieve data transfer and synchronization between different threads.
The following is an example of implementing a multi-producer, single-consumer model using Rust 's std::sync::mpsc . In this example, multiple threads send
messages to a channel, and a single thread receives these messages from the channel.
fn main() { //
let
(sender, receiver) = mpsc::channel();
//
for i in 0..3 { let tx =
sender.clone(); // thread::spawn(move
|| {
tx.send(i).expect("Failed to send message");
});
}
//
for _ in 0..3 { let
received_message = receiver.recv().expect("Failed to receive message"); println!("Received message:
{}", received_message);
}
The following is a simple example of using synchronization channels for multi-thread synchronization:
121
Machine Translated by Google
8 channels
drop(tx);
// Unbounded receiver waiting for all senders to complete. while let Ok(msg) =
rx.recv() { println!("{msg}");
println!("mpsc_example4 completed");
Note that the sync_channel function here creates a bounded channel with a buffer size of 3. This means that when there are 3 messages in the
channel , the sender will be blocked until a message is received. In this example, we created 3 senders, which will send messages to the channel,
and the main thread serves as the receiver, waiting for all messages to be received before exiting.
If the buffer of the synchronization channel is 0, then there will be strict synchronization between the sender and the receiver, and each send operation
must wait for the receiver to be ready to receive. In this case, the sender and receiver are completely synchronized, and every message needs to be
received immediately.
fn main() { //
let 0
//
thread::spawn(move || { for i in
0..5
{ sender.send(i).expect("Failed to send message"); println!("Sent
message: {}", i);
}
});
//
thread::spawn(move || { for _ in
0..5 { let
received_message = receiver.recv().expect("Failed to receive messag println!("Received message:
{}", received_message);
}
});
//
thread::sleep(std::time::Duration::from_secs(10));
122
Machine Translated by Google
8.2 crossbeam-channel
Since there is mpsc, does it have spmc, that is, single producer and multiple consumers, which is the function of broadcasting?
Does it have mpmc, that is, the function of multiple producers and multiple consumers? Does it have spsc, that is, the function of
single producer and single consumer? The answer is yes, but it is not provided by the standard library, but by third-party libraries,
such as crossbeam-channel. This library provides multiple channel types such as mpsc, spmc, mpmc, bounded, and unbounded ,
which can meet different needs. For spsc, the synchronization channel buffer of zero can meet the needs. Of course, there is also
a special type called oneshot that specifically implements this function.
Next, I will introduce some well-known channel libraries, such as crossbeam-channel, flume, tokio, crossfire,
etc. These libraries are all excellent and can meet different needs.
8.2 crossbeam-channel
crossbeam-channel is a Rust library that provides multi-producer and multi-consumer thread-safe channels.
Main functions and features: - Provides two channels : unbounded and bounded . Unbounded channels can send messages
without limit , and bounded channels can set capacity limits. -Support multiple producers and multiple consumers. Multiple threads
can send or receive messages simultaneously. - Provide select! macro, which can operate on multiple channels at the same time.
Have you ever felt the convenience similar to Go channel ? -Provides abstractions such as Receiver and Sender , and is Usage
-friendly.
Here is a simple example demonstrating how to use crossbeam-channel to communicate between two threads:
fn main() { // let
10
(sender, receiver): (Sender<i32>, Receiver<i32>) = bounded(10);
//
let producer = thread::spawn(move || { for i in 0..10
{ sender.send(i).unwrap();
});
//
let consumer = thread::spawn(move || { for _ in 0..10 { let
data =
receiver.recv().unwrap(); println!("Received: {}", data);
});
123
Machine Translated by Google
8 channels
//
producer.join().unwrap();
consumer.join().unwrap();
}
In this example, we create a bounded channel with a capacity of 10. Then, a producer thread is started, sending numbers 0 to 9 to the
channel. At the same time, a consumer thread is started to receive data from the channel and print it out. Finally, wait for both threads to
complete.
When using an unbounded channel, the capacity of the channel is theoretically unlimited, allowing the sender to send data all the time without
limit. In actual use, this may lead to increased memory consumption, so unbounded channels need to be used with caution.
fn main() { //
let
(sender, receiver): (Sender<i32>, Receiver<i32>) = unbounded();
//
let producer = thread::spawn(move || { for i in 0..
{ sender.send(i).unwrap();
}
});
//
let consumer = thread::spawn(move || { for _ in 0..10
{ let data =
receiver.recv().unwrap(); println!("Received: {}",
data);
}
});
//
producer.join().unwrap();
consumer.join().unwrap();
}
In this example, the producer thread will continuously send increasing numbers to the unbounded channel, while the consumer thread will
only receive the first 10 numbers and print them out. Since the channel is unbounded, the producer thread can send data forever.
The select! macro is a method provided by crossbeam-channel , which is used to monitor events of multiple channels at the same time and execute
124
Machine Translated by Google
8.2 crossbeam-channel
Perform corresponding operations. This is useful for handling multiplexing situations where different logic can be executed based on different
channel events.
Here is a simple example that demonstrates how to use the select! macro to listen to two channels and perform corresponding operations based
on the events:
fn main() { //
let
(sender1, receiver1): (Sender<&str>, Receiver<&str>) = unbounded(); let (sender2, receiver2):
(Sender<&str>, Receiver<&str>) = unbounded();
//
let producer1 = thread::spawn(move || { for i in 0..5
{ sender1.send(&format!
("Channel 1: Message {}", i)).unwrap(); thread::sleep(std::time::Duration::from_millis(200));
});
//
let producer2 = thread::spawn(move || { for i in 0..5
{ sender2.send(&format!
("Channel 2: Message {}", i)).unwrap(); thread::sleep(std::time::Duration::from_millis(300));
});
// select!
let consumer = thread::spawn(move || { for _ in 0..10
{ select!
125
Machine Translated by Google
8 channels
});
//
producer1.join().unwrap();
producer2.join().unwrap();
consumer.join().unwrap();
}
The select! macro is a method provided by crossbeam-channel to listen to events from multiple channels at the same time and
perform corresponding operations. This is useful for handling multiplexing situations where different logic can be executed based on
different channel events.
The following is a simple example that demonstrates how to use the select! macro to listen to two channels and perform corresponding
// Since both operations are initially ready, a random one will be executed. select! { recv(r1) -> msg =>
assert_eq!
(msg, Ok(10)), send(s2, 20) -> res => {
In the crossbeam library, after, at, never and tick are some functions provided by the crossbeam_channel module for timing operations.
They are often used with the select! macro to select between a channel's events and timer events.
For example, after creates a channel that will generate an event after a specified period of time.
fn main() {
let timeout = Duration::from_secs(2);
126
Machine Translated by Google
8.2 crossbeam-channel
loop
{ select!
{ recv(timeout_channel) -> println! _
=> {
("Timeout reached!"); break;
} default => { //
at creates a channel that will generate an event after a specified point in time:
fn main() {
let start_time = Instant::now(); let target_time =
start_time + Duration::from_secs(2); let timeout_channel = at(target_time);
loop
{ select!
{ recv(timeout_channel) -> println! _
=> {
("Timeout reached!"); break;
fn main() { let
never_channel = never();
loop
{ select! {
127
Machine Translated by Google
8 channels
recv(never_channel) -> // _
=> {
unreachable!();
} default => { //
tick creates a timed trigger channel that generates an event every specified time:
fn main() {
let tick_interval = Duration::from_secs(1); let ticker =
tick(tick_interval);
8.3 flume
Flume is an asynchronous lock-free multi-producer multi-consumer (mpmc) channel library in Rust , focusing on providing high-performance
asynchronous communication. It is based on a lock-free design, making concurrent operations more efficient in a multi-threaded environment.
128
Machine Translated by Google
8.3 flume
thread::spawn(move || {
(0..10).for_each(|i| {
tx.send(i).unwrap();
})
});
assert_eq!((0..10).sum::<u32>(), received);
}
In this example, we use flume to create an unbounded channel, the producer sends data to the queue, and the consumer receives data
When using the flume library to create a bounded queue, you can use the bounded function to specify the capacity of the queue. Here
fn main() { //
let 3
//
let producer = thread::spawn(move || { for i in 0..5
{ sender.send(i).unwrap(); println!
("Produced: {}", i);
std::thread::sleep(std::time::Duration::from_millis(200));
}
});
});
129
Machine Translated by Google
8 channels
//
producer.join().unwrap();
consumer.join().unwrap();
}
In this example, we use bounded(3) to create a bounded queue with a capacity of 3. The producer thread sends data in a loop, but due to
the limited capacity of the queue, it can only accept 3 elements, so on the fourth attempt to send, the producer thread will be blocked until
space is available.
This demonstrates the nature of bounded queues in that when capacity limits are reached, producers may be blocked until sufficient
space is available. This is useful for situations where memory usage or traffic is controlled.
It's a pity that flume doesn't implement the select macro, but it provides the Selecor structure. You can listen to events from multiple
channels at the same time and perform corresponding operations based on the events. Here is an example of using the select! macro:
std::thread::spawn(move ||
{ tx0.send(true).unwrap();
tx1.send(42).unwrap();
});
flume::Selector::new()
.recv(&rx0, |b| println!("Received {:?}", b)) .recv(&rx1, |n| println!
("Received {:?}", n)) .wait();
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
tokio::spawn(async move
{ tx.send_async(5).await.unwrap();
});
8.4 async_channel
async_channel is an asynchronous multi-producer multi-consumer channel library for asynchronous Rust programming, in which each
message can only be received by one of all existing consumers.
130
Machine Translated by Google
8.5 futures_channel
There are two types of channels: - Bounded channels with limited capacity. - Unbounded channels with unlimited capacity.
A channel has two ends : Sender and Receiver . Both ends are cloneable and can be shared among multiple threads.
The channel is closed when all Senders or all Receivers are discarded . After the channel is closed, no more messages can be sent,
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
tokio::spawn(async move {
tx.send(5).await.unwrap();
});
8.5 futures_channel
futures_channel is another asynchronous multi-producer multi-consumer channel library for asynchronous programming in Rust . Like
threads, concurrent tasks sometimes need to communicate with each other. This module contains two basic abstractions to implement
it: - oneshot, a way of sending a single value between tasks. - mpsc, a multi-producer single-consumer channel, used to send values
between tasks, similar to the structure of the same name in the standard library.
First, let’s introduce mpsc, which is similar to the standard library, but is asynchronous:
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
tokio::spawn(async move { for _
in 0..3 { let mut tx =
tx.clone(); thread::spawn(move
|| tx.start_send("ok"));
}
drop(tx);
});
// Unbounded receiver waiting for all senders to complete. while let Ok(msg) =
rx.try_next() {
131
Machine Translated by Google
8 channels
println!("{:?}", msg);
}
println!("futures_channel_mpsc_example completed");
});
This code demonstrates how to use the tokio runtime and futures_channel::mpsc to create an asynchronous multi-producer single-
consumer channel, then simulate multiple producers to send messages to the channel through asynchronous tasks, and finally wait for all
Next, we will introduce a new synchronization primitive: oneshot. Oneshot is an asynchronous communication mode, usually used for one-
time, one-way message delivery in asynchronous programming. Its name "oneshot" means that it can only deliver a message once. In
Rust , this communication pattern is typically implemented using tokio::sync::oneshot or futures::channel::oneshot , depending on the
asynchronous runtime used.
Characteristics of oneshot : -Single -shot: Oneshot can only deliver a message once. Once a message is sent, the receiving end consumes the
message and subsequent attempts to send it again will fail. -One -way communication: It is one-way, that is, messages can only be delivered from the
sending end to the receiving end. If bidirectional communication is required, you may need to use other asynchronous communication modes, such as
Oneshot is usually used in the following situations: - Asynchronous task completion notification: In asynchronous programming, one task may need
to wait for another task to complete, and only cares about the task completion message. Oneshot can be used to notify the completion of waiting
tasks. -Asynchronous function returns result: In an asynchronous function, sometimes you need to wait for the asynchronous operation to complete
and obtain the result. oneshot can be used to pass results between asynchronous functions. -Exit notification of asynchronous tasks: When an
asynchronous task exits, you can use oneshot to notify other tasks or threads.
thread::spawn(|| { println!
("THREAD: sleeping zzz...");
thread::sleep(Duration::from_millis(1000)); println!("THREAD:
i'm awake! sending."); sender.send(3).unwrap();
});
futures::executor::block_on(async {
println!("MAIN: waiting for msg..."); println!("MAIN:
got: {:?}", receiver.await)
});
132
Machine Translated by Google
8.6 crossfire
8.6 crossfire
This crate provides a channel for use between async-async or async-blocking code, supporting all directions. The design takes
lock-free implementation into consideration, and the underlying layer is based on crossbeam-channel.
Faster than channel in std or mpsc in tokio , slightly slower than crossbeam itself (because of the asynchronous overhead
required to wake up the sender or receiver).
It provides the functions of mpsc and mpmc . The usage is similar, except that the performance of mpsc is better because there
is only one receiver.
pub fn crossfire_mpsc() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
tokio::spawn(async move { for i in
0i32..10 { let _ =
tx.send(i).await; println!("sent {}",
i);
}
});
loop { if
let Ok(_i) = rx.recv().await { println!("recv {}",
_i); } else { println!("rx closed");
break;
});
}
Note that it is executed asynchronously, here we use the asynchronous runtime tokio.
pub fn crossfire_mpmc() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
133
Machine Translated by Google
8 channels
});
sender_handles.push(handle);
}
rx.recv().await {
println!("thread {} recv {}", i, _i); } else { println!("rx
closed");
break;
});
handles.push(handle);
}
} drop(tx);
});
}
134
Machine Translated by Google
8.7 channels
8.7 channels
The kanal library is a Rust implementation of the CSP (Communicating Sequential Process) model channel , designed to help programmers create efficient
concurrent programs. The library provides multi-producer multi-consumer channels with advanced features and lock-free one-time channels that only
allocate pointer sizes on the heap, allowing fast communication. This library strives to unify messaging between synchronous and asynchronous parts of
Rust code through a combination of synchronous and asynchronous APIs , while maintaining high performance.
Why is canal faster? 1. Kanal uses highly optimized combination technology to transfer objects. When the data size is less than or equal to the pointer size,
it uses serialization to encode the data into a pointer address. Conversely, when the data size exceeds the pointer size, the protocol adopts a strategy
similar to that used by the Golang programming language, using direct memory access to copy the object from the sender's stack or write directly to the
receiver's stack. This combined approach not only eliminates unnecessary pointer accesses but also eliminates heap allocations for bounded (0) channels.
2. Kanal uses a specially tuned mutex for its channel locking mechanism, which benefits from the predictable internal locking time of the channel. In other
words, you can use the Rust standard mutex lock and std-mutex features, and Kanal 's performance will be better than its competitors under this feature .
3. Take advantage of Rust's high-performance compiler and powerful LLVM backend for highly optimized memory access and well-thought-out algorithms.
kanal refers to the channel of the Go language . You can try kanal based on the following reasons : - kanal communication is fast and efficient - kanal can
communicate through synchronous and asynchronous, and can even convert between synchronous and asynchronous by converting the sender/receiver
Convert to other APIs. -Compared with other Rust libraries, kanal provides a clearer API. - Similar to Golang, you have access to the Close function and
can broadcast a close signal from any instance of the channel to close both ends of the channel. - kanal provides both high-performance MPMC channels
thread::spawn(move || {
(0..10).for_each(|i| {
tx.send(i).unwrap();
});
drop(tx)
});
This code demonstrates using kanal to create an asynchronous channel, send numbers in a new thread, then receive these numbers in the main thread
and calculate their sum, and finally output the sum. This is a typical multi-producer and multi-consumer scenario, in which threads communicate through
kanal channels.
let rt = tokio::runtime::Runtime::new().unwrap();
135
Machine Translated by Google
8 channels
rt.block_on(async move {
tokio::spawn(async move {
tx.send(5).await.unwrap();
});
Kanal provides unbounded and bounded channels, and corresponding synchronous and asynchronous channels. You can
At the same time, kanal also provides the oneshot function and its asynchronous version (oneshot_async). The following is an
example of a synchronous version:
thread::spawn(move ||
{ tx.send(5).unwrap();
});
136
Machine Translated by Google
9
timer
Timer and Ticker are both tools used to handle time-related tasks, usually used in programming. They may have different
implementations in different programming languages and frameworks, I will briefly introduce their general concepts.
Ticker (timer) is usually used to repeatedly execute tasks at fixed intervals, similar to timers, but more focused on
repeated execution.
9.1 Timer
In Rust , you can create a simple timer using std::thread::sleep from the standard library . This method allows
you to pause execution in the current thread for a period of time. Here is a simple example:
fn main()
{ println!(" ");
// 5
thread::sleep(Duration::from_secs(5));
println!("5 ");
}
In the above example, Duration::from_secs(5) means waiting for 5 seconds. This method blocks the execution of the
current thread and implements a simple timer effect.
137
Machine Translated by Google
9 timers
If you need a more complex timer, consider using a third-party library such as tokio. Tokio is a library for asynchronous
programming that provides more powerful timer functionality. Here is an example using tokio :
// 5
sleep(Duration::from_secs(5)).await;
println!("5 ");
}
#[tokio::main]
async fn main()
{ my_timer().await;
}
In this example, tokio::time::sleep returns a Future, which can be used with async/await to achieve
asynchronous timer effects.
usae timer;
use chrono;
use std::sync::mpsc::channel;
timer.schedule_with_delay(chrono::Duration::seconds(3), move || {
tx.send(()).unwrap(); });
rx.recv().unwrap(); println!
("This code has been executed after 3 seconds");
This Rust code example uses timer and chrono crates to implement a simple scheduled task.
The main steps are: 1. Create a timer::Timer instance for subsequent timing operations. 2. Use std::sync::mpsc::channel to
create a channel. This channel is used for communication between threads. 3. Call the schedule_with_delay method on the
timer to schedule a scheduled task. 4. Scheduled tasks
138
Machine Translated by Google
9.1 Timer
It is a closure and will be executed after 3 seconds. In the closure , send a message to the tx end. 5. The main thread receives messages through the rx
end. recv will block until the message is received. After the message is received, a printout indicates that the code has been executed.
The entire process uses Rust 's channels and timers to create a 3 -second delay task. The timer will execute closure in another thread and send messages
This is a simple example of multi-threading working with scheduled tasks. timer provides flexible scheduled task scheduling functions , chrono provides
time and date operation capabilities, and std::sync::mpsc::channel provides inter-thread communication capabilities. Timing tasks can be easily implemented
In the current implementation, each timer is executed as two threads. The scheduler thread is responsible for maintaining the queue of callbacks to be
executed and actually executing them. The communication thread is responsible for communicating with the scheduler thread (which requires acquiring a
mutex lock that may be held for a long time) without blocking the calling thread.
let _guard =
timer.schedule_with_date(Local::now().add(chrono::Duration::seconds(1)), move |
let _ignored = tx.send(()); // Avoid unwrapping here.
});
rx.recv().unwrap(); println!
("This code has been executed after 1 seconds");
}
This example demonstrates executing the closure task 1 second after the current time .
use timer;
use chrono;
use std::thread; use
std::sync::{Arc, Mutex};
139
Machine Translated by Google
9 timers
*count.lock().unwrap() += 1; }) };
// Now drop the guard. This should stop the timer. drop(guard);
thread::sleep(std::time::Duration::new(0, 100));
This example increments counter every 5 millimeters . After 1 second, the value of counter should be between 190 and 210 . Then
stop the timer, and after another 1 second, the value of counter should remain unchanged.
9.1.2 futures_timer
futures-timer is a library for working with timers. The following is a simple example that demonstrates how
to use futures-timer to create a recurring scheduled task:
smol::block_on(async { for _ in
0..5
{ Delay::new(Duration::from_secs(1)).await; println!(" ");
});
}
In this example, Delay::new(Duration::from_secs(1)) creates a timer that fires every second. Then, wait
and trigger the timer through Delay::await to achieve the effect of repeated execution. The above
example will be executed five times.
140
Machine Translated by Google
9.1 Timer
Timer::after(Duration::from_secs(1)).await;
.map(|timeout|
Timer::after(timeout)) .unwrap_or_else(Timer::never);
run_lengthy_operation().or(timer).await;
}
Timer::after(Duration::from_secs(1)).await;
141
Machine Translated by Google
9 timers
9.1.4 of such
A dedicated chapter ( Chapter 13 ) introduces the concurrency capabilities provided by the tokio library.
9.1.5 smol::Timer
Timed event generator. Since it can be used as a Timer, it can also be used as a Ticker.
The following is a simple example that demonstrates how to use smol::Timer to create a scheduled task:
fn main() {
let timer = Timer::after(Duration::from_secs(1));
smol::block_on(async
{ timer.await;
println!(" ");
});
}
9.1.6 async-timer
9.1.7 timer kit
9.1.8 hierarchical_hash_wheel_timer
142
Machine Translated by Google
9.2 ticker
9.2 ticker
Async-io 's Timer provides interval- related methods to implement periodic triggering tasks. The schedule_repeating function of the timer
library also provides similar functions. There are also specialized libraries such as ticker that do this.
9.2.1 ticker
The ticker library is a timer library used to handle periodic tasks. It allows you to create a timer, which looks like a current
limit for the iterator. The timer will be triggered every iteration.
Let's look at a simple example using the Ticker library. Suppose you have a task that needs to be executed every second. You can use
In this example, we create a Ticker that executes once every second for a total of 10 times. At each execution, we print out the current
number of iterations.
If you want to trigger forever, you can use the following method:
In this example, we create a Ticker that executes once every second and countless times. At each execution, we print out the current
number of iterations.
9.2.2 tokio::time::interval
Introduced in the chapter tokio
143
Machine Translated by Google
Machine Translated by Google
10
parking_lot concurrent library
This library provides smaller, faster, and more flexible implementations of Mutex, RwLock, Condvar , and Once than those in the Rust standard
library, as well as a recursive lock ReentrantMutex. It also exposes a low-level API for creating your own efficient synchronization primitives.
When testing parking_lot::Mutex on x86_64 Linux , it was found to be 1.5 times faster than std::sync::Mutex without contention , and up to 5
times faster with contention from multiple threads . The performance of RwLock will vary depending on the number of read and write threads,
but is almost always faster than the standard library's RwLock , in some cases even 50 times faster.
Function
The primitives provided by this library have several advantages over the primitives in the Rust standard library:
1. Mutex and Once only require 1 byte of storage space, while Condvar and RwLock only require 1 word of storage space. On the other
hand, standard library primitives require a dynamically allocated Box to hold operating system-specific synchronization primitives.
This library's Mutex is particularly small, which facilitates the use of fine-grained locks to improve parallelism. 2. Since they consist of
only one atomic variable, have constant initialization and do not require a destructor, these primitives can be used as static global variables.
Standard library primitives require dynamic initialization, so lazy_static! lazy initialization is required .
3. Contention-free lock acquisition and release are completed through fast inline paths, and they require only one atomic operation. 4.
Microcontention (contention for locks with short critical sections) is handled efficiently by several attempts (spins) to acquire the lock.
5. These locks are adaptive and will hang the thread after several failed spin attempts. This makes the lock suitable for long and short critical
145
Machine Translated by Google
9. Ensure that Condvar will not generate false wake-ups. The thread will only be awakened when it times out or is awakened by a notification .
10. Condvar::notify_all will only wake up one thread and requeue the remaining threads to wait for the relevant Mutex. This avoids the crowd effect
problem caused by all threads trying to acquire the lock at the same time.
experimental deadlock detector effective for Mutex, RwLock and ReentrantMutex . This function defaults to
17. RwLock supports atomically upgrading "upgradable" read locks to write locks. 18. Optional
serde support. Enabled via the serde feature. Note! This support is only available for Mutex,
ReentrantMutex and RwLock, Condvar and Once are currently not supported .
19. When the send_guard feature is enabled , lock guards can be sent to other threads.
To keep these primitives lean, all thread queuing and pausing functionality has been moved to parking_lot. The idea is based on Webkit 's
WTF::ParkingLot class, which essentially consists of a hash table mapping of lock addresses to parked (sleeping) thread queues. Webkit 's parking lot
is actually inspired by Linux futex , but is more powerful than futex because it allows callbacks to be called while the queue lock is held.
In this way, the primitive itself contains only an atomic variable, while thread queuing and park operations are delegated to the park-ing_lot module.
This allows primitives to be very lean and fast, but still allows for advanced features like timeouts and fairness . The parking_lot module can be
Next, let us introduce the various concurrency primitives of the parking_lot module, because it has many similarities with the functions of the
concurrency primitives of the standard library, so it is easier for you to understand and learn.
10.0.1 Mutex
Mutex primitives for protecting shared data
This mutex blocks threads waiting for the lock. Mutexes can be created through static initialization or using the new constructor. Each mutex has a
type parameter that represents the data it protects. Data can only be accessed through the RAII protection returned by lock and try_lock , which
ensures that the data can only be accessed when the mutex is locked.
fairness
A typical unfair lock usually occurs when a single thread acquires and releases the same mutex in rapid succession, starving other threads waiting to
acquire the mutex. Although this approach improves throughput (because it does not force a context switch when the thread tries to reacquire the
This mutex uses eventual fairness to ensure that the lock is fair in the long term without sacrificing throughput. This is achieved by forcing a fair unlock
every 0.5 milliseconds on average, which forces the lock to go to the next line waiting on the mutex
146
Machine Translated by Google
Moderate.
Additionally, any critical section longer than 1 millisecond always uses fair unlocking, which has a negligible impact on throughput given the
When unlocking a mutex, you can also force a fair unlock by calling MutexGuard::unlock_fair instead of simply discarding the MutexGuard.
The following is an example of using Mutex : using ten threads, each thread adds 1 to the shared data, and finally prints the result.
} //
});
}
rx.recv().unwrap();
147
Machine Translated by Google
match mutex.try_lock()
{ Some(_guard) => println!("thread {} got the lock", i), None => println!("thread
{} did not get the lock", i),
}
})
}) .collect();
println!("mutex_example3: done");
In fact , Mutex is the type definition of parking_lot::Mutex<RawMutex, T> . parking_lot::Mutex provides general
lock methods, such as try_lock_for, try_lock_until and other request lock methods with timeout functions.
use parking_lot::Mutex;
// mutex
mutex.force_unlock(); // mutex
force_unlock can force unlock the Mutex without the guard holding the Mutex . This is generally not recommended
as it may cause data races, but is desirable in some special cases.
This is useful when used in combination with mem::forget to hold the lock without maintaining an active
MutexGuard object, for example when dealing with FFI . This method can only be called when the current thread
logically owns a MutexGuard, but the guard has been discarded through mem::forget . If the mutex is unlocked
while it is unlocked, the behavior is undefined.
let mem::forget //
_guard = mem::forget(mutex.lock());
// mutex
148
Machine Translated by Google
// mutex
unsafe
{ mutex.force_unlock();
}
10.0.2 FairMutex
An always fair mutex primitive that can be used to protect shared data.
This mutex blocks threads waiting for the lock. Mutexes can be created through static initialization or using the new constructor. Each mutex has a
type parameter that represents the data it protects. Data can only be accessed through the RAII protection returned by lock and try_lock , which
ensures that the data can only be accessed when the mutex is locked.
The ordinary mutex provided by parking_lot uses eventual fairness (which defaults to the fair algorithm after a period of time), but eventual fairness
does not provide the same guarantees that the always-fair approach would. Fair mutexes are generally slower, but are sometimes needed.
In a fair mutex, waiters form a queue and the lock is always granted to the next requestor in the queue in first-in, first-out order. This ensures that one
Fair mutexes may not be very interesting if threads have different priorities (this is called priority inversion).
So you can see that the difference between it and Mutex is that it sacrifices a little efficiency to ensure fairness.
} //
});
}
rx.recv().unwrap();
Its usage is similar to Mutex , because they are both implemented by the type definition of parking_lot::FairMutex<RawMutex, T> .
149
Machine Translated by Google
10.0.3 RwLock
Read-write lock types allow multiple readers or at most one writer at any point in time . Write locks usually allow modification of the underlying
data (exclusive access), and read locks usually allow read-only access (shared access).
This lock uses a task-fair locking strategy that prevents reader and writer starvation. This means that even if the lock is unlocked , readers trying
to acquire the lock will be blocked if there are writers waiting to acquire the lock. Therefore, recursively acquiring read locks in a single thread
The type parameter T represents the data protected by this lock. T needs to satisfy Send to be shared between threads and Sync to allow
concurrent access through readers. The RAII guard returned by the lock method implements Deref (and DerefMut for the write method ) to allow
Fairness A typical unfair lock usually occurs when a single thread acquires and releases the same read-write lock in rapid succession, which
starves other threads waiting to acquire the read-write lock. Although this approach improves throughput (because it does not force a context
switch when the thread tries to reacquire the read-write lock that was just released), it may starve other threads.
This read-write lock uses eventual fairness to ensure that the lock is fair in the long run without sacrificing throughput. This is achieved by forcing
a fair unlock on average every 0.5 milliseconds, which forces the lock to pass to the next thread waiting for the read-write lock.
Additionally, any critical section longer than 1 millisecond always uses fair unlocking, which has a negligible impact on throughput given the
When unlocking a read-write lock , you can also force a fair unlock by calling RwLockReadGuard::unlock_fair or RwLockWriteGuard::unlock_fair
Here is an example of using RwLock : using ten threads, the even-numbered threads add 1 to the shared data, and the odd-numbered threads
})
})
150
Machine Translated by Google
.collect();
try_read contains a set of methods for trying to acquire read locks, such as try_read, try_read_for,
try_read_until, try_read_recursive, try_read_recursive_for, try_read_recursive_until, try_upgradable_read,
try_upgradable_read_for, try_upgradable_read_until. I don’t want to introduce the differences between
these methods in detail here. They always provide more Refined operation, the behavior of acquiring
a read lock when there is a reader or writer , and the corresponding read_recursive and upgradable_read
methods.
At the same time, it also provides methods to force lock release: force_unlock_read,
force_unlock_read_fair, force_unlock_write, force_unlock_write_fair, which are generally used in
conjunction with mem::forget .
Well, you may not know much about mem::forget , let’s briefly introduce it.
1. When a value type implements the Drop trait , Rust will automatically call its destructor when the value leaves the
scope. But sometimes you need to leave the scope early and don't want to trigger the destructor. In this case, you
can use mem::forget.
2. When holding a RAII protector (such as MutexGuard) , the lock will normally be released when leaving the scope. But if you need to exit the
scope early without releasing the lock, you can use mem::forget to discard the protector and keep the lock. 3. Move the value to another
value still needs to remain valid. At this time, you can use mem::forget to avoid triggering destruction when moving. 4. In some special concurrency
controlled. mem::forget can prevent destructors from affecting this fine-grained control.
When using mem::forget, please note that the value being forgotten is not put into the heap and will be leaked directly. At the same time, you need to
ensure that this discarded value will not be used again later.
In summary, mem::forget is suitable for rare cases where more fine-grained control over value lifetime and destruction is required. Not recommended
10.0.4 ReentrantMutex
Reentrant mutex primitive allows the same thread to acquire the lock multiple times.
This type is the same as a Mutex , except for the following: - Multiple locks from the same thread will work properly instead of dying
151
Machine Translated by Google
Lock. - ReentrantMutexGuard does not give mutable references to locked data. If you need a mutable reference, use RefCell.
From its definition, you can see that it will determine whether to reentry based on the thread ID :
pub fn reentrantmutex_example() {
let lock = ReentrantMutex::new(());
reentrant(&lock, 10);
println!("reentrantMutex_example: done");
}
By the way, const_fair_mutex, const_mutex, const_reentrant_mutex, and const_rwlock are convenience functions for creating
const locks.
10.0.5 Once
This is a synchronization primitive that can be used to run one-time initialization. Useful for one-time initialization of global
variables, FFIs, or related functions.
});
VAL
}
152
Machine Translated by Google
10.0.6 Condvar
Condition variables represent the ability to block a thread so that it does not consume CPU time while waiting for an event to occur . Condition
variables are usually associated with conditions (boolean predicates) and mutexes. The condition is always verified inside the mutex before it is
});
Note that we first acquired the lock in one thread, changed the condition (started=true), called the notify_one method,
and then acquired the lock again in another thread. This is allowed because Condvar 's wait method will release the
lock, and then Acquire the lock again after waking up.
notify_all will wake up all waiting threads, while notify_one will only wake up one waiting thread.
There are several variants of wait , providing more refined methods: wait_until, wait_while,
wait_while_for, wait_while_until.
153
Machine Translated by Google
Machine Translated by Google
11
crossbeam concurrency library
Crossbeam is an indispensable library for multi-threaded concurrent programming in rust . It abstracts many common patterns in
concurrent scenarios and is very simple and convenient to use. Many concurrent codes in Rust will use the primitives provided by
crossbeam . Therefore, as a rust developer, it is necessary to master the usage of crossbeam .
• Atomic
Pick. (no_std) •
Data structure
– SegQueue, an unbounded MPMC queue that allocates small buffers (segments) on demand. (alloc) •
Memory
management – epoch, epoch-based garbage collector. (alloc)
• Thread
synchronization – channel, a multi-producer multi-consumer channel for message delivery.
155
Machine Translated by Google
– scope, used to spawn threads that borrow local variables from the stack.
crossbeam-skiplist implements concurrent map and set based on lock-free skip list, which is still experimental
at present.
AtomicCell is an atomic data structure, which ensures that read and write operations on internal data are atomic.
• AtomicCell internally encapsulates a variable of generic T , which can only be read and written through a given method.
Variables cannot directly access the
internal value. • You need to specify a generic T when creating , such as let cell = AtomicCell::new(10);
• Provide load and store methods to atomically read and write its internal
values. • Methods such as fetch_add and fetch_sub can read and write internal values atomically, and they will
return the old value. • The clone method can create a new AtomicCell with the same internal values. Can be used in
multiple threads. • The from and into methods can convert from an ordinary T type to AtomicCell.
• Implements the Sync and Send traits so they can be safely shared between threads.
pub fn atomic_cell_example() {
let a = AtomicCell::new(0i32);
a.store(1);
assert_eq!(a.load(), 1);
use crossbeam::atomic::AtomicCell;
156
Machine Translated by Google
// count
for _ in 0..10 { let
atomic_count = atomic_count.clone();
thread::spawn(move || {
let c = atomic_count.fetch_add(1); println!
("Incremented count to {}", c + 1); });
//
while atomic_count.load() < 10 {} println!("Final
count is {}", atomic_count.load());
The above example creates an AtomicCell and initializes the count to 0. Then perform the self-increment operation
fetch_add(1) on it concurrently in 10 threads . Each thread reads the current value, increments it by 1 and then writes it.
Due to the use of AtomicCell, the entire process is atomic and there will be no data competition. The final output count
value of the main thread is confirmed to be 10.
AtomicCell provides a multi-thread-safe atomic data container that avoids explicit locking and synchronization.
These data structures are most commonly used in work-stealing schedulers. A typical setup involves multiple threads,
each with its own FIFO or LIFO queue (worker ). There is also a global FIFO queue (Injector) and a list of references to
worker queues that can steal tasks (Stealer).
We spawn a new task into the scheduler by pushing the task to the injector queue. Each worker thread waits in a loop
until it finds the next task to run, and then runs it. To find a task, it first looks at its local worker queue, then at injectors
and stealers.
Suppose a thread in the work-stealing scheduler is idle, looking for the next task to run. To find an available task, it might
do the following: - Try to pop a task from the local worker queue. -Try to steal a batch of tasks from the global injector
queue. -Try to steal a task from another thread using the stealer list.
157
Machine Translated by Google
fn find_task<T>(
local: &Worker<T>,
global: &Injector<T>,
stealers: &[Stealer<T>],
) -> Option<T> {
//
local.pop().or_else(|| {
//
iter::repeat_with(|| {
//
global.steal_batch_and_pop(local)
// .find(|s| !s.is_retry())
// .and_then(|s| s.success())
})
}
Assuming you have a scheduler with corresponding Workers, Injectors , and Stealers , you can call
find_task function to find the next task to run. Here is a simple example:
fn main() {
//
let local_worker = Worker::new_fifo();
//
let global_injector = Injector::new();
//
let stealer1 = local_worker.stealer();
let stealer2 = local_worker.stealer();
//
let stealers = vec![stealer1, stealer2];
// find_task
if let Some(task) = find_task(&local_worker, &global_injector, &stealers) {
println!("Found task: {:?}", task);
} else {
println!("No task found.");
158
Machine Translated by Google
11.2.2 ArrayQueue
The queue allocates a fixed-capacity buffer when constructed to store pushed elements. The queue cannot hold more elements than the buffer allows.
Attempts to push elements to a queue that is full will fail. Alternatively, force_push enables the queue to be used as a ring buffer. Pre-allocating buffers
use crossbeam::queue::ArrayQueue;
//
for i in 0..10 { let
queue = queue.clone();
thread::spawn(move|| {
queue.push(i).unwrap(); });
//
for _ in 0..10 { let
queue = queue.clone();
thread::spawn(move|| { while
let Ok(item) = queue.pop() { println!("Consumed
{}", item);
} });
}
11.2.3 SegQueue
Unbounded multi-producer multi-consumer queue.
The queue is implemented as a linked list of segments, where each segment is a small buffer that can hold a small number of elements. There is no
limit on how many elements can be in the queue at the same time. However, this queue is slightly slower than an ArrayQueue due to the need to
The following is an example of using SegQueue to demonstrate multiple producers and multiple consumers:
159
Machine Translated by Google
fn main() { //
SegQueue
let seg_queue = SegQueue::new();
//
let producer = thread::spawn(move || { for i in 0..5
{ seg_queue.push(i);
println!("Produced: {}", i);
});
//
let consumer = thread::spawn(move || { for _ in 0..5 { //
let value =
seg_queue.pop(); println!("Consumed:
{:?}", value);
}
});
//
producer.join().unwrap();
consumer.join().unwrap();
}
In this example, we create a SegQueue and then start a producer thread and a consumer thread. The producer thread pushes numbers to the queue,
and the consumer thread pops the numbers from the queue and prints them. Since SegQueue is an unbounded queue, an unlimited number of
There are very few usage scenarios and will be ignored for now. This module mainly provides a garbage collection capability.
11.4.1 channel
Multi-producer multi-consumer channels for message delivery.
This crate is a replacement for std::sync::mpsc with more features and better performance.
Of course, the current std::sync::mpsc has also been replaced with the crossbeam channel . crossbeam _
160
Machine Translated by Google
Channels can be created using two functions: - bounded creates a channel with limited capacity, i.e. the number of consumers it can accommodate at one time
The number of coupons is limited. - unbounded creates a channel with unlimited capacity, i.e. it can accommodate any number of messages simultaneously.
Both functions return a Sender and a Receiver, which represent the two opposite ends of the channel.
point.
use crossbeam_channel::bounded;
// 5
// 5
for i in 0..5 {
s.send(i).unwrap();
}
`send`
// // s.send(5).unwrap();
This code uses the bounded function from the crossbeam_channel crate to create a bounded volume.
Quantity channel. In this example, the channel can hold up to 5 messages at the same time. By looping, we use
s.send(i).unwrap() sends five messages from 0 to 4 to the channel. Since the capacity of the channel is 5, send these
Messages do not cause blocking. Then, the commented out s.send(5).unwrap() indicates that calling send again will try
Send a new message (number 5). But since the channel is full, this operation blocks until there is room to accommodate
New news.
use crossbeam_channel::unbounded;
//
let (s, r) = unbounded();
//
for i in 0..1000 {
s.send(i).unwrap();
}
A special case is a zero-capacity channel, which cannot hold any messages. Instead, to pair and deliver messages, send and receive
use std::thread;
use crossbeam_channel::bounded;
161
Machine Translated by Google
//
let (s, r) = bounded(0);
//
thread::spawn(move || s.send("Hi!").unwrap());
//
assert_eq!(r.recv(), Ok("Hi!"));
This code demonstrates creating a zero-capacity channel using the bounded function from the crossbeam_channel crate . In a
zero-capacity channel, send and receive operations must occur simultaneously. First, a new thread is created through
thread::spawn , which calls s.send("Hi!").unwrap() to try to send the message "Hi!'' to the channel. Since the channel capacity is
zero, the send operation will Block until a receiving operation occurs at the other end. Then, the main thread calls r.recv() to try to
receive messages from the channel. Because there is a sending operation at the other end, the receiving operation will succeed
and the return result is Ok ("Hi!").
// ,
thread::spawn(move ||
{ r2.recv().unwrap();
s2.send(2).unwrap(); });
//
s1.send(1).unwrap();
r1.recv().unwrap();
This code shows several uses of crossbeam channel : - Create a bounded channel (s1, r1) with a capacity of 0 - Use clone() to
clone the sender and receiver to get (s2, r2) - In a new thread, pass r2 receives the message, and sends the message through
s2 - in the main thread, sends the message through s1 , and receives the message through r1
This achieves message passing between two threads. The main thread sends first, the new thread recvs and then sends, and
the main thread recvs last .
Crossbeam 's channel is very suitable for simple message passing and communication between threads. This example shows
the basic way of passing messages in a thread.
When all senders or receivers associated with a channel are dropped , the channel will become dis-connected . At this time, no
more messages can be sent to the channel , but the remaining remaining messages can still be received.
162
Machine Translated by Google
news. Send and receive operations on disconnected channels will not be blocked .
//
let (s, r) = unbounded();
//
s.send(1).unwrap();
s.send(2).unwrap();
s.send(3).unwrap();
//
drop(s);
//
assert_eq!(r.recv(), Ok(1));
assert_eq!(r.recv(), Ok(2));
assert_eq!(r.recv(), Ok(3));
//
assert!(r.is_empty());
// `r.recv()` // `Err(RecvError)`
assert_eq!(r.recv(), Err(RecvError));
This code uses the unbounded function from the crossbeam_channel crate to create an infinite capacity
channel, where s is the sending end and r is the receiving end. Through s.send(1).unwrap();, s.send(2).unwrap();
and s.send(3).unwrap(); sends three messages to the channel. Then, the only send is discarded via drop(s);
end, which causes the channel to be disconnected. Then, the remaining messages, which are 1, 2 and 3, can be received through r.recv() . Next, pass
Confirm that there are no more messages in the channel through assert!(r.is_empty()) ;. Finally, through assert_eq!(r.recv(),
Err(RecvError)); Verify that calling r.recv() does not block, but returns Err(RecvError) immediately. This is because
Because the channel has been disconnected, no more messages can be received.
The receiving end can be used as an iterator. For example, the iter method creates an iterator that receives messages until
until the channel becomes empty and disconnected. Note that the iteration may block, waiting for the next message to arrive:
use std::thread;
use crossbeam_channel::unbounded;
thread::spawn(move || {
s.send(1).unwrap();
163
Machine Translated by Google
s.send(2).unwrap();
s.send(3).unwrap();
drop(s); //
});
`collect`
// // let v: Vec<_> = r.iter().collect();
try_recv will not block, recv may or may not be blocked (disconnected channel). try_iter does not block and will only return the
elements in the current channel .
The select! macro supports select syntax similar to Go , which can monitor multiple channels at the same time and select
one of the channels to receive messages.
thread::spawn(move || s1.send(10).unwrap());
thread::spawn(move || s2.send(20).unwrap());
// recv
select!
{ recv(r1) -> msg => assert_eq!(msg, Ok(10)), recv(r2) ->
msg => assert_eq!(msg, Ok(20)), default(Duration::from_secs(1))
=> println!("timed out"),
}
If no channel is ready to receive a message, the select! macro will block until one of the channels is ready to receive a
message. If no channel is ready to receive messages, but a default branch is provided, the select! macro will execute the
default branch. If no channels are ready to receive messages and no default branch is provided, the select! macro will block
until one of the channels is ready to receive messages.
If you need to make a selection from a dynamically created list of channel operations, use the Select function. The select!
macro is just a convenience wrapper for Select :
164
Machine Translated by Google
//
let oper = sel.select(); let index =
oper.index(); oper.recv(&rs[index])
Crossbeam also has three special channels built in, all of which only return a receiver handle: - after: Create a channel
that delivers a single message after a certain period of time. - tick: Create a channel that delivers messages periodically.
- never: Create a channel that never delivers messages
loop
{ select!
{ recv(ticker) -> _
=> println!("elapsed: {:?}", start.elapsed()),
recv(timeout) -> _ => break,
}
11.4.2 Parking
In Rust 's concurrent programming, Parking is a thread waiting mechanism that allows threads to wait for a certain condition to be met before
continuing to run.
Conceptually, each Parker is associated with a token, which is initially unavailable: - The park method blocks the current thread unless or until the
token is available, at which time it will automatically consume the token. - The park_timeout and park_deadline methods work the same as park , but
will block after the specified maximum time. - The unpark method atomically makes the token available if it was not available before. Because the
token is initially unavailable, unpark followed by park will cause park to return immediately. In other words, each Parker is similar to a spin lock and
Parker provides a primitive way of thread waiting and waking up, which can be used to implement more advanced synchronization primitives such as
Mutex. It can pause threads to avoid taking up CPU time, and is suitable for multi-thread scenarios that require waiting and synchronization.
use std::thread;
165
Machine Translated by Google
//
u.unpark(); //
p.park();
thread::spawn(move || {
thread::sleep(Duration::from_millis(500)); u.unpark();
});
`u.unpark()` //
p.park();
11.4.3 ShardedLock
A sharded read-write lock. This lock is similar to RwLock, but reads are faster and writes are slower.
ShardedLock internally consists of a series of shards, each shard is a RwLock and occupies a separate cache line. A read operation
will select one of the shards based on the current thread and lock it. Write operations require locking all shards in sequence. By
splitting locks into shards, in most cases concurrent read operations will select different shards and thus update different cache
lines, which is good for scalability. However, write operations require more work and are therefore slower than usual. The lock priority
policy depends on the underlying operating system implementation, and this type does not guarantee that any specific policy will be used.
use crossbeam_utils::sync::ShardedLock;
//
{
let r1 = lock.read().unwrap(); let r2 =
lock.read().unwrap(); assert_eq!(*r1, 5);
assert_eq!(*r2, 5); } //
//
{
166
Machine Translated by Google
11.5 Utilities
11.4.4 WaitGroup
There is the concept of WaitGroup in Go language , the concept of CountdownLatch in Java , and the concept of
std::latch in C++ . These are all the same synchronization primitives.
There is no such synchronization primitive in the Rust standard library, but it is provided in crossbeam. Of course, there are also third-party libraries
WaitGroup is very similar to Barrier , with some differences: - Barrier needs to know the number of threads at construction
time, while WaitGroup can be cloned to register more threads. - Barrier can be reused after all threads are synchronized,
while WaitGroup only synchronizes threads once. -All threads wait for other threads to arrive at the Barrier. With
WaitGroup, each thread can choose to wait for other threads, or it can choose to continue without blocking.
//
let wg = WaitGroup::new();
wg = wg.clone();
thread::spawn(move || { //
//
drop(wg);
});
}
//
wg.wait();
11.5 Utilities
167
Machine Translated by Google
11.5.1 Backoff
Execute the exponential quenching algorithm (backoff) in a spin loop .
Performing backoff operations within a spin loop can reduce contention and improve overall performance.
This basic operation can execute the CPU 's YIELD and PAUSE instructions, yield the current thread to the operating system scheduler, and tell when it is a
good time to block the thread using a different synchronization mechanism. Each step in the backoff process is roughly twice as long as the previous step.
} backoff.spin();
}
This code uses Backoff from the crossbeam_utils crate to perform spin backoff, attempting to perform a multiplication in an atomic operation. In the loop,
you first load the atomic value and then use the compare_exchange method to try to atomically multiply it by b. If the atomic operation is successful, the
snooze backs off in blocking loops. This method should be used when we need to wait for another thread to make progress. The processor may yield using
a YIELD or PAUSE instruction, and the current thread may yield by giving up its time slice to the operating system scheduler. If possible, use is_completed
to check when it is recommended to stop using backoff and block the current thread using a different synchronization mechanism:
168
Machine Translated by Google
11.5 Utilities
Note that callers setting ready to true should use unpark() to wake up dormant threads.
11.5.2 CachePadded
In concurrent programming, it is sometimes desirable to ensure that frequently accessed pieces of data are not placed in the same cache
line. Updating an atomic value invalidates the entire cache line it belongs to, which causes the next access to a value in the same cache
line from other CPU cores to be slower. Using CachePadded ensures that updating one piece of data does not invalidate other cached
data.
The cache line length is assumed to be N bytes, depending on the architecture: - On x86-64, aarch64 and powerpc64 , N = 128. - N =
32 on arm, mips, mips64 and riscv64 . - On s390x , N = 256. - On all other architectures, N = 64.
Note that N is only a reasonable guess and is not guaranteed to match the actual cache line length of the machine the program is running
on. On modern Intel architectures, the space prefetcher fetches a pair of 64- byte cache lines at a time, so we pessimistically assume
CachePadded is used to pad and align values to the cache line length. It's a wrapper that puts the value in a cache line
and pads some bytes before and after it to make sure it doesn't share the cache line with other values. The code below is
an example:
use crossbeam_utils::CachePadded;
memory does not fall on the same cache line. Next, by getting the addresses of the two elements in the array, we calculated the address
difference between them, and checked whether their addresses were aligned in multiples of 64 bytes.
The specific explanation is as follows: - assert!(addr2 - addr1 >= 64);: Ensure that the address difference of the two elements is at least
64 bytes to ensure that they are in different cache lines. - assert_eq!(addr1 % 64, 0);: Ensure that the address of the first element is
aligned to 64 bytes. - assert_eq!(addr2 % 64, 0);: Ensure that the address of the second element is aligned according to 64 bytes.
The purpose of this is to prevent two adjacent elements from landing on the same cache line to reduce contention between cache lines.
CachePadded is useful when we implement a Queue data structure to place head and tail in different cache lines so that concurrent
threads do not invalidate each other's cache lines when pushing and popping elements:
169
Machine Translated by Google
struct Queue<T>
{ head: CachePadded<AtomicUsize>, tail:
CachePadded<AtomicUsize>, buffer: *mut
T,
}
11.5.3 Scope
All child threads that have not been manually joined will be automatically joined before the end of this function call. If all
joined threads complete successfully, an Ok containing the return value of f is returned . If any of the joined threads
panic, Err will be returned containing the error from the panic thread . Note that if panic is achieved by aborting the
process, no error will be returned.
use crossbeam_utils::thread;
thread::scope(|s| { s.spawn(|
_| {
println!("A child thread borrowing `var`: {:?}", var);
}); }).unwrap();
11.6 crossbeam-skiplist
It implements concurrent mapping and collections based on skip lists.
This crate provides types SkipMap and SkipSet. These data structures provide interfaces similar to BTreeMap and
BTreeSet respectively , but they support safe concurrent access between multiple threads.
SkipMap and SkipSet implement Send and Sync so they can be easily shared between multiple threads.
Methods that change the mapping, such as insert, accept &self instead of &mut self. This allows them
to be called concurrently.
scope(|s| {
170
Machine Translated by Google
11.6 crossbeam-skiplist
//
s.spawn(|_|
{ person_ages.insert("Spike Garrett", 22);
person_ages.insert("Stan Hancock", 47);
person_ages.insert("Rea Bryan", 234);
}); }).unwrap();
assert!(person_ages.contains_key("Spike Garrett"));
person_ages.remove("Rea Bryan"); assert!
(!person_ages.contains_key("Rea Bryan"));
This code creates a skip list map per-son_ages using the SkipMap from the crossbeam_skiplist
crate . A thread scope is created via the scope in the crossbeam_utils crate , and entries are
inserted into the map concurrently in two different threads.
Finally, the results of some operations were checked through assertions, including the inserted value and whether the key was included after deleting it.
171
Machine Translated by Google
Machine Translated by Google
12
rayon gorge
rayon is a data parallel library for Rust . It is very lightweight and can easily convert sequential calculations into parallel calculations. It also
ensures that the code avoids data races.
Rayon makes it very easy to convert sequential iterators into parallel iterators: Typically, just
change your foo.iter() call to foo.par_iter() and Rayon will take care of the rest:
use rayon::prelude::*; fn
sum_of_squares(input: &[i32]) -> i32 {
input.par_iter() // .map(|
&i| i * i) .sum()
The parallel iterator is responsible for deciding how to divide the data into tasks; it dynamically adapts for maximum performance. If you
need more flexible options than this, Rayon also provides join and scope functions that allow you to create parallel tasks yourself. If you
want more control, you can create a custom thread pool instead of using Rayon 's default global thread pool.
It provides parallel iterators for arrays, standard library collections, Option, Rane, Result, Slice, String and Vec , so when you
want to change the iteration of these collections to parallel execution, especially when you need to process a large amount
173
Machine Translated by Google
12 rayon gorge
12.2 scope
This represents a fork-join scope and can be used to generate any number of tasks.
The spawn method generates a task in the fork-join scope self . This task will be executed at some point before the fork-
join scope is completed. Tasks are specified in the form of a closure, and this closure receives a reference to the scope self
as a parameter. This can be used to inject new tasks into self .
The resulting closures cannot pass values directly back to the caller, although they can write to local variables on the stack
(if those variables outlive the scope), or communicate over shared channels.
ˆ
`handle` `s1`
// //
value_a = Some(22);
// `s`
s1.spawn(|_|
{ value_b = Some(44);
});
});
s.spawn(|_|
{ value_c = Some(66);
});
});
assert_eq!(value_a, Some(22)); assert_eq!
(value_b, Some(44)); assert_eq!(value_c,
Some(66));
This code uses the Rayon library to create a parallel scope and generate some tasks in it. Let’s understand step by step:
1. Let mut value_a = None; and other statements declare three variable variables value_a, value_b and
174
Machine Translated by Google
In the task, value_a is set to Some(22). Note that s1 is the scope handle used in the spawned task,
because scope handles cannot be passed across thread boundaries.
4. In the generated task, s1.spawn(|_| { value_b = Some(44); }); generates another task
and sets value_b to Some(44). 5.
s.spawn(|_| { value_c = Some(66); }); generates another task in the main scope,
Set value_c to Some(66).
6. Since the Rayon scope guarantees that it will not end before all tasks in it are completed, in the assert_eq!
assertion, you can check whether the values of value_a, value_b and value_c are as expected after the
task is executed.
spawn_broadcast spawns an asynchronous task on each thread of this thread pool. This task will run in the implicit
global scope, which means it may outlive the current stack frame - therefore, it cannot capture any references on
the stack (you may need to use a move closure). The function spawn_fifo places a task in the static or global scope
of the Rayon thread pool. Just like a standard thread, this task is not associated with the current stack frame, so it
cannot hold any reference other than having a 'static lifetime'. If you want to spawn a task that references stack
data, you can use the scope_fifo() function to create a scope. This function behaves essentially the same as the
spawn function, except that calls from the same thread will be processed first in FIFO (first in, first out) order.
} let (a, b) = rayon::join(|| fib(n - 1), || fib(n - 2)); // runs inside of `pool return a + b;
println!("{}", n);
install() executes a closure in the ThreadPool 's thread. Additionally, any calls inside install()
175
Machine Translated by Google
12 rayon gorge
Other Rayon operations will also be performed in the context of the ThreadPool .
When a ThreadPool is discarded, a signal is triggered to terminate the threads it manages, and they will complete execution.
broadcast performs the operation op in each thread of the thread pool . Then, any attempt to use join, scope or
The broadcast operation is executed after each thread has exhausted its local work queue and then attempts to steal from other threads
Work. The goal of this strategy is to execute in a timely manner without causing too much disruption to current work. If necessary
Yes, more or less intrusive broadcast styles may be added in the future.
fn main() {
// 5
//
let v: Vec<usize> = pool.broadcast(|ctx| ctx.index() * ctx.index());
// [0, 1, 4, 9, 16]
assert_eq!(v, &[0, 1, 4, 9, 16]);
//
let count = AtomicUsize::new(0);
// AtomicUsize
pool.broadcast(|_| count.fetch_add(1, Ordering::Relaxed));
// 5
assert_eq!(count.into_inner(), 5);
}
To configure the global thread pool, you can use the build_global method:
rayon::ThreadPoolBuilder::new().num_threads(22).build_global().unwrap();
ThreadPoolBuilder can configure the thread pool, such as thread name, stack size, etc.
12.4 join
The join method is a function in the Rayon library that is used to execute multiple tasks in parallel and wait for them all to complete. it
Usually used to replace join or thread::spawn in the standard library , making parallelized code simpler and more efficient.
Specifically, the rayon::join function accepts two closures as parameters, representing two tasks executed in parallel.
These two tasks will be executed simultaneously in the Rayon thread pool, and their results will be returned after both tasks are completed. this
In this way, you can avoid the complexity of manually managing parallel tasks through explicit use of threads, channels, etc., and the Rayon library will
The thread pool used automatically allocates tasks to improve parallel performance.
176
Machine Translated by Google
12.4 join
For example:
use rayon::join;
fn main() {
let result = join(|| expensive_operation1(), || expensive_operation2()); // let final_result = result.0 + result.1; println!
// Partition // // // fn `<=`
" "
177
Machine Translated by Google
12 rayon gorge
i += 1;
}
} v.swap(i, pivot); i
178
Machine Translated by Google
13
tokio library
Tokio is an event-driven, non-blocking I/O -based platform for writing asynchronous applications using Rust . At a high level, it provides some main components:
• Tools for handling asynchronous tasks, including synchronization primitives, channels, timeouts, sleeps, and intervals. • APIs for
performing asynchronous I/O operations , including TCP and UDP sockets, file system operations, and processes and
Signal management.
• Runtime for executing asynchronous code, including task scheduler, operating system event queue-based I/O driver
Activators (epoll, kqueue, IOCP, etc.), and high-performance timers.
This chapter does not introduce Tokio 's asynchronous I/O, network programming and other functions, but focuses on Tokio 's asynchronous task scheduler and
synchronization primitives.
However, here we add a process management function of tokio , tokio::process::Command, which is an imitation
of std::process::Command and can execute commands asynchronously.
use tokio::process::Command;
#[tokio::main] async
fn main() -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new("echo").arg("hello").arg("world") .output();
179
Machine Translated by Google
13 tokio library
assert!(output.status.success()); assert_eq!
(output.stdout, b"hello world\n"); Ok(())
use std::process::Stdio;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { let mut cmd =
Command::new("cat");
// // // cmd.stdout(Stdio::piped());
//
tokio::spawn(async move { let
status = child.wait().await .expect("child
process encountered an error");
Ok(())
}
180
Machine Translated by Google
#[tokio::main] async
fn main() -> Result<(), Box<dyn std::error::Error>> { let mut echo =
Command::new("echo") .arg("hello
assert!(echo_result.unwrap().success());
Ok(())
}
181
Machine Translated by Google
13 tokio library
13.2.1 Mutex
This type behaves like std::sync::Mutex, but there are two main differences: lock is an asynchronous method and therefore does not block,
Contrary to popular belief, it is okay, and often preferable, to use plain Mutexes from the standard library in asynchronous code.
The feature provided by an asynchronous mutex relative to a blocking mutex is the ability to maintain the lock at the .await point. This makes
asynchronous mutexes more expensive than blocking mutexes, so blocking mutexes should be preferred in situations where a blocking mutex
can be used. The main use case for asynchronous mutexes is to provide shared variable access to IO resources such as database connections.
If the value behind the mutex is just data, you can usually use a blocking mutex from the standard library or parking_lot .
Note that although the compiler does not prevent std::Mutex from keeping its guard on .await points in cases where tasks cannot be moved
between threads, in practice this will almost never result in correct concurrent code, Because it can easily lead to deadlock.
A common pattern is to wrap an Arc<Mutex<. . . » in a struct, provide non-async methods for the data in it, and only lock the mutex inside
Additionally, when shared access to an IO resource is indeed required , it is usually best to spawn a task to manage that IO resource and use
#[tokio::main]
async fn main() { let
data1 = Arc::new(Mutex::new(0)); let data2 =
Arc::clone(&data1);
});
There are a few things worth noting in this example: - The mutex is wrapped in an Arc to allow sharing across threads. -Each spawned task
acquires the lock and releases the lock on each iteration. -Mutation of data protected by a mutex is performed by dereferencing the acquired
Tokio 's mutex locks use a simple FIFO (first in, first out) approach, and all calls to the lock are completed in the order in which they are
executed. Therefore, mutex locks are "fair" and predictable in allocating locks to internal data . in each iteration
182
Machine Translated by Google
The lock is released and reacquired later, so basically each thread moves to the end of the queue after incrementing the value. Note that there
is some unpredictability between the timing of threads when they start, but once they start running they alternate predictably. Finally, since
there is only one active lock at any given time, there is no possibility of race conditions when mutating internal values.
Note that unlike std::sync::Mutex , this implementation does not cause the mutex to become poisonous when the thread holding the
MutexGuard panics . In this case, the mutex will be unlocked. If a panic is caught, this may leave the data protected by the mutex in an
inconsistent state.
13.2.2 RwLock
This type of lock allows multiple readers or at most one writer to exist simultaneously. The write portion of such a lock typically allows
modification of the underlying data (exclusive access), while the read portion typically allows read-only access (shared access).
In contrast, a mutex does not distinguish between readers or writers who acquire the lock, so any task that causes it to wait for the lock must
be abandoned. RwLock will allow any number of readers to acquire the lock, as long as no writers hold the lock.
Tokio 's read-write lock priority policy is fair (or write-biased) to ensure that readers cannot starve writers. By using a first-in-first-out queue to
ensure fairness, tasks waiting for a lock are not issued read locks until the write lock is released. This is different from the Rust standard
library's std::sync::RwLock , whose priority policy depends on the operating system's implementation.
The type parameter T represents the data protected by this lock. To be shared between threads, T is required to satisfy Send. The RAII guard
returned from the lock method implements Deref (and also DerefMut for write methods) to allow access to the contents of the lock.
use tokio::sync::RwLock;
#[tokio::main]
async fn main() { let
lock = RwLock::new(5);
183
Machine Translated by Google
13 tokio library
13.2.3 Barrier
Barriers allow multiple tasks to synchronize the start of a computation.
// Will not resolve until all "after wait" messages have been printed let mut num_leaders = 0; for handle in
handles { let wait_result =
handle.await.unwrap(); if
wait_result.is_leader() { num_leaders += 1;
13.2.4 Notify
Notify a single task to wake up.
Notify provides a basic mechanism for notifying individual tasks of the occurrence of events. Notify itself does not carry any data. Instead, it is used to
Think of Notify as a semaphore with 0 licenses. The notified().await method waits for a license to be available,
while notify_one() sets the license if no license is currently available.
The synchronization details of Notify are similar to thread::park and Thread::unpark in std . Notify value contains
184
Machine Translated by Google
A separate license. notified().await waits for a license to be available, consumes the license, and continues execution.
notify_one () sets the license and wakes up a pending task if there is one.
If notify_one() is called before notified().await , the next call to notified().await will complete immediately, consuming
the license. Any subsequent calls to notified().await will wait for a new license.
If notify_one() is called multiple times before notified( ).await , only one license is stored. The next call to noti-
fied().await will complete immediately, but subsequent calls will wait for a new license.
#[tokio::main]
async fn main() { let
notify = Arc::new(Notify::new()); let notify2 =
notify.clone();
println!("sending notification");
notify.notify_one();
If there is currently a task waiting, this task will be notified. Unlike notify_one() , the license is not stored for the next
call to notify().await . The purpose of this method is to notify all registered waiters. By calling notified() to obtain a
Notified future instance, you can register to receive notifications.
#[tokio::main]
async fn main() { let
notify = Arc::new(Notify::new()); let notify2 =
notify.clone();
185
Machine Translated by Google
13 tokio library
});
notified1.await;
notified2.await; println!
("received notifications");
}
13.2.5 Semaphore
Count semaphore to perform asynchronous permission acquisition.
Semaphores maintain a set of licenses that are used to synchronize access to shared resources. A semaphore differs from a mutex in that it allows
When acquire is called and the semaphore has a license remaining, this function immediately returns a license. However, if no remaining licenses
are available, acquire waits (asynchronously) until an unused license is released. At this point, the released license will be assigned to the caller.
This semaphore is fair, meaning that licenses are distributed in the order in which they are requested. This fairness also applies when acquire_many
is involved, so if an acquire_many call ahead of the queue requests more licenses than are currently available, this may prevent the acquire call
from completing, even if the semaphore has enough licenses to Complete the acquire call.
To use semaphores in polling functions, you can use the PollSemaphore utility.
#[tokio::main]
async fn main() { let
semaphore = Semaphore::new(3);
assert_eq!(semaphore.available_permits(), 0);
186
Machine Translated by Google
13.3 channels
13.2.6 OnceCell
A thread-safe cell that can only be written to once.
OnceCell is typically used for global variables that need to be initialized once when first used, but do not require further changes.
In Tokio , OnceCell allows the initialization process to be asynchronous.
use tokio::sync::OnceCell;
#[tokio::main]
async fn main() { let
result = ONCE.get_or_init(some_computation).await; assert_eq!(*result,
2);
}
Or wrap it up:
use tokio::sync::OnceCell;
}).await
}
#[tokio::main]
async fn main() { let
result = get_global_integer().await; assert_eq!(*result,
2);
}
13.3 channels
In Tokio programs, the most common form of synchronization is message passing. The two tasks run independently and are synchronized
Message passing is implemented through channels. Channels support sending messages from one producer task to one or more
consumer tasks. Tokio offers several different types of channels. Each channel type supports different messaging modes. when
187
Machine Translated by Google
13 tokio library
When a channel supports multiple producers, many independent tasks can send messages. When a channel supports multiple consumers, many
Since different messaging patterns work best with different implementations, Tokio provides many different types of channels.
13.3.1 mpsc
The mpsc channel supports sending multiple values from multiple producers to a single consumer. Such channels are typically used to send work
This channel should also be used if you want to send multiple messages from a single producer to a single consumer. There is no dedicated spsc
channel.
use tokio::sync::mpsc;
#[tokio::main] async
fn main() { let (tx, mut rx)
= mpsc::channel(100);
});
The parameter of mpsc::channel is the capacity of the channel. This is the maximum number of values that can be stored in the channel waiting to
be received at any given time. Setting this value correctly is critical to achieving a robust program, as channel capacity plays a key role in handling
backpressure.
13.3.2 oneshot
One-shot channels support sending a single value from a single producer to a single consumer. Typically, this channel is used to send the results
of calculations to waiters.
use tokio::sync::oneshot;
188
Machine Translated by Google
13.3 channels
#[tokio::main]
async fn main() { let
(tx, rx) = oneshot::channel();
//
//
let res = rx.await.unwrap();
}
value. This channel can be used to implement publish/subscribe or ' fanout' style patterns common in 'chat' systems.
This channel is less used compared to single shot and mpsc but still has its use cases.
use tokio::sync::broadcast;
#[tokio::main]
async fn main() { let
(tx, mut rx1) = broadcast::channel(16); let mut rx2 =
tx.subscribe();
tokio::spawn(async move
{ assert_eq!(rx1.recv().await.unwrap(), 10); assert_eq!
(rx1.recv().await.unwrap(), 20);
});
tokio::spawn(async move
{ assert_eq!(rx2.recv().await.unwrap(), 10); assert_eq!
(rx2.recv().await.unwrap(), 20);
});
tx.send(10).unwrap();
tx.send(20).unwrap();
189
Machine Translated by Google
13 tokio library
are sent, the consumer is notified, but there is no guarantee that the consumer will see all values.
Use cases for observation channels include broadcasting configuration changes or signaling program state changes, such as switching to a shutdown state.
The following example uses an observation channel to notify task configuration changes. In this example, the configuration file is checked periodically. When the file changes, the
use std::io;
impl Config {
async fn load_from_file() -> io::Result<Config> {
// file loading and deserialization logic here
}
async fn my_async_operation() {
// Do something here
}
#[tokio::main]
async fn main() { //
Load initial configuration value let mut config
= Config::load_from_file().await.unwrap();
// Create the watch channel, initialized with the loaded configuration let (tx, rx) =
watch::channel(config.clone());
190
Machine Translated by Google
13.3 channels
time::sleep(Duration::from_secs(10)).await;
// If the configuration changed, send the new config value // on the watch
channel. if new_config != config
{ tx.send(new_config.clone()).unwrap();
config = new_config;
});
// Spawn tasks that runs the async operation for at most `timeout`. If // the timeout elapses, restart
the operation. // // The task simultaneously watches the `Config`
for changes. When the // timeout duration changes, the timeout is updated without restarting //
the in-flight operation. for _ in 0..5 { // Clone a config watch handle for use in this task let mut rx
= rx.clone();
loop
{ tokio::select! {
_
= &mut sleep => {
191
Machine Translated by Google
13 tokio library
// The configuration has been updated. Update the // `sleep` using the
new `timeout` value. sleep.as_mut().reset(op_start +
conf.timeout);
}
_
= &mut op => {
// The operation completed! return
});
handles.push(handle);
}
This module provides types for executing code after a period of time.
• Sleep: is a future that is completed at a specific moment and does not perform any
work. • Interval: is a stream that generates values at a fixed period. It is initialized with a Duration and is generated
repeatedly every time that duration
elapses. • Timeout: Wrap a future or stream and set an upper limit on the time it is allowed to execute. If the future or stream
fails to complete within the specified time, it will be canceled and an error will be returned.
192
Machine Translated by Google
These types must be used within the context of Tokio 's Runtime .
13.4.1 Sleep
Wait until the duration has elapsed.
No work is performed while waiting for the sleep future to complete. Sleep runs at millisecond granularity and should not be used for tasks
that require high-resolution timers. The implementation is platform specific, and some platforms (especially Windows) will provide timers
The maximum duration of sleep is 68719476734 milliseconds (approximately 2.2 years). If it takes
longer, use tokio::time::delay_for.
#[tokio::main]
async fn main()
{ sleep(Duration::from_millis(100)).await; println!("100
ms have elapsed");
}
#[tokio::main]
async fn main()
{ sleep_until(Instant::now() + Duration::from_millis(100)).await; println!("100 ms have
elapsed");
}
13.4.2 Interval
Creates a new Interval that generates values at specified intervals. The first tick is done immediately.
Interval will tick indefinitely. At any time, the Interval value can be discarded. This cancels the interval.
#[tokio::main]
async fn main() {
193
Machine Translated by Google
13 tokio library
interval_at creates a new Interval that generates values at the specified time interval. The first tick is completed at the specified point
in time.
#[tokio::main]
async fn main() { let
start = Instant::now() + Duration::from_millis(50); let mut interval =
interval_at(start, Duration::from_millis(10));
13.4.3 Timeout
The future needs to be completed before the specified duration has elapsed .
If the future completes before the duration has elapsed, the completed value is returned. Otherwise, an error is returned and the future
is canceled.
Note that the timeout is checked before the future is polled , so if the future is not yielded during execution, the future may complete
This function returns a future whose return type is Result<T,Elapsed>, where T is the return type of the provided future .
If the supplied future completes immediately, the future returned from this function is guaranteed to complete immediately in the Ok
use std::time::Duration;
194
Machine Translated by Google
// Wrap the future with a `Timeout` set to expire in 10 milliseconds. if let Err(_) =
timeout(Duration::from_millis(10), rx).await { println!("did not receive value within 10
ms");
}
timeout_at needs to complete the future before the specified time point .
use std::time::Duration;
// Wrap the future with a `Timeout` set to expire 10 milliseconds into the // future. if let Err(_) =
195
Machine Translated by Google
Machine Translated by Google
14
Other concurrency libraries
In the previous chapters, we introduced various synchronization primitives by category and how to use them to build concurrent programs.
In this chapter, we will introduce some other concurrency libraries that provide some high-level synchronization primitives that do not fit in
The process_lock library provides the ability to perform mutually exclusive access between processes. It implements mutex locks by
sharing a file between processes.
First, process_lock creates an empty file in a publicly accessible location, which acts as a lock. Then, if a process wants to acquire the
lock, it will try to open the file exclusively. If the file is already open by another process, opening the file will fail, thus achieving mutually
exclusive access.
The process that obtained the lock needs to ensure that it closes and deletes the file after using the lock, otherwise other processes will no
longer be able to obtain the lock. process_lock provides a RAII- style ProcessLock structure to wrap this logic, which will automatically close
pub fn process_lock() {
let lock = ProcessLock::new(String::from(".process_lock"), None); let start =
Instant::now(); loop { if lock.is_ok()
{ println!
("lock success");
break;
197
Machine Translated by Google
} std::thread::sleep(Duration::from_millis(100));
} std::thread::sleep(Duration::from_millis(500));
process_lock is suitable for scenarios where synchronized access between processes is required and mechanisms such as IPC or
database cannot or do not want to be used. However, it also has some limitations: -Relies on an external file as a lock. If the file is
accidentally deleted , the lock mechanism will fail. -The process must have access to the directory where this file is located. -Does not
provide the ability to block waiting for locks, and will fail if the lock is occupied. -Weaker effect in distributed systems.
Therefore, the advantages and limitations of process_lock need to be weighed during design . Overall, it provides Rust with a simple
inter-process mutual exclusion mechanism that can work well in some scenarios.
The named-lock library also provides a file-based named lock. It uses a file as a lock and can wait on the lock.
On UNIX , this is accomplished using files and flock . The path to the created lock file will be $TM-
PDIR/.lock, or /tmp/.lock if the TMPDIR environment variable is not set .
one example:
// Do something...
Ok(())
}
14.2 oneshots
A one-shot channel means that it can only send and receive one value, and the channel will be closed after sending or receiving. This
can be used to send a signal or value between threads without maintaining a persistent channel.
oneshot provides Sender and Receiver structures to represent both ends of a single channel. The sender uses
Sender and the receiver uses Receiver.
198
Machine Translated by Google
14.2 oneshots
pub fn oneshot_example() {
let (sender, receiver) = oneshot::channel::<i32>(); let sender =
thread::spawn(move || { sender.send(1).unwrap();
});
let receiver = thread::spawn(move || { let v =
receiver.recv().unwrap(); println!("get value
{}", v);
});
sender.join().unwrap();
receiver.join().unwrap();
}
async_oneshot is a small and convenient crate for asynchronous Rust programming . It provides an implementation of a one-shot
asynchronous channel.
Different from oneshot , async_oneshot is based on asynchronous tasks and Future , which can better integrate into the asynchronous
programming model.
Mainly provides two structures: - Sender - sending end - Receiver - receiving end
How to use:
pub fn async_oneshot_example() {
let (mut sender, receiver) = async_oneshot::oneshot(); smol::block_on(async
{ sender.send(1).unwrap();
});
smol::block_on(async { let v =
receiver.try_recv().unwrap(); println!("get value {}",
v);
});
}
Advantages of async_oneshot : - Convenient to send signal messages between asynchronous tasks. -Encapsulates synchronization primitives, and using await to receive
values is very simple. -Sending a value will consume the sending end, receiving a value will consume the receiving end, and will be automatically closed. -Seamless integration
catty is also a oneshot library, which is faster, simpler and lighter than futures::oneshot . one example:
});
199
Machine Translated by Google
});
sender.join().unwrap();
receiver.join().unwrap();
}
14.3 map
DashMap is a thread-safe hash table that provides an interface similar to HashMap , but can be safely accessed
concurrently in multiple threads. DashMap attempts to implement a simple and easy-to-use API similar to
std::collections::HashMap , with some minor changes to handle concurrency.
DashMap aims to be a very simple and easy-to-use tool that can directly replace
RwLock<HashMap<K, V>>. To achieve these goals, all methods use &self instead of modifying
methods using &mut self. This allows you to put a DashMap into an Arc and share it between
threads while still being able to modify it. one example:
});
loop { if
let Some(v) = map2.get(&2) { println!("get
value {} for key 2", *v); break;
200
Machine Translated by Google
14.3 map
});
whandle.join().unwrap();
rhandle.join().unwrap();
}
Flurry is a thread-safe hash table implemented with reference to Java ConcurrentHashMap . It provides an interface
similar to HashMap , but can be safely accessed concurrently in multiple threads. At the same time, it also implements
a HashSet. one example:
// Remove a book.
books.remove(&"Three Men In A Raft", &guard);
one example:
201
Machine Translated by Google
let r = book_reviews_r.clone();
thread::spawn(move || { loop
{ let l =
r.len(); if l == 0
{ thread::yield_now(); } else { //
the reader
will either see all the reviews, // or none of them, since refresh()
is atomic. assert_eq!(l, 4); break;
})
}) .collect();
// do some writes
book_reviews_w.insert("Adventures of Huckleberry Finn", "My favorite book."); book_reviews_w.insert("Grimm
Fairy Tales", "Masterpiece."); book_reviews_w.insert("Pride and Prejudice", "Very
enjoyable."); book_reviews_w.insert("The Adventures of Sherlock Holmes", "Eye lyked
it alot." // expose the writes book_reviews_w.refresh();
202
Machine Translated by Google
async_lock provides asynchronous synchronization primitives, including: - Barrier - used to enable tasks to be synchronized all at the same time.
- Mutex - Mutex lock. - RwLock - Read-write lock, allows any number of reads or a single write. - Semaphore - Limits the
number of concurrent operations.
one example:
pub fn async_lock_mutex() {
let lock = Arc::new(Mutex::new(0));
});
});
thread::scope(|s| {
203
Machine Translated by Google
});
});
}
});
}
Some atomic operation encapsulation, such as atomicbox provides AtomicBox<T> and AtomicOptionBox<T>; atomig
provides generic, convenient and lock-free std atomic operations through Atomic<T> . Can be used with many basic types
(including floating point) as well as custom types; sharded_slab provides preallocated storage for many instances of a
single data type. When a large number of values of a single type are required, this may be more efficient than assigning
each item individually. Since the allocated items are of the same size, memory fragmentation is reduced and the cost of
creating and deleting new items is also very low;
waitgroup provides an implementation similar to Go 's WaitGroup , which is used to wait for a group of tasks to complete. one
example:
pub fn waitgroup_example()
{ smol::block_on(async { let
wg = WaitGroup::new(); for _ in
0..100 { let w =
wg.worker(); let _ =
smol::spawn(async move { // do work
drop(w); //
drop w means task finished
});
}
wg.wait().await;
})
}
wg is also a lightweight Go language-style WaitGroup implementation. It allows you to wait for a set of tasks to complete.
one example:
204
Machine Translated by Google
use wg::WaitGroup;
});
}
wg.wait();
assert_eq!(ctr.load(Ordering::Relaxed), 5);
}
`awaitgroup` WaitGroup
```rust
pub fn awaitgroup_example() { use
awaitgroup::WaitGroup;
let _ = smol::spawn(async { // Do
some work...
});
}
205
Machine Translated by Google
wg.wait().await;
});
}
event-listener notifies asynchronous tasks or threads. This is a synchronization primitive, similar to eventcounts
invented by Dmitry Vyukov . You can use this crates convert non-blocking data structures into asynchronous or
blocking data structures.
one example:
pub fn event_listener_example() {
let flag = Arc::new(AtomicBool::new(false)); let event =
Arc::new(Event::new());
// Spawn a thread that will set the flag after 1 second. thread::spawn({ let flag
= flag.clone(); let
event = event.clone(); move ||
{ // Wait for a second.
thread::sleep(Duration::from_secs(1));
// Notify all listeners that the flag has been set. event.notify(usize::MAX);
});
206
Machine Translated by Google
break;
}
println!("flag is set");
}
Triggered is a trigger for one-time events between tasks and threads. The mechanism consists of two types, Trigger and
Listener. They appear as a pair. Sort of like a sender/receiver pair for a channel. A trigger has a Trigger::trigger method,
which will cause all tasks/threads waiting on the listener to continue executing. The listener has both a synchronous
Listener::wait method and asynchronously supported Future<Output = ()>.
Both Trigger and Listener can be cloned. Therefore, any number of trigger instances can trigger any number of waiting
listeners. When any trigger instance belonging to the pair is fired, all waiting listeners will be unblocked. Waiting on an already
triggered listener returns immediately. Therefore, each trigger/listener pair can only be fired once.
one example:
pub fn triggered_example()
{ smol::block_on(async { let
(trigger, listener) = triggered::trigger();
// This will make any thread blocked in `Listener::wait()` or async task awaiti // listener continue execution again.
trigger.trigger();
let _ = task.await;
})
}
barrage is a simple asynchronous broadcast channel. It is runtime agnostic and can be used from any executor. It can also
operate synchronously.
pub fn barrage_example()
{ smol::block_on(async {
207
Machine Translated by Google
14.6 Queue
There are two types of queues: - Bounded queues with limited capacity. -Unbounded queue with unlimited capacity.
The queue can also be closed at any time. After closing, no more items can be pushed to the queue, but remaining items can still
be popped. These features make it easy to build channels similar to std::sync::mpsc on top of this crate .
one example:
pub fn concurrent_queue_example() {
let q = Arc::new(ConcurrentQueue::unbounded());
});
});
whandle.join().unwrap();
rhandle.join().unwrap();
}
208
Machine Translated by Google
14.7 scc
14.7 scc
A collection of high-performance containers and utilities for concurrent and asynchronous programming.
Features - The counterpart to asynchronous blocking and synchronized methods. -Formally verified EBR implementation. -Almost
linear scalability. -No spinlocks and busy loops. - SIMD lookup to scan multiple entries in parallel. -Zero dependence on other
Concurrent and asynchronous container - HashMap is a concurrent and asynchronous hash map. - HashSet is a concurrent and
asynchronous hash set. - HashIndex is a read-optimized concurrent and asynchronous hash map. - HashCache is a sampled LRU
one example:
pub fn scc_hashmap() {
let hashmap: HashMap<usize, usize, RandomState> = HashMap::with_capacity(1000); assert_eq!
(hashmap.capacity(), 1024);
} drop(ticket);
assert_eq!(hashmap.capacity(), 1024);
}
assert!(!hashindex.remove(&1)); assert!
(hashindex.insert(1, 0).is_ok()); assert!
(hashindex.remove(&1));
}
209
Machine Translated by Google
} drop(ticket);
assert_eq!(hashset.capacity(), 1024);
}
queue.push(37);
queue.push(3);
queue.push(1);
pub fn async_weighted_semaphore_example() {
smol::block_on(async {
let sem = async_weighted_semaphore::Semaphore::new(1); let a =
sem.acquire(2); let b =
sem.acquire(1); pin_mut!(a);
pin_mut!(b);
assert!(poll!
(&mut a).is_pending());
210
Machine Translated by Google
14.9 singleflight
assert!(poll!(&mut b).is_pending());
sem.release(1);
assert!(poll!(&mut a).is_ready()); assert!(poll!
(&mut b).is_ready());
});
}
(s.try_acquire_arc().is_some());
}
14.9 singleflight
singleflight_async provides a SingleFlight structure that can be used to share and reuse calculation
results among multiple gorou-tines . The Go extension library SingleFlight is widely used. one example:
pub fn singleflight_example()
{ smol::block_on(async { let
group = SingleFlight::new(); let mut futures
= Vec::new(); for _ in 0..10
{ futures.push(group.work("key", || async {
println!("will sleep to simulate async task");
smol::Timer::after(std::time::Duration::from_millis(100)).await; println!("real task done");
"my-result" }));
});
211
Machine Translated by Google
14.10 arc_swap
arc_swap provides something similar to RwLock<Arc<T>> or Atomic<Arc<T>> , which is optimized for scenarios where reading is the main
one example:
});
for _ in 0..10
{ scope.spawn(|_|
{ loop
{ let v = value.load(); println!
("value is {}", v); return;
});
} }).unwrap()
212