0% found this document useful (0 votes)
40 views

Rust Concurrency Cookbook

Rust cookbook

Uploaded by

venkatnetha
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
40 views

Rust Concurrency Cookbook

Rust cookbook

Uploaded by

venkatnetha
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 212

Machine Translated by Google

For an in-depth understanding of Rust concurrent programming,

from getting started to giving up, this book is enough.

Written by Chao Yuepan (@birdnest )

Version 0.14
December 27 , 2023
Machine Translated by Google
Machine Translated by Google

Table of contents

1 Thread 1.1 Create thread . 9

. . . . . . . . . . . . . . . . . . . . . . . . . 11

1.2 Thread Builder . . . . . . . . . . . . . . . . . . . . . . . . 12

1.3 Current thread . 1.4 Concurrency number and current thread number . 1.5 . . . . . . . . . . . . . . . . . . . . . . . . 13

sleep and park . 1.6 scoped thread . . . . . . . . . . . . . . . . . . . . . . 14

. . . . . . . . . . . . . . . . . . . . . . . 15

. . . . . . . . . . . . . . . . . . . . . . . 17

1.7 ThreadLocal . . . . . . . . . . . . . . . . . . . . . . . . . 18

1.8 Move . 1.9 Control the newly created thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

1.10 Set thread priority . 1.11 Set affinity . . . . . . . . . . . . . . . . . . . . . . . . 20

. . . . . . . . . . . . . . . . . . . . . . . 21

. . . . . . . . . . . . . . . . . . . . . . . . 23

1.12 Panic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

. . . . . . . . . . . . . . . . . . 25
1.13 crossbeam scoped thread .

. . . . . . . . . . . . . . . . . . . . 25
1.14 Rayon scoped thread . 1.15 send_wrapper .

. . . . . . . . . . . . . . . . . . . . . . . 26

1.16 Go- style startup threads . . . . . . . . . . . . . . . . . . . . . . 27

2 thread pool 2.1 rayon thread pool . 29

. . . . . . . . . . . . . . . . . . . . . . . . 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

3.5 futures . 3.6 futures_lite . 3.7 async_std . 3.8 smol . 3.9 . . . . . . . . . . . . . . . . . . . . . . . . . . 55

. . . . . . . . . . . . . . . . . . . . . . . . 56
try_joinÿjoinÿselect ÿ zip .

. . . . . . . . . . . . . . . . . . . . . . . . . 57

. . . . . . . . . . . . . . . . . . . . . . . . . . . 57

. . . . . . . . . . . . . . . . . 58

4 Container synchronization primitives 61

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

5 basic synchronization primitives 69

5.1 Arc. . 5.2 Mutex Mutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

. . . . . . . . . . . . . . . . . . . . . . . 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 .

6 Concurrent Collections 6.1 Thread-safe 101

Vec . 6.2 Thread-safe HashMap . 6.3 dashmap . . . . . . . . . . . . . . . . . . . . . . . . 101

. . . . . . . . . . . . . . . . . . . . 102

. . . . . . . . . . . . . . . . . . . . . . . . . 104
Machine Translated by Google

6.4 lockfree. . . . . . . . . . . . . . . . . . . . . . . . . . . 104

6.5 cuckoofilter . . . . . . . . . . . . . . . . . . . . . . . . . 105

. . . . . . . . . . . . . . . . . . . . . . . . . . 105
6.6 evmap . 6.7 arc-swap .

. . . . . . . . . . . . . . . . . . . . . . . . . 106

7 Process 7.1 Create a 109

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

8 channel channel 8.1 mpsc . 8.2 crossbeam-channel . 119

. . . . . . . . . . . . . . . . . . . . . . . . . . . 119

. . . . . . . . . . . . . . . . . . . . . 123

8.3 flume. . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

. . . . . . . . . . . . . . . . . . . . . . . 130
8.4 async_channel . 8.5 futures_channel . 8.6 crossfire .

. . . . . . . . . . . . . . . . . . . . . . 131

. . . . . . . . . . . . . . . . . . . . . . . . . 133

8.7 channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

9 Timer 9.1 Timer . 9.1.1 timer library . 137

9.1.2 futures_timer . 9.1.3 async-io Timer . 9.1.4 tokio. . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

9.1.5 smol::Timer . . . . . . . . . . . . . . . . . . . . . . . 138

. . . . . . . . . . . . . . . . . . . . 140

. . . . . . . . . . . . . . . . . . 141

. . . . . . . . . . . . . . . . . . . . . . . 142

. . . . . . . . . . . . . . . . . . . . . 142

. . . . . . . . . . . . . . . . . . . . . 142
9.1.6 async-timer . 9.1.7 timer-kit .

. . . . . . . . . . . . . . . . . . . . . . 142

. . . . . . . . . . . . . 142
9.1.8 hierarchical_hash_wheel_timer .

9.2 ticker. . . . . . . . . . . . . . . . . . . . . . . . . . . . 143

9.2.1 ticker . . . . . . . . . . . . . . . . . . . . . . . . 143

9.2.2 tokio::time::interval . . . . . . . . . . . . . . . . . . 143

145
10parking_lot concurrent library

10.0.1 Mutex . . . . . . . . . . . . . . . . . . . . . . . . 146


Machine Translated by Google

10.0.2 FairMutex . . . . . . . . . . . . . . . . . . . . . . 149

10.0.3 RwLock . . . . . . . . . . . . . . . . . . . . . . . 150

10.0.4 ReentrantMutex . . . . . . . . . . . . . . . . . . . . 151

10.0.5 Once . . . . . . . . . . . . . . . . . . . . . . . . . 152

10.0.6 Condvar . . . . . . . . . . . . . . . . . . . . . . . 153

11crossbeam concurrency library 11.1 Atomic operations . 11.2 Data structure . 11.2.1 155

Bidirectional queue deque . . . . . . . . . . . . . . . . . . . . . . . . . . 156

. . . . . . . . . . . . . . . . . . . . . . . . . 157

. . . . . . . . . . . . . . . . . . . 157

. . . . . . . . . . . . . . . . . . . . . 159
11.2.2 ArrayQueue .

. . . . . . . . . . . . . . . . . . . . . 159
11.2.3 SegQueue . 11.3 Memory management . 11.4 Thread synchronization .

. . . . . . . . . . . . . . . . . . . . . . . . . 160

. . . . . . . . . . . . . . . . . . . . . . . . . 160

11.4.1 channel . . . . . . . . . . . . . . . . . . . . . . . 160

. . . . . . . . . . . . . . . . . . . . . . 165
11.4.2 Parking .

11.4.3 ShardedLock . . . . . . . . . . . . . . . . . . . . . 166

. . . . . . . . . . . . . . . . . . . . . 167
11.4.4 WaitGroup . 11.5 Utilities .

. . . . . . . . . . . . . . . . . . . . . . . . . 167

11.5.1 Backoff . . . . . . . . . . . . . . . . . . . . . . . . 168

11.5.2 CachePadded . . . . . . . . . . . . . . . . . . . . . 169

. . . . . . . . . . . . . . . . . . . . . . . 170
11.5.3 Scope . 11.6 crossbeam-skiplist .

. . . . . . . . . . . . . . . . . . . . . 170

173 . 173
12rayon library 12.1 Parallel collections . 12.2

scope . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . 174

12.3 Thread Pond . 12.4 join . . . . . . . . . . . . . . . . . . . . . . . . . . 175

. . . . . . . . . . . . . . . . . . . . . . . . . . . 176

13tokio library 13.1 Asynchronous runtime . 179

13.2 Synchronization primitives . . . . . . . . . . . . . . . . . . . . . . . . . 179

. . . . . . . . . . . . . . . . . . . . . . . . . 181

13.2.1 Mutex . . . . . . . . . . . . . . . . . . . . . . . . 182

13.2.2 RwLock . . . . . . . . . . . . . . . . . . . . . . . 183

13.2.3 Barrier . . . . . . . . . . . . . . . . . . . . . . . . 184

. . . . . . . . . . . . . . . . . . . . . . . 184
13.2.4 Notify .

. . . . . . . . . . . . . . . . . . . . . 186
13.2.5 Semaphore .

13.2.6 OnceCell . 13.3 channel . 13.3.1 mpsc . 13.3.2 oneshot . . . . . . . . . . . . . . . . . . . . . . . 187

. . . . . . . . . . . . . . . . . . . . . . . . . . . 187

. . . . . . . . . . . . . . . . . . . . . . . 188

. . . . . . . . . . . . . . . . . . . . . . 188

. . . . . . . . . . . . . . . . . . 189
13.3.3 broadcast (mpmc) . 13.3.4 watch (spmc) .

. . . . . . . . . . . . . . . . . . . . 190
Machine Translated by Google

13.4 Time related . . . . . . . . . . . . . . . . . . . . . . . . . . 192

. . . . . . . . . . . . . . . . . . . . . . . 193
13.4.1 Sleep .

13.4.2 Interval . . . . . . . . . . . . . . . . . . . . . . . 193 . 194

13.4.3 Timeout . . . . . . . . . . . . . . . . . . . . . .

14 Other concurrency libraries 14.1 Process lock . 197

. . . . . . . . . . . . . . . . . . . . . . . . . . 197

14.2 oneshot . . . . . . . . . . . . . . . . . . . . . . . . . . . 198

. . . . . . . . . . . . . . . . . . . . . . . . . . . 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 are usually independent, whereas threads exist as a subset of processes

• Processes carry much more state information than threads, and multiple threads in a process share process state and memory

and other resources

• 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

The advantages and disadvantages of threads and processes include:

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.

Figure 1.1. Processes and threads

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

adopt preemptive multitasking.

(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

1.1 Create thread

1.1 Create thread

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.

In this code, we use handle.join().unwrap() directly. In fact, join() returns Result.


Type, if the thread paniced , then it will return Err otherwise it will return , Ok(_) . This is interesting, ,

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 }

The following code starts multiple threads:

1 pub fn start_two_threads() {
2 let handle1 = thread::spawn(|| {
3 println!("Hello from a thread1!");
4 });
5

6 let handle2 = thread::spawn(|| {

11
Machine Translated by Google

1 thread

7 println!("Hello from a thread2!");


8 });
9

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

4 let handles: Vec<_> = (0..N)


5 .map(|i| {
6 thread::spawn(move || {
7 println!("Hello from a thread{}!", i);
8 })
9 })
10 .collect();
11

12 for handle in handles {


13 handle.join().unwrap();
14 }

15 }

1.2 Thread Builder


Through Builder, you can have more control over the initial state of the thread, such as setting the name of the thread, the size of the stack

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

6 let handler = builder


7 .spawn(|| {
8 println!("Hello from a thread!");
9 })
10 .unwrap();
11

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.3 Current thread

1 #![feature(thread_spawn_unchecked)]
2 use thread;
3

4 let builder = Builder::new()


5 ;

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

15 // caller has to ensure ‘join()‘ is called, otherwise


16 // it is possible to access freed memory if ‘x‘ gets
17 // dropped before the thread closure is executed!
18 handler.join().unwrap();

1.3 Current thread


Because threads are the smallest scheduling and computing unit of the operating system, the execution of a piece of code belongs to a certain thread. like

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

9 let builder = thread::Builder::new()


10 .name("foo".into()) // set thread name
11 .stack_size(32 * 1024); // set stack size
12

13 let handler = builder


14 .spawn(|| {
15 let current_thread = thread::current();
16 println!(
17 "child thread: {:?},{:?}",
18 current_thread.id(),
19 current_thread.name()
20 );
21 })
22 .unwrap();
23

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

4 let parked_thread = thread::Builder::new()


5 .spawn(|| {
6 println!("Parking thread");
7 thread::park();
8 println!("Thread unparked");
9 })
10 .unwrap();
11

12 thread::sleep(Duration::from_millis(10));
13

14 println!("Unpark the thread");


15 parked_thread.thread().unpark();
16

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.

Threads that meet the conditions are temporarily unexecutable.

1.4 Number of concurrency and current number of threads

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 :

1 use {io, thread};


2

3 fn main() -> Result<()> {


4 let count = thread::available_parallelism().unwrap().get();
5 assert!(count >= 1_usize);
6

7 Ok(())
8 }

affinity ( MacOS is not supported) The crate can provide the current number of CPU cores:

1 let cores: Vec<usize> = (0..affinity::get_core_num()).step_by(2).collect();


2 println!("cores : {:?}", &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

1.5 sleep and park

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. )

1 let count = thread::available_parallelism().unwrap().get();


2 println!("available_parallelism: {}", count);
3

4 if let Some(count) = num_threads::num_threads() {


5 println!("num_threads: {}", count);
6 } else {
7 println!("num_threads: not supported");
8 }
9

10 let count = thread_amount::thread_amount();


11 if !count.is_none() {
12 println!("thread_amount: {}", count.unwrap());
13 }
14

15 let count = num_cpus::get();


16 println!("num_cpus: {}", count);

1.5 sleep and park


Sometimes we need to suspend the current business for a period of time, maybe because certain conditions are not met, such as implementing

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

7 let handle2 = thread::spawn(|| {


8 thread::sleep(Duration::from_millis(1000));
9 println!("Hello from a thread4!");
10 });
11

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 }

If unpark is called first, the next park will return immediately:

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

1.6 scoped thread

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 .

Users should be aware of this possibility.

1.6 scoped thread

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.

A ThreadLocal example is as follows:

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

8 let handle1 = thread::spawn(move || {


9 COUNTER.with(|c| {

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

18 let handle2 = thread::spawn(move || {


19 COUNTER.with(|c| {
20 *c.borrow_mut() = 4;
21 });
22

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

move , sometimes move is not used ÿ

Make the corresponding closure not used move depend on whether to obtain ownership of external variables. If you do not get the external variable

ownership, you don’t need to use move. In most cases


, we will use external variables, so here move is more

common:

1 pub fn start_one_thread_with_move() {
2 let x = 100;
3

4 let handle = thread::spawn(move || {


5 println!("Hello from a thread with move, x={}!", x);
6 });
7

8 handle.join().unwrap();
9

10 let handle = thread::spawn(move || {

19
Machine Translated by Google

1 thread

11 println!("Hello from a thread with move again, x={}!", x);


12 });
13 handle.join().unwrap();
14

15 let handle = thread::spawn(|| {


16 println!("Hello from a thread without move");
17 });
18 handle.join().unwrap();
19 }

When we refer to the variable x in the thread , we use move ,


When we don't reference the variable, we don't use move

There is a question here. Doesn’t move transfer the ownership of x to the first sub-thread? Why does the second sub-thread

Can the thread still move and use x ?

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

4 let handle = thread::spawn(move || {


5 println!("Hello from a thread with move, x={:?}!", x);
6 });
7

8 handle.join().unwrap();
9

10 let handle = thread::spawn(move|| {


11 println!("Hello from a thread with move again, x={:?}!", x);
12 });
13 handle.join().unwrap();
14

15 let handle = thread::spawn(|| {


16 println!("Hello from a thread without move");
17 });
18 handle.join().unwrap();
19

20 }

1.9 Control newly created threads

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.10 Set thread priority

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

18 println!("This thread is stopped")


19 }

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

Determined since control.is_interrupted() == true .

1.10 Set thread priority


The priority of the thread can be set by crate thread-priority .

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

The method of setting priority is also very simple:

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 }

Or set a specific value:

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());

You can also set platform-specific priority values:

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

11 let thread2 = ThreadBuilder::default()


12
.name("MyThread")
13
.priority(ThreadPriority::Max)
14
.spawn_careless(|| {

22
Machine Translated by Google

1.11Set affinity _

15 println!("We don't care about the priority result.");


16 })
17 .unwrap();
18

19 thread1.join().unwrap();
20 thread2.join().unwrap();
21 }

Or use thread_priority::ThreadBuilderExt; to extend the ThreadBuilder support of the standard library

Keep setting the priority.

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.

Try to bind threads to the core of the same NUMA node.

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

Will exit, can be checked through JoinHandle

This error is like the following code:

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 }

If caught, the external handle cannot detect the panic :

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.13 crossbeam scoped thread

1.13 crossbeam scoped thread


crossbeam also provides the function of creating a scoped thread , which is similar to the scope function of the standard library, but
The scoped thread it creates can continue to create scoped threads:

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

Grandchild threads can also be created in child threads.

1.14 Rayon scoped thread

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.

For example, the following scope_fifo code:

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

14 }); // point end

The order of concurrent execution of its threads is similar to the following order:

1 | (start)
2 |

3 | (FIFO scope `s` created)


4 +--------------------+ (task s.1)
5 +-------+ (task s.2) |
6 | | +---+ (task s.1.1)
7 ||||||||| ||

8 (mid) | | | | | | (FIFO scope `t` created)


9 <------+-+--- | +----------------+ (task t.1)
10 + (scope `s` | +---+ (task t.2) |
11 ends) ||| |

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

1.16 Go- style startup threads

2 let counter = Rc::new(42);


3

4 let (sender, receiver) = channel();


5

6 let _t = thread::spawn(move || {
7 sender.send(counter).unwrap();
8 });
9

10 let value = receiver.recv().unwrap();


11

12 println!("received from the main thread: {}", value);


13 }

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

The library, send_wrapperhttps://crates.io/crates/send_wrapper, wraps it for implementation

Sender: Send .
1 pub fn send_wrapper() {
2 let wrapped_value = SendWrapper::new(Rc::new(42));
3

4 let (sender, receiver) = channel();


5

6 let _t = thread::spawn(move || {
7 sender.send(wrapped_value).unwrap();
8 });
9

10 let wrapped_value = receiver.recv().unwrap();


11

12 let value = wrapped_value.deref();


13 println!("received from the main thread: {}", value);
14 }

1.16 Go- style startup threads

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

5 // Spawn a thread that captures values by move.


6 go! {
7 for _ in 0..100 {
8 counter_cloned.fetch_add(1, Ordering::SeqCst);

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.

Advantages of thread pools include:

• 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

avoiding resource exhaustion and excessive competition.

• 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.

2.1 rayon thread pool

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. new() method: Create a new ThreadPoolBuilder instance.

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

Easier to identify threads.

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

Thread pool and returns a rayon::ThreadPool instance.

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

The pool will only be initialized once.

1 rayon::ThreadPoolBuilder::new().num_threads(22).build_global().unwrap();

30
Machine Translated by Google

2.1 rayon thread pool

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:

1 fn fib(n: usize) -> usize {


2 if n == 0 || n == 1 {
3 return n;
4 }
5 let (a, b) = rayon::join(|| fib(n - 1), || fib(n - 2)); // district

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::ThreadPoolBuilder is used to create a thread pool. Set up to use 8 threads

• pool.install() runs fib in the thread pool

• 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:

• Threads can be reused to avoid the overhead of frequently creating/destroying threads

• The number of threads is configurable, generally set according to the number of CPU cores

• Avoid resource competition problems caused by a large number of threads

Next, let’s look at a piece of code using build_scoped :

1 scoped_tls::scoped_thread_local!(static POOL_DATA: Vec<i32>);


2 pub fn rayon_threadpool2() {
3 let pool_data = vec![1, 2, 3];
4

5 // We haven’t assigned any TLS data yet.


6 assert!(!POOL_DATA.is_set());
7

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

16 \/\/ Once we've returned, `pool_data` is no longer borrowed. drop(pool_data);


17

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,

Thread-Local Storage) in a thread pool .

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.

4. The line rayon::ThreadPoolBuilder::new() starts to build a Rayon thread pool.

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

release pool_data to ensure that it is no longer accessed by any thread.

32
Machine Translated by Google

2.2 threadpool library

2.2 threadpool library


threadpool is a Rust library for creating and managing thread pools, making parallelizing tasks easier. Wire

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

Utilize system resources.

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

Errors that may occur during execution.

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

6 let pool = threadpool::ThreadPool::new(4);


7

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:

1 // create at least as many workers as jobs or you will deadlock yourself


2 let n_workers = 42;
3 let n_jobs = 23;
4 let pool = threadpool::ThreadPool::new(n_workers);
5 let an_atomic = Arc::new(AtomicUsize::new(0));
6

7 assert!(n_jobs <= n_workers, "too many jobs, will deadlock");


8

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);

2.3 rusty_pool library


This is an adaptive thread pool based on crossbeam multi-producer multi-consumer channel implementation. It has the following characteristics:

• Two sizes: core thread pool and maximum thread pool

• Core threads continue to survive, and additional threads have idle recycling mechanisms

• Supports waiting for task results and asynchronous tasks

• 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

• Provides JoinHandle to wait for task results

• If the task panics, the JoinHandle will receive a cancellation error

• Can be used as futures executor when asyncfeature is turned on

– 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

2.3 rusty_pool library

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

scalable Rust concurrent programs.

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.

The main steps include:

• Create rusty_pool thread pool, default configuration • Submit 10

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:

1 let handle = pool.evaluate(||


{ thread::sleep(Duration::from_secs(5)); 2 return 4;
3

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.

It mainly includes two processing methods:

a1. Create the default rusty_pool thread pool

a2. Use pool.complete to execute an async block synchronously

• You can use await in an async block to run asynchronous functions •complete will

block until the entire async block is completed

35
Machine Translated by Google

2 thread pool

• Can get the return value of async block

b1. Use pool.spawn to execute async blocks asynchronously

• spawn will immediately return a JoinHandle


• async blocks will be executed asynchronously in the thread pool

• The results are saved here through Atomics variables

b2. Call join in the main thread and wait for the asynchronous task to complete.

b3. Check the results of asynchronous tasks

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

4 let handle = pool.complete(async {


5 let a = some_async_fn(4, 6).await; // 10
6 let b = some_async_fn(a, 3).await; // 13
7 let c = other_async_fn(b, a).await; // 3
8 some_async_fn(c, 5).await // 8
9 });
10 assert_eq!(handle.await_complete(), 8);
11

12 let count = Arc::new(AtomicI32::new(0));


13 let clone = count.clone();
14 pool.spawn(async move {
15 let a = some_async_fn(3, 6).await; // 9
16 let b = other_async_fn(a, 4).await; // 5
17 let c = some_async_fn(b, 7).await; // 12
18 clone.fetch_add(c, Ordering::SeqCst);
19 });
20 pool.join();
21 assert_eq!(count.load(Ordering::SeqCst), 12);
22 }

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

2.4 fast_threadpool library

10 let count = Arc::new(AtomicI32::new(0));


11 for _ in 0..15 {
12 let clone = count.clone();
13 pool.execute(move || {
14 thread::sleep(Duration::from_secs(5));
15 clone.fetch_add(1, Ordering::SeqCst);
16 });
17 }
18

19 // ThreadPool
worker
20 pool.shutdown_join();
21 assert_eq!(count.load(Ordering::SeqCst), 15);
22 }

2.4 fast_threadpool library


This thread pool implementation is optimized for minimal latency. In particular, ensure that you do not

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

The spawn_blocking function.

1 pub fn fast_threadpool_example() -> Result<(), fast_threadpool::


ThreadPoolDisconnected>{
2 let threadpool = fast_threadpool::ThreadPool::start(ThreadPoolConfig::
default(), ()).into_sync_handler();
3

4 assert_eq!(4, threadpool.execute(|_| { 2 + 2 })?);


5

6 Ok(())
7 }

This example shows the usage of fast_threadpool crate .

The main steps include:

• Create a thread pool using default configuration

• Convert the thread pool to sync_handler for synchronous submission of tasks

• Submit a simple calculation task to the thread pool

• Collect results and verify in main thread

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
});

2.5 scoped_threadpool library


In Rust multi-threaded programming, scoped is a specific concept, which refers to a thread with a limited scope.

The main characteristics of scoped threads are:

• 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

A typical scoped thread pool usage is as follows:

1
pool.scoped(|scope| {
2
scope.execute(|| {
3

4
// });
5
}); // , Join

The advantages of scoped threads are:

• Concise code, no need to manually synchronize threads

• Scope control automatically manages thread lifetime


• Borrow checks to ensure safety

Scoped threads are suitable for:

• Short tasks that require access to shared state

• Scenarios where it is difficult to manually manage thread lifetime

• Scenarios that require high code security

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.

In this section we introduce a specialized scoped_threadpool library.

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

2.6 scheduled_thread_pool library

11 s.execute(move || {
12 *e += 1;
13 });
14 }
15 });
16

17 assert_eq!(thing, thing![1, 2, 3, 4, 5, 6, 7, 8]);


18 }

This example shows how to create a scoped thread pool using the scoped_threadpool library .

• First create a scoped thread pool, specifying the use of 4 threads

• Define a vector vec as external shared state

• 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

• After all threads are executed, all elements of vec are +1

The main features of scoped thread pool:

• Threads can directly access external state, no channel or mutex required

• Borrow checking of external state is done automatically

• 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:

• Code is more concise, no need to manually synchronize external state

• Borrow checking ensures thread safety

• Scope control automatically manages thread lifetime

The scoped thread pool provides a safer and more convenient concurrency mode, which is very suitable for use in Rust .

2.6 scheduled_thread_pool library


scheduled-thread-pool is a Rust library that provides a thread pool implementation that supports task scheduling. Down
Let me introduce its main functions and usage:

• Supports scheduled execution of tasks, no need to implement a scheduler yourself

• Provide one-time and recurring scheduling methods

• Based on the thread pool model to avoid repeated creation and destruction of threads

• Tasks can be canceled at any time

1 pub fn scheduled_thread_pool() {
2 let (sender, receiver) = channel();
3

4 let pool = scheduled_thread_pool::ScheduledThreadPool::new(4);


5 let handle = pool.execute_after(Duration::from_millis(1000), move ||{
6 println!("Hello from a scheduled thread!");
7 sender.send("done").unwrap();
8 });
9

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 .

• Create a scheduled thread pool containing 4 threads

• Use pool.execute_after to schedule a task after 1 second

• 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.

The main functions of scheduled thread pool:

• Can schedule tasks to be executed at a certain point in the future

• Provides two methods of one-time scheduling and periodic scheduling

• 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

2.7 poolite library


Poolite is a very lightweight Rust thread pool library with the following main features:

1. API is simple and easy to use

Provides basic interfaces for creating pools, adding tasks, etc.:

1 let pool = pool::Pool::new()?;


2 pool.push(|| println!("hello"));

2. Support scoped scope threads

scoped can automatically wait for tasks to complete:

1 pool.scoped(|scope| {
2 scope.push(|| println!("hello"));
3 });

3. The default number of threads is the number of CPU cores

The number of threads can be customized through Builder :

1 let pool = poolite::Pool::builder().thread_num(8).build()?;

40
Machine Translated by Google

2.7 poolite library

4. Combine with arc and mutex

Poollite also provides good support for accessing our common shared resources . The following example calculates Fibonacci

Concurrent version of the Bonacci sequence:

1 use spool::Spool;
2

3 use std::collections::BTreeMap;
4 use std::sync::{Arc, Mutex};
5

6 \/\/\/ `cargo run --example arc_mutex`


7 fn main() {
8 let pool = Pool::new().unwrap();
9 // You also can use RwLock instead of Mutex if you read more than write.
10 let map = Arc::new(Mutex::new(BTreeMap::<i32, i32>::new()));
11 for i in 0..10 {
12 let map = map.clone();
13 pool.push(move || test(i, map));
14 }
15

16 pool.join(); //wait for the pool


17

18 for (k, v) in map.lock().unwrap().iter() {


19 println!("key: {}\tvalue: {}", k, v);
20 }
21 }
22

23 fn test(msg: i32, map: Arc<Mutex<BTreeMap<i32, i32>>>) {


24 let res = fib(msg);
25 let mut maplock = map.lock().unwrap();
26 maplock.insert(msg, res);
27 }
28

29 fn fib(msg: i32) -> i32 {


30 match msg {
31 0...2 => 1,
32 x => fib(x - 1) + fib(x - 2),
33 }
34 }

5. Cooperation with mpsc

6. You can use builder to customize the pool

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

16 pool.join(); //wait for the pool


17 println!("{:?}", pool);
18 }

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.

Scenarios, such as the runtime of scripting languages, etc.

If you need a small and sophisticated Rust thread pool, poolite is a very good choice.

2.8 executor_service library


executor_service is a Rust library that provides thread pool abstraction , imitating Java 's ExecutorService.

The main features are as follows:

executor_service is a Rust library that provides thread pool abstraction . Its main features are as follows:

1.Support fixed and cached thread pools

Different types of thread pools can be created on demand:

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.

It is set during initialization, but it cannot exceed 150.

2. Provide an interface for executing tasks

Supports closure, Future and other task forms:

1 //
2 pool.execute(|| println!("hello"));
3

4 // future
5 pool.spawn(async {

42
Machine Translated by Google

2.8 executor_service library

6 // ...
7 });

3.Support obtaining task results

submit_sync can submit tasks synchronously and get the return value:

1 let result = pool.submit_sync(|| {


2 // run task
3 return result;
4 })?;

4. Provide a convenient thread pool builder

Thread pool parameters can be customized:

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

5 let mut executor_service =


6 Executors::new_fixed_thread_pool(10).expect("Failed to create the thread
pool");
7

8 let counter = Arc::new(AtomicUsize::new(0));


9

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

22 let mut executor_service = Executors::new_fixed_thread_pool(2).expect("Failed


to create the thread pool");
23

24 let some_param = "Mr White";


25 let res = executor_service.submit_sync(move || {
26

27 sleep(Duration::from_secs(5));

43
Machine Translated by Google

2 thread pool

28 println!("Hello {:}", some_param);


29 println!("Long computation finished");
30 2

31 }).expect("Failed to submit function");


32

33 println!("Result: {:#?}", res);


34 assert_eq!(res, 2);
35 }

The example does the following:

1. Create a thread pool with a fixed 10 threads

2. Submit 10 tasks, pause each task for a period of time and then add 1 to the counter.

3. Verify the counter value after the main thread is paused

4. Create a thread pool with fixed 2 threads

5. Submit a task, print messages and pause within the task

6. The main thread uses submit_sync to execute the task synchronously and obtain the return value

2.9 threadpool_executor library


threadpool_executor is a feature-rich Rust thread pool library that provides a highly configurable thread pool implementation.

now. The main features are as follows:

1. Thread pool builder

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();

2.Support different task submission methods

Closures, async blocks, callback functions, etc.:

1 //
2 pool.execute(|| println!("hello"));
3

4 //
5 pool.execute(async {
6 // ...
7 });

3. The task returns the Result type for error handling

Result<T, E> is returned after all tasks are executed :

44
Machine Translated by Google

2.9 threadpool_executor library

1 let result = pool.execute(|| {


2 Ok(1 + 2)
3 })?;
4

5 let res = result.unwrap().get_result_timeout(std::time::Duration::from_secs(3))


;

6 assert!(res.is_err());
7 if let Err(err) = res {
8 matches!(err.kind(), threadpool_executor::error::ErrorKind::TimeOut);
9 }

4. Provide task cancellation interface

Submitted tasks can be canceled at any time:

1 let mut task = pool.execute(|| {}).unwrap();


2 task.cancel();

5. Implement thread pool expansion and idle recycling

Create threads on demand and automatically recycle idle threads.

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

6 let pool = threadpool_executor::threadpool::Builder::new()


7 .core_pool_size(1)
8 .maximum_pool_size(3)
9 .keep_alive_time(std::time::Duration::from_secs(300))
10 .exeed_limit_policy(threadpool_executor::threadpool::ExceedLimitPolicy::
Wait)
11 .build();
12

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 }

The example does the following:

1. Create a single-threaded thread pool, submit a task and get the results

45
Machine Translated by Google

2 thread pool

2. Use Builder to create a configurable thread pool

• Set the number of core threads to 1 and the maximum number of

threads to 3 • Set the idle thread survival time to 300

seconds • The task overflow policy is waiting

3. Submit a long-term task to the thread pool

4. Cancel a task immediately after submitting it

Some key features of threadpool_executor :

• Provides thread pool builder for fine-grained configuration •

Supports task submission in the form of callbacks and

closures • Task returns Result for error handling •

Implements thread pool expansion and contraction and idle recycling

strategies • Provides task cancellation and thread pool shutdown interfaces

threadpool_executor provides a fully functional thread pool implementation, suitable for scenarios that require fine-grained control.

46
Machine Translated by Google

2.9 threadpool_executor library

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

3.1 Overview of 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.

Asynchronous programming has the following advantages:

• Improve system concurrency and response speed • Reduce thread waiting

time and improve resource utilization • Can handle a large number of concurrent requests

or tasks • Support efficient event-driven programming style

Asynchronous programming is widely used in the following scenarios:

• Network programming: handle a large number of concurrent network requests •I/O

-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

3.2 Asynchronous programming model in Rust

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

3 async/await asynchronous programming

The most worth reading is Rust ’s official Rust asynchronous programming book

Chinese version: Rust Asynchronous Programming Guide

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

scenarios, the thread pool is still not enough.

• 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

reduced. The famous JS once had callback hell.

• 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

languages and custom

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

units communicate and transfer data through


,
message passing, which is very similar to the design concept of a distributed system. Since the 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

the problem this chapter tries to solve.

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

discussed in multi-threading. It has been explained in depth in the chapter

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

TCP/UDP socket, thread pool, timer and other functions.

50
Machine Translated by Google

3.2 Asynchronous programming model in Rust

• async-std - Newer but full-featured runtime that provides asynchronous abstractions similar to Tokio . The code is simpler

Clean and easy to use.


• smol - A lightweight runtime that focuses on simplicity, ergonomics and small

ÿÿ
• 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.

// `foo()` `Future<Output = u8>`, `foo().await` // `Future`


u8 { 5 } async fn foo() -> `u8`

fn bar() -> impl Future<Output = u8> { `async`


// `Future<Output = u8>`
async
{ let x: u8 = foo().await; x + 5

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:

async fn foo() -> Result<u8, String> {


Ok(1)

} async fn bar() -> Result<u8, String> {


Ok(1)

} pub fn main() { let


fut = async { foo().await?;
bar().await?;
Ok(())

};
}

The above code will report an error after compilation:

error[E0282]: type annotations needed


--> src/main.rs:14:9

51
Machine Translated by Google

3 async/await asynchronous programming

| 11 | let fut = async { --- consider


| giving `fut` a type
...

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

let fut = async { foo().await?;


bar().await?;
Ok::<(), String>(()) //

};

• 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,

ultimately achieving concurrency.

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

notified and run the Future again , and so on until completion.

• Future Trait: represents the Future Trait of asynchronous tasks and provides execution and status management of asynchronous tasks.

pub trait Future { type


Output;

// Required method fn
poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

3.3 async/await syntax and usage

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.

As an example, the following example is a traditional concurrent download of web pages:

fn get_two_sites() { // let

thread_one = thread::spawn(|| download("https://course.rs")); let thread_two =


thread::spawn(|| download("https://fancy.rs"));

//
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

3 async/await asynchronous programming

The main components of Tokio include:

• tokio - core runtime, providing task scheduling, IO resources, etc. • tokio::net -

Implementation of asynchronous TCP and UDP . • tokio::sync -

Concurrency primitives such as mutexes, semaphores, etc. • tokio::time -

time related tools. • tokio::fs - Asynchronous file IO.

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.

You can also use the explicitly created runtime method:

pub fn tokio_async() { let rt =


tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!
("Hello from tokio!");

rt.spawn(async
{ println!("Hello from a tokio task!"); println!("in
spawn")

}) .await .unwrap();
});

rt.spawn_blocking(|| println!("in spawn_blocking"));


}

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

here to wait for the task to end.

Finally, use spawn_blocking to perform a normal blocking task at runtime. This task will run in the thread pool without blocking the runtime.

To summarize the key points demonstrated in this example:

• Use block_on to execute asynchronous tasks in the Tokio runtime • Use spawn to

execute tasks asynchronously in the runtime • Use spawn_blocking

to execute blocking tasks in the thread pool • You can awaitJoinHandle to wait for the end

of the asynchronous task

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.

Mainly provides the following functions:

• 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

futures . • Utilities - functions for composing and creating futures

pub fn futures_async() {
let pool = ThreadPool::new().expect("Failed to build pool"); let (tx, rx) =
mpsc::unbounded::<i32>();

let fut_values = async { let


fut_tx_result = async move { (0..100).for_each(|
v| {
tx.unbounded_send(v).expect("Failed to send");
})
};
pool.spawn_ok(fut_tx_result);

let fut_values = rx.map(|v| v * 2).collect();

fut_values.await
};

55
Machine Translated by Google

3 async/await asynchronous programming

let values: Vec<i32> = executor::block_on(fut_values);

println!("Values={:?}", values);
}

This example shows how to use futures and thread pools for asynchronous programming:

1. Create a thread pool pool 2.


Create an unbounded channel tx and rx to transfer data between tasks 3. Define
an asynchronous task fut_values, which first uses spawn_ok to asynchronously execute a task in the thread pool
A task, this task will send numbers 0-99 through the channel. 4. Then create a

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

thread and obtains the results.

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,

and removes most of the unsafe code.

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;

async fn hello_async() { println!


("Hello, async world!");
}

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/

O, network operations, and task management in an asynchronous context more convenient.

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;

async fn hello_async() { println!


("Hello, async world!");
}

fn main()
{ task::block_on(hello_async());
}

This example first imports async_std::task.

Then define an asynchronous function hello_async, which simply prints a sentence.

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

async/await to write asynchronous Rust code very easily .

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

to manage asynchronous tasks.

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

3 async/await asynchronous programming

• 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") });
}

3.9 try_joinÿjoinÿselect ÿ zip


In Rust , there are two common macros that can be used to wait for multiple futures at the same time: select and join.

The select! macro can wait for multiple futures at the same time and only process the future that completes first :

use futures::future::{select, FutureExt};

let future1 = async { /* future 1 */ }; let future2 =


async { /* future 2 */ };

let result = select! {


res1 = future1 => { /* handle result of future1 */ }, res2 = future2 => { /*
handle result of future2 */ },
};

The join! macro can wait for multiple futures at the same time and process the results of all futures :

use futures::future::{join, FutureExt};

let future1 = async { /* future 1 */ }; let future2 =


async { /* future 2 */ };

let (res1, res2) = join!(future1, future2);

join! returns a tuple containing 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

3.9 try_joinÿjoinÿselect ÿ zip

use futures::try_join;

let future1 = async {


Ok::<(), Error>(/*...*/) };

let future2 = async {


Err(Error::SomethingBad) };

let result = try_join!(future1, future2);

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};

let future1 = async { 1 }; let future2


= async { 2 };

let result = zip(future1, future2); println!


("smol_zip: {:?}", result.await);

let future1 = async { Ok::<i32, i32>(1) }; let future2 =


async { Err::<i32, i32>(2) };

let result = try_zip(future1, future2).await; println!("smol_try_zip:


{:?}", result);
});
}

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

data to provide other richer functions.

4.1 cow

Cow is not, but the abbreviation of clone-on-write or copy-on-write .

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:

let s1 = String::from("hello"); let s2 = s1; //


s1 s2

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

4 Container synchronization primitives

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

bring significant performance improvements.

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:

let origin = "hello world"; let mut


cow = Cow::from(origin); assert_eq!(cow,
"hello world");

// Cow can be borrowed as a str let s:


&str = &cow; assert_eq!
(s, "hello world");

assert_eq!(s.len(), cow.len());

// Cow can be converted to a String let s:


String = cow.into(); assert_eq!(s,
"HELLO WORLD");

Next we have an example of cloning while writing . The following example changes all characters in a string to uppercase letters:

// Cow can be borrowed as a mut str let s:


&mut str = cow.to_mut();
s.make_ascii_uppercase();
assert_eq!(s, "HELLO WORLD");
assert_eq!(origin, "hello world");

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.

So if you want to implement copy-on-write/clone-on-write functionality on some data , you may


consider using std::borrow::Cow.

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:

pub fn beef_cow() { let


borrowed: beef::Cow<str> = beef::Cow::borrowed("Hello"); let owned: beef::Cow<str>
= beef::Cow::owned(String::from("World")); let _
= beef::Cow::from("Hello");

assert_eq!(format!("{} {}!", borrowed, owned), "Hello World!",);

const WORD: usize = size_of::<usize>();

assert_eq!(size_of::<std::borrow::Cow<str>>(), 3 * WORD); assert_eq!


(size_of::<beef::Cow<str>>(), 3 * WORD); assert_eq!
(size_of::<beef::lean::Cow<str>>(), 2 * WORD);
}

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:

let val: u8 = 5; let


boxed: Box<u8> = Box::new(val);

So how do you do the opposite? The following example moves a value from the heap to the stack via dereference:

let boxed: Box<u8> = Box::new(5); let val: u8


= *boxed;

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

4 Container synchronization primitives

Cons(T, List<T>),
Nil,
}

Now you can use Box :

#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}

let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil)))); println!("{list:?}");

Currently, Rust also provides an experimental type ThinBox, which is a thin pointer, regardless of the type of the internal element:

pub fn thin_box_example() { use


std::mem::{size_of, size_of_val}; let size_of_ptr =
size_of::<*const ()>();

let box_five = Box::new(5); let


box_slice = Box::<[i32]>::new_zeroed_slice(5); assert_eq!(size_of_ptr,
size_of_val(&box_five)); assert_eq!(size_of_ptr * 2,
size_of_val(&box_slice));

let five = ThinBox::new(5); let


thin_slice = ThinBox::<[i32]>::new_unsize([1, 2, 3, 4]); assert_eq!(size_of_ptr,
size_of_val(&five)); assert_eq!(size_of_ptr,
size_of_val(&thin_slice));
}

4.3 CellÿRefCellÿOnceCellÿLazyCell ÿ LazyLock


Cell and RefCell are two important types used for interior mutability in Rust .

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

Cell nor RefCell are thread-safe (they do not implement Sync).

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

4.3 CellÿRefCellÿOnceCellÿLazyCell ÿ LazyLock

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

method, and its value can be modified even if there is a reference y to x :

use std::cell::Cell;

let x = Cell::new(42); let y =


&x;

x.set(10); //

println!("y: {:?}", y.get()); // y: 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);

let y = x.borrow(); // println!

("y: {:?}", *y.borrow());


}

let mut z = x.borrow_mut(); // *z = 10;

println!("x: {:?}", x.borrow().deref());

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

4 Container synchronization primitives

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

let value: &String = cell.get_or_init(|| "Hello, World!".to_string()); assert_eq!(value, "Hello,


World!"); assert!(cell.get().is_some()); //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.

Here's an example of using it:

#![feature(lazy_cell)]

use std::cell::LazyCell;

let lazy: LazyCell<i32> = LazyCell::new(|| { println!


("initializing"); 46

});
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

static HASHMAP: LazyLock<HashMap<i32, String>> = LazyLock::new(|| { println!("initializing");


let mut m = HashMap::new();
m.insert(13, "Spica".to_string());
m.insert(74, "Hoyten".to_string());

});

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

or Mutex . • Rc needs extra

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;

let data = Rc::new(42);

let reference1 = Rc::clone(&data); let reference2


= Rc::clone(&data);

// 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

4 Container synchronization primitives

If you still want to modify the data, you can use the Cell- related types in the previous section. For example, in the following

example, we use the Rc<RefCell<HashMap>> type to achieve this requirement:

pub fn rc_refcell_example() { let


shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new())); {

let mut map: RefMut<_> = shared_map.borrow_mut();


map.insert("africa", 92388);
map.insert("kyoto", 11837);
map.insert("piccadilly", 11826);
map.insert("marbles", 38);
}

let total: i32 = shared_map.borrow().values().sum(); println!("{total}");

In this way, we achieve data variability for the immutable type Rc .

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

shared resources needs to be in a certain order.

In order to achieve these synchronization requirements, synchronization primitives need to be used. Common synchronization primitives include mutex locks, semaphores,

condition variables, etc.

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

handle synchronization needs between multi-threads correctly and effectively.

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

5 basic synchronization primitives

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.

Here is a simple example demonstrating how to use Arc:

use std::sync::Arc; use


std::thread;

fn main() { //
let
data = Arc::new(46);

// data
let thread1 = { let
data = Arc::clone(&data);
thread::spawn(move || { // data
println!("Thread 1: {}",
data);
})
};

let thread2 = { let


data = Arc::clone(&data);
thread::spawn(move || { // data
println!("Thread 2: {}", 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

operations are generally more expensive than non-atomic operations.

70
Machine Translated by Google

5.1 Arc

– Rc performs better in a single-threaded environment because it does not require atomic

operations. •

Variability: – Arc cannot be used with variable data. If you need to share mutable data in a multi-threaded environment, you usually make

Use Mutex, RwLock , etc. to synchronize primitives and Arc.


– Rc also cannot be used for mutable data because it does not provide safety from concurrent access.

• Behavior when reference count is decremented:

– 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-

threading because it does not take concurrency into account.

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:

use std::sync::{Arc, Mutex}; use


std::thread;

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;

});
handles.push(handle);
}

//
for handle in handles {
handle.join().unwrap();
}

//
println!("Final count: {}", *counter.lock().unwrap());
}

71
Machine Translated by Google

5 basic synchronization primitives

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:

use std::sync::{Arc}; use


std::cell::RefCell; use std::thread;

fn main() { //
let
counter = Arc::new(RefCell::new(0));

//
let mut handles = vec![];

for _ in 0..5 { let


counter = Arc::clone(&counter); let handle =
thread::spawn(move || { RefCell
//
let mut num = counter.borrow_mut(); *num += 1;

});
handles.push(handle);
}

//
for handle in handles {
handle.join().unwrap();
}

//
println!("Final count: {}", *counter.borrow());
}

5.2 Mutex lock Mutex


Mutex locks have a long history and are implemented in many programming languages.

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 Mutex lock Mutex

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:

use std::sync::{Mutex, Arc}; use


std::thread;

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;

});
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

5 basic synchronization primitives

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.

Here is a simple example using try_lock :

use std::sync::{Mutex, Arc}; use


std::thread;

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 || {
//
if let Ok(mut num) = counter.try_lock() {
*num += 1; }
else
{ println!("Thread failed to acquire lock.");
}

});
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

5.2 Mutex lock Mutex

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:

use std::sync::{Mutex, Arc, LockResult, PoisonError}; use std::thread;

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 result: LockResult<_> = counter.lock();

//
match result
{ Ok(mut num) =>
{ *num +=
1; // panic if *num
== 3 { panic!
("Simulated panic!");
}

Err(poisoned) => {

75
Machine Translated by Google

5 basic synchronization primitives

// "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.

5.2.4 Release mutex locks faster

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

leaving the scope . :

use std::sync::{Mutex, Arc}; use


std::thread;

fn main() { //
let
counter = Arc::new(Mutex::new(0));

//
let mut handles = vec![];

76
Machine Translated by Google

5.2 Mutex lock Mutex

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 :

use std::sync::{Mutex, Arc};


use std::thread;

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

5 basic synchronization primitives

// !!!!!!!!
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.

5.3 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.

Read-write locks are generally used in the following scenarios:

• 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

RWMutex can provide more flexible control when writing is allowed

The following is an example of using RWMutex :

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


std::thread;

fn main() { //
let RwLock
counter = Arc::new(RwLock::new(0));

78
Machine Translated by Google

5.3 Read-write lock RWMutex

//
let mut read_handles = vec![];

for _ in 0..3 { let


counter = Arc::clone(&counter); let handle =
thread::spawn(move || {
//
let num = counter.read().unwrap(); println!
("Reader {}: {}", thread::current().id(), *num);
});
read_handles.push(handle);
}

//
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.

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


std::thread;

fn main() {

79
Machine Translated by Google

5 basic synchronization primitives

// 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));

// // let mut num = counter.write().unwrap();


*num += 1;
println!("Writer {}: Incremented counter to {}", thread::current().id()
})
};

//
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

5.3 Read-write lock RWMutex

let counter = counter.clone();


thread::spawn(move || {
//
let num = counter.read().unwrap();
println!("Reader#1: {}", *num);

//
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

5 basic synchronization primitives

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:

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


use std::thread;

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

5.4 Once initialization Once

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.

5.4 Once initialization Once

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.

Here is an example using Once :

use std::sync::{Once, ONCE_INIT};

static INIT: Once = ONCE_INIT;

fn main() { //
call_once
INIT.call_once(|| { // println!

("Initialization code executed!");


});

// 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

during the entire program life cycle.

83
Machine Translated by Google

5 basic synchronization primitives

The following example is an example with a return value to implement lazy loading of global configuration scenarios:

use std::sync::{Once, ONCE_INIT};

static mut GLOBAL_CONFIG: Option<String> = None; static


INIT: Once = ONCE_INIT;

fn init_global_config() { unsafe

{ GLOBAL_CONFIG = Some("Initialized global configuration".to_string());


}

fn get_global_config() -> &'static str {


INIT.call_once(|| 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

can store any type of data.

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;

static CELL: OnceCell<String> = OnceCell::new(); assert!


(CELL.get().is_none());

84
Machine Translated by Google

5.5 Barrier/ Barrier

std::thread::spawn(|| {
let value: &String = CELL.get_or_init(|| { "Hello,
World!".to_string()
});
assert_eq!(value, "Hello, World!"); }).join().unwrap();

let value: Option<&String> = CELL.get(); assert!


(value.is_some()); assert_eq!
(value.unwrap().as_str(), "Hello, World!");

5.5 Barrier/ Barrier


Barrier is a concurrency primitive in the Rust standard library that is used to create a synchronization point between multiple threads. It

allows multiple threads to wait at a certain point until all threads have reached that point, and then they can continue executing simultaneously.

Here is an example using Barrier :

use std::sync::{Arc, Barrier}; use


std::thread;

fn main() { //
let Barrier
barrier = Arc::new(Barrier::new(3)); // 3

//
let mut handles = vec![];

for id in 0..3 { let


barrier = Arc::clone(&barrier); let handle =
thread::spawn(move || {
//
println!("Thread {} working", id);
thread::sleep(std::time::Duration::from_secs(id as u64));

//
barrier.wait();

//
println!("Thread {} resumed", id);
});

handles.push(handle);
}

85
Machine Translated by Google

5 basic synchronization primitives

//
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 ,

they continue execution at the same time.

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

reset and can be used again. This reset operation is automatic.

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 :

let barrier = Arc::new(Barrier::new(10)); let mut handles


= vec![];

for _ in 0..10 { let


barrier = barrier.clone();
handles.push(thread::spawn(move || {
println!("before wait1"); let dur =
rand::thread_rng().gen_range(100..1000);
thread::sleep(std::time::Duration::from_millis(dur));

//step1
barrier.wait(); println!
("after wait1");
thread::sleep(time::Duration::from_secs(1));

//step2
barrier.wait(); println!
("after wait2"); }));

86
Machine Translated by Google

5.6 Condition variable Condvar

for handle in handles {


handle.join().unwrap();
}

5.6 Condition variable Condvar

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

thread can be awakened and continue execution.

Here is an example of Condvar :

use std::sync::{Arc, Mutex, Condvar}; use


std::thread;

fn main() { //
let Mutex Condvar
mutex = Arc::new(Mutex::new(false)); let condvar =
Arc::new(Condvar::new());

//
let mut handles = vec![];

for id in 0..3 { let


mutex = Arc::clone(&mutex); let condvar =
Arc::clone(&condvar);

let handle = thread::spawn(move || {


// Mutex
let mut guard = mutex.lock().unwrap();

// true
while !*guard { guard
= condvar.wait(guard).unwrap();
}

//
println!("Thread {} woke up", id);
});

handles.push(handle);
}

87
Machine Translated by Google

5 basic synchronization primitives

//
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.

5.7 LazyCell and LazyLock


We introduced OnceCell and OnceLock, and we will introduce two similar concurrency primitives for lazy
loading, LazyCell and LazyLock.

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.

Type usage Initialization timing thread safety

LazyCell lazy initialization value first access no

88
Machine Translated by Google

5.8 Exclusive

Type usage Initialization timing thread safety

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 .

let mut exclusive = Exclusive::new(92); println!


("ready");
std::thread::spawn(move || { let
counter = exclusive.get_mut(); println!("{}",
*counter); *counter =
100; }).join().unwrap();

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

5 basic synchronization primitives

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

each sender atomically delivering a message to the receiver.

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.

A simple channel example is as follows:

use std::thread; use


std::sync::mpsc::channel;

// Create a simple streaming channel let (tx, rx) =


channel(); thread::spawn(move|| {

tx.send(10).unwrap();
});
assert_eq!(rx.recv().unwrap(), 10);

An example of multiple producers and single consumer:

use std::thread; use


std::sync::mpsc::channel;

// 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();
});
}

for _ in 0..10 { let j =


rx.recv().unwrap(); assert!(0 <= j &&
j < 10);
}

An example of a synchronous channel :

use std::sync::mpsc::sync_channel; use


std::thread;

90
Machine Translated by Google

5.9 mpsc

let (tx, rx) = sync_channel(3);

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());

// Drop the last sender to stop `rx` waiting for message.


// The program will not complete if we comment this out.
// **All** `tx` needs to be dropped for `rx` to have `Err`. drop(tx);

// Unbounded receiver waiting for all senders to complete. while let Ok(msg)
= rx.recv() { println!("{msg}");

println!("completed");

When the sender is released, the receiver will receive an error:

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 ,

such as oneshot, broadcaster, mpmc, etc.

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 basic synchronization primitives

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),

so I will not introduce them specifically in this chapter.

This chapter still focuses on the introduction of the concurrency primitives of the standard library.

5.11 Atomic operation atomic

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

performed safely in a multi-threaded environment without using additional locks.

Atomic can be used in various scenarios, such as: - Guaranteeing the consistency of a certain value. -Prevent multiple threads from modifying a value at

the same time. -Implement mutex lock.

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.

Figure 5.1. Processes and 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

nomicon for more information.

92
Machine Translated by Google

5.11 Atomic operation atomic

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:

// i64 AtomicI64 pub


unsafe fn from_ptr<'a>(ptr: *mut i64) -> &'a AtomicI64 pub const fn
as_ptr(&self) -> *mut i64 pub fn get_mut(&mut self)
-> &mut i64 pub fn from_mut(v: &mut i64) ->
&mut AtomicI64 pub fn get_mut_slice(this: &mut [AtomicI64])
-> &mut [i64] pub fn from_mut_slice(v: &mut [i64]) -> &mut [AtomicI64] pub
fn into_inner(self) -> i64

//
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

) -> Result<i64, i64> pub fn


compare_exchange_weak( &self,
current:
i64, new: i64,
success:
Ordering, failure:
Ordering
) -> Result<i64, i64> pub fn
fetch_add(&self, val: i64, order: Ordering) -> i64 pub fn fetch_sub(&self, val:
i64, order: Ordering) -> i64 pub fn fetch_and(&self, val: i64, order: Ordering)
-> i64 pub fn fetch_nand(&self, val: i64, order: Ordering) -> i64

93
Machine Translated by Google

5 basic synchronization primitives

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

) -> Result<i64, i64> where

F: FnMut(i64) -> Option<i64>,


pub fn fetch_max(&self, val: i64, order: Ordering) -> i64 pub fn fetch_min(&self,
val: i64, order: Ordering) -> i64

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

The following example demonstrates the basic atomic operations of AtomicI64 :

use std::sync::atomic::{AtomicI64, Ordering};

let atomic_num = AtomicI64::new(0);

//
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 -

swap: atomic exchange - store: atomic storage

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

5.11 Atomic operation atomic

5.11.1 Ordering of atomic operations

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

correspondence with memory order in C++ :

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

performed after the current operation.

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.

• C++ (memory_order_seq_cst): In C++ , memory_order_seq_cst also represents a total order operation,


ensuring that all threads can see a consistent order of operations. It is the strongest memory ordering in
C++ .

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

5 basic synchronization primitives

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

to obtain higher performance.

The following is a simple example demonstrating the use of Ordering::Relaxed :

use std::sync::atomic::{AtomicBool, Ordering}; use std::thread;

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.

Here is a simple example using Ordering::Acquire :

use std::sync::atomic::{AtomicBool, Ordering};

96
Machine Translated by Google

5.11 Atomic operation atomic

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
// //
}

println!("Received value: true");


});

//
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

The work is visible to other threads.

Here is a simple example using Ordering::Release :

use std::sync::atomic::{AtomicBool, Ordering};


use std::thread;

fn main() {
//
let atomic_bool = AtomicBool::new(false);

// true
let producer_thread = thread::spawn(move || {

97
Machine Translated by Google

5 basic synchronization primitives

// true
atomic_bool.store(true, Ordering::Release);
});

//
let consumer_thread = thread::spawn(move || {
// true
while !atomic_bool.load(Ordering::Acquire) {
Release
// //
}

println!("Received value: true");


});

//
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.

use std::sync::atomic::{AtomicBool, Ordering};


use std::thread;

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

5.11 Atomic operation atomic

//
let consumer_thread = thread::spawn(move || {
// true
while !atomic_bool.load(Ordering::AcqRel) {
AcqRel
// //
}

println!("Received value: true");


});

//
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

Write operations from the producer thread are correctly observed.

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.

Here is a simple example using Ordering::SeqCst :

use std::sync::atomic::{AtomicBool, Ordering};


use std::thread;

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

5 basic synchronization primitives

while !atomic_bool.load(Ordering::SeqCst) {
SeqCst
// //
}

println!("Received value: true");


});

//
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.

In Rust , Ordering::Acquire memory ordering is often used in conjunction with Ordering::Release .

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

In addition, Ordering::AcqRel is often used to have both semantics.

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,

V>, BTreeSet<T> , etc. Their characteristics are as follows:

• Vec - This is a variable size array that allows efficient addition and removal of elements at the head or tail. Other categories

Similar to C++ 's vector or Java 's ArrayList.

• 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.

• HashSet - This is a hash-based set that can quickly determine

whether a value is in the set. It is similar to

C++ 's unordered_set or Java 's HashSet.


• VecDeque - This is a double-ended queue that allows efficient addition and removal of elements from the head or tail. Other categories

Similar to C++ 's deque or Java 's ArrayDeque.

• 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

underlying data structure. • BTreeSet - This is an ordered set,

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

introduced earlier to wrap these types and make them thread-safe.

6.1 Thread-safe Vec


To implement a thread-safe Vec, you can use a combination of Arc (atomic reference counting) and Mutex (mutex lock).

Arc allows multiple threads to share ownership of the same data, while Mutex is used to synchronize when accessing data, ensuring that only

one thread can modify the data.

101
Machine Translated by Google

6 concurrent collections

Here is a simple example demonstrating how to create a thread-safe Vec:

use std::sync::{Arc, Mutex}; use


std::thread;

fn main() { //
let Arc Mutex A thing

shared_vec = Arc::new(Mutex::new(Vec::new()));

// Vec let mut handles = vec![]; for


i in 0..5 { let shared_vec =

Arc::clone(&shared_vec); let handle = thread::spawn(move ||


{
//
let mut vec = shared_vec.lock().unwrap();

// Object
vec.push(i);
});
handles.push(handle);
}

//
for handle in handles {
handle.join().unwrap();
}

// A thing

let final_vec = shared_vec.lock().unwrap(); println!("Final


Vec: {:?}", *final_vec);
}

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.

6.2 Thread-safe HashMap


To implement a thread-safe HashMap, you can use a combination of Arc (atomic reference counting) and Mutex
(mutex lock), or use RwLock (read-write lock) to provide more fine-grained concurrency control. Here is a simple
example using Arc and Mutex :

102
Machine Translated by Google

6.2 Thread-safe HashMap

use std::collections::HashMap; use


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

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

thread can modify data at any time.

The following collection types can be implemented in this way.

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 is an implementation of concurrent associative array/hashmap 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.

DashMap is very performance focused and aims to be as fast as possible.

let map = Arc::new(DashMap::new()); let mut handles


= vec![];

for i in 0..10 { let map =


Arc::clone(&map);
handles.push(std::thread::spawn(move || { map.insert(i, i); }));

for handle in handles {


handle.join().unwrap();
}

println!("DashMap: {:?}", map);

Based on DashMap, it also implements DashSet

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.

• Per-Object Thread-Local Storage • Channels


(SPSC, MPSC, SPMC, MPMC) • Map • Set • Stack

• 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.

Here is an example using the cuckoofilter library:

let value: &str = "hello world";

// Create cuckoo filter with default max capacity of 1000000 items let mut cf =
CuckooFilter::new();

// Add data to the filter


cf.add(value).unwrap();

// Lookup if data is in the filter let success =


cf.contains(value); assert!(success);

// Test and add to the filter (if data does not exists then add) let success =
cf.test_and_add(value).unwrap(); assert!(!success);

// Remove data from the filter. let success


= cf.delete(value); 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.

Here is a simple example demonstrating how to use evmap:

let (book_reviews_r, book_reviews_w) = evmap::new();

105
Machine Translated by Google

6 concurrent collections

// start some writers. // since


evmap does not support concurrent writes, we need // to protect the write handle
by a mutex. let w = Arc::new(Mutex::new(book_reviews_w));
let writers: Vec<_> = (0..4) .map(|i| {

let w = w.clone();
std::thread::spawn(move || {
let mut w = w.lock().unwrap(); w.insert(i,
true); w.refresh();

})

}) .collect();

// eventually we should see all the writes while


book_reviews_r.len() < 4
{ std::thread::yield_now();
}

// all the threads should eventually finish writing for w in writers.into_iter()


{ assert!(w.join().is_ok());

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

of readers may block updates for any length of time.

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ÿ

Here is an example of arc-swap :

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

creating, controlling, and interacting with external processes.

7.1 Create process

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("-

l") .output() .expect("Failed to execute command");

println!("Output: {:?}", output);


}

This example runs the ls -l command and prints the output of the command.

7.2 Wait for the process to end

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 =

Command::new("ls") .spawn() .expect("Failed to start command");

let status = child.wait().expect("Failed to wait for command"); println!("Command exited


with: {:?}", status);
}

7.3 Configure input and output

You can configure the standard input, standard output, and standard error streams of a process through the stdin, stdout , and stderr

methods.

use std::process::{Command, Stdio};

fn main() {
let output = Command::new("echo") .arg("Hello,

Rust!") .stdout(Stdio::piped()) .output() .expect("Failed to execute command");

println!("Output: {:?}", String::from_utf8_lossy(&output.stdout));


}

In this example, stdout is configured as a pipe, and we read the output of the process.

7.4 Environment variables

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",

"HelloRust") .output() .expect("Failed to execute command");

println!("Output: {:?}", String::from_utf8_lossy(&output.stdout));


}

110
Machine Translated by Google

7.5 Set working directory

In this example, an environment variable named MY_VAR is set .

7.5 Set working directory

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");

println!("Output: {:?}", String::from_utf8_lossy(&output.stdout));


}

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.

7.6 Set the UID and GID of the process

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

.expect("Failed to execute command");

println!("Output: {:?}", String::from_utf8_lossy(&output.stdout));


}

In this example, the uid and gid methods are used to set the UID and GID of the process. You need to replace them

for the UID and GID you actually want to use .

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.

7.7 Pass files opened to child processes

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:

use std::process::{Command, Stdio};


use std::net::{TcpListener, TcpStream};
use std::os::unix::io::AsRawFd;
use nix::unistd::{dup2, close, ForkResult};

fn main() {
// TCP

let listener = TcpListener::bind("127.0.0.1:8080").expect("Failed to bind to ad

//
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

7.8 Controlling child processes

dup2(backup_fd, socket_fd).expect("Failed to restore file descriptor");

//
close(backup_fd).expect("Failed to close backup file descriptor");

// // let mut buffer = [0; 1024]; let _ =


TcpStream::from_raw_fd(socket_fd).read(&mut buffer); println!("Child process.
Received: {:?}", String::from_utf8_lossy(&buffer))
}

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 .

7.8 Controlling child processes

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.

Here are some common operations:

7.8.1 Wait for the child process to end

use std::process::Command;

fn main() {
let mut child = Command::new("echo") .arg("Hello,
Rust!") .spawn() .expect("Failed
to start
command");

let status = child.wait().expect("Failed to wait for command"); println!("Command exited


with: {:?}", status);
}

113
Machine Translated by Google

7 processes

7.8.2 Sending signals to child processes

use std::process::{Command, Stdio};

fn main() {
let mut child =

Command::new("sleep") .arg("10") .stdout(Stdio::null()) .spawn() .expect("Failed to start command");

//
child.kill().expect("Failed to send signal");
}

7.8.3 Interacting with child processes through standard input and output

use std::process::{Command, Stdio}; use


std::io::Write;

fn main() {
let mut child =

Command::new("cat") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Failed to start comm

if let Some(mut stdin) = child.stdin.take() {


stdin.write_all(b"Hello, Rust!\n").expect("Failed to write to stdin");
}

let output = child.wait_with_output().expect("Failed to wait for command"); println!("Output: {:?}",


String::from_utf8_lossy(&output.stdout));
}

7.9 Implementing pipelines

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

7.10 I/O interaction with child processes

let producer = Command::new("echo") .arg("Hello,

Rust!") .stdout(Stdio::piped()) .spawn() .expect("Failed to start producer command");

//
let consumer = Command::new("grep")

.arg("Rust") .stdin(producer.stdout.unwrap()) .output() .expect("Failed to start consumer command");

//
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";

let output = Command::new("sh")


.arg("-

c") .arg(command) .output() .expect("Failed to execute command");

println!("Output: {:?}", String::from_utf8_lossy(&output.stdout));


}

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.

7.10 I/O interaction with child processes

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

a pipe is to be created and used as the corresponding standard stream.

Here is a simple example demonstrating how to use Stdio::piped():

use std::process::{Command, Stdio}; use


std::io::Write;

fn main() { //
let
mut child = Command::new("echo") .arg("Hello,

Rust!") .stdout(Stdio::piped()) .spawn() .expect("Failed to start command");

//
let mut output = String::new();
child.stdout.unwrap().read_to_string(&mut output).expect("Failed to read from s

println!("Output: {:?}", output);


}

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.

Stdio::null() is another member of the std::process::Stdio enumeration, which represents a special


standard input, standard output, or standard error, that is, a null device .

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

immediately returns EOF (End of File).

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.

Here is a simple example demonstrating how to use Stdio::null():

use std::process::{Command, Stdio};

fn main() { //
let
mut child = Command::new("echo") .arg("Hello,
Rust!") .stdout(Stdio::null())

116
Machine Translated by Google

7.10 I/O interaction with child processes

.spawn() .expect("Failed to start command");

//
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

channel, the sender loses ownership of the value.

• Channels can be set up to be synchronous or asynchronous. Synchronous channels block the sender when no receiver is ready.

Asynchronous channels buffer unprocessed messages in the

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

receiver. It has the following main features:

• 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

to a very high level.

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

consumer) at the same time .

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

atomically delivering a message to the receiver.

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 .

Here is an example of a single producer and a single consumer:

use std::sync::mpsc; use


std::thread;

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

let received_message = receiver.recv().expect("Failed to receive message"); println!("Received


message: {}", received_message);
}

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.

use std::sync::mpsc; use


std::thread;

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:

let (tx, rx) = sync_channel(3);

for _ in 0..3 { let tx


= tx.clone(); // cloned tx
dropped within thread thread::spawn(move ||
tx.send("ok").unwrap());
}

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.

The following is a simple example of a synchronized channel buffer of 0 :

use std::sync::mpsc; use


std::thread;

fn main() { //
let 0

(sender, receiver) = mpsc::sync_channel::<i32>(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:

use crossbeam_channel::{bounded, Sender, Receiver}; use std::thread;

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.

Here is a simple example using unbounded channels:

use crossbeam_channel::{unbounded, Sender, Receiver}; use std::thread;

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:

use crossbeam_channel::{unbounded, select, Sender, Receiver}; use std::thread;

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!

{ recv(receiver1) -> msg1 => { match msg1


{
Ok(msg) => println!("Received from Channel 1: {}", msg), Err(_) => println!
("Channel 1 closed"),
}

} recv(receiver2) -> msg2 => { match


msg2 {
Ok(msg) => println!("Received from Channel 2: {}", msg), Err(_) => println!
("Channel 2 closed"),

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

operations based on the event. It also provides a default branch:

use crossbeam_channel::{select, unbounded};

let (s1, r1) = unbounded(); let (s2, r2)


= unbounded(); s1.send(10).unwrap();

// 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 => {

assert_eq!(res, Ok(())); assert_eq!


(r2.recv(), Ok(20));

} default => println!("not ready"),


}

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.

use crossbeam_channel::{after, select}; use


std::time::Duration;

fn main() {
let timeout = Duration::from_secs(2);

126
Machine Translated by Google

8.2 crossbeam-channel

let timeout_channel = after(timeout);

loop
{ select!
{ recv(timeout_channel) -> println! _
=> {
("Timeout reached!"); break;

} default => { //

println!("Performing other operations...");


}

at creates a channel that will generate an event after a specified point in time:

use crossbeam_channel::{at, select}; use std::time::


{Duration, Instant};

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;

neverCreate a channel that never generates events:

use crossbeam_channel::{never, select};

fn main() { let
never_channel = never();

loop
{ select! {

127
Machine Translated by Google

8 channels

recv(never_channel) -> // _
=> {

unreachable!();

} default => { //

println!("Performing other operations...");


}

tick creates a timed trigger channel that generates an event every specified time:

use crossbeam_channel::{tick, select}; use


std::time::Duration;

fn main() {
let tick_interval = Duration::from_secs(1); let ticker =
tick(tick_interval);

for _ in 0..5 { select!


{ recv(ticker)
-> => { println!("Tick!"); _

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.

• Feature rich: unlimited, finite and rendezvous queues


available • Fast: performance is always faster than std::sync::mpsc and sometimes faster than crossbeam-
channel • Safe: no unsafe code in the entire code base! •
Flexible: Sender and Receiver both implement Send + Sync + Clone • Familiar: can
seamlessly replace std::sync::mpsc • Powerful: additional
support for functions like MPMC and send timeout/deadline • Simple: fewer
dependencies, streamlined code base, compilation
Fast • Asynchronous: supports async and can be mixed with
synchronous code • Humanized: provides a powerful selective interface

128
Machine Translated by Google

8.3 flume

Here is an example using flume :

pub fn flume_example() { let (tx,


rx) = flume::unbounded();

thread::spawn(move || {
(0..10).for_each(|i| {
tx.send(i).unwrap();
})
});

let received: u32 = rx.iter().sum();

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

from the queue and calculates the sum.

When using the flume library to create a bounded queue, you can use the bounded function to specify the capacity of the queue. Here

is an example of a bounded queue:

use flume::{Sender, Receiver, bounded}; use


std::thread;

fn main() { //
let 3

(sender, receiver): (Sender<i32>, Receiver<i32>) = bounded(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));
}

});

let consumer = thread::spawn(move || { for _ in 0..5


{ let data =
receiver.recv().unwrap(); println!("Received: {}",
data);
}

});

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:

let (tx0, rx0) = flume::unbounded(); let (tx1, rx1) =


flume::unbounded();

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();

An asynchronous example, Sender provides send_async, and Receiver provides recv_async:

let rt = tokio::runtime::Runtime::new().unwrap();

let (tx, rx) = flume::unbounded();

rt.block_on(async move {
tokio::spawn(async move
{ tx.send_async(5).await.unwrap();
});

println!("flume async rx: {}", rx.recv_async().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,

but remaining messages can still be received.

Channels can also be closed manually by calling Sender::close() or Receiver::close() .

let rt = tokio::runtime::Runtime::new().unwrap();

let (tx, rx) = async_channel::unbounded();

rt.block_on(async move {
tokio::spawn(async move {
tx.send(5).await.unwrap();
});

println!("rx: {}", rx.recv().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();

let (tx, mut rx) = futures_channel::mpsc::channel(3);

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

producers to complete and output Task completion information.

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

mpsc (multiple producers single consumer).

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.

Let’s look at an example of futures_channel::oneshot :

use futures::channel::oneshot; use


std::time::Duration;

let (sender, receiver) = oneshot::channel::<i32>();

thread::spawn(|| { println!
("THREAD: sleeping zzz...");
thread::sleep(Duration::from_millis(1000)); println!("THREAD:
i'm awake! sending."); sender.send(3).unwrap();

});

println!("MAIN: doing some useful stuff");

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.

Here is an example using mpsc :

pub fn crossfire_mpsc() {
let rt = tokio::runtime::Runtime::new().unwrap();

let (tx, rx) = mpsc::bounded_future_both::<i32>(100);

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.

Here is an example using mpmc :

pub fn crossfire_mpmc() {
let rt = tokio::runtime::Runtime::new().unwrap();

let (tx, rx) = mpmc::bounded_future_both::<i32>(100);

rt.block_on(async move {

133
Machine Translated by Google

8 channels

let mut sender_handles = vec![];

for _ in 0..4 { let tx


= tx.clone(); let handle =
tokio::spawn(async move { for i in 0i32..10 { =
tx.send(i).await; let _
println!("sent {}", i);

});
sender_handles.push(handle);
}

let mut handles = vec![]; for i in


0..4 { let rx =
rx.clone(); let handle =
tokio::spawn(async move { loop { if let Ok(_i) =

rx.recv().await {
println!("thread {} recv {}", i, _i); } else { println!("rx
closed");
break;

});
handles.push(handle);
}

for handle in sender_handles


{ handle.await.unwrap();

} drop(tx);

for handle in handles {


handle.await.unwrap();
}

});
}

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

and lock-free one-shot channels in one package.

Here is an example using kanal :

let (tx, rx) = kanal::unbounded();

thread::spawn(move || {
(0..10).for_each(|i| {
tx.send(i).unwrap();
});

drop(tx)
});

let received: u32 = rx.sum();

println!("received sum: {}", received);

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.

Give an example of using kanal asynchronously :

let rt = tokio::runtime::Runtime::new().unwrap();

135
Machine Translated by Google

8 channels

let (tx, rx) = kanal::unbounded_async();

rt.block_on(async move {
tokio::spawn(async move {
tx.send(5).await.unwrap();
});

println!("rx: {}", rx.recv().await.unwrap());


});

Kanal provides unbounded and bounded channels, and corresponding synchronous and asynchronous channels. You can

choose according to your own needs: - bounded - unbounded - bounded_async - unbounded_async

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:

let (tx, rx) = kanal::oneshot();

thread::spawn(move ||
{ tx.send(5).unwrap();
});

println!("kanal oneshot rx: {}", rx.recv().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.

Timer is used to perform specific tasks after a specified time interval.

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:

use std::thread; use


std::time::Duration;

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 :

use tokio::time::{sleep, Duration};

async fn my_timer() { println!


(" ");

// 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.

Next we introduce the professional Timer library.

9.1.1 timer library


The timer library is a simple timer implementation in rust .

usae timer;
use chrono;
use std::sync::mpsc::channel;

let timer = timer::Timer::new(); let (tx, rx) =


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

to the main thread through the channel.

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

using these features of Rust .

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.

schedule_with_date specifies to execute the closure task at a certain time:

pub fn timer_schedule_with_date() { let timer =


timer::Timer::new(); let (tx, rx) = channel();

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 .

schedule_repeating is like a Ticker, executed regularly:

use timer;
use chrono;
use std::thread; use
std::sync::{Arc, Mutex};

let timer = timer::Timer::new(); // Number of


times the callback has been called. let count = Arc::new(Mutex::new(0));

// Start repeating. Each callback increases `count`. let guard = {

let count = count.clone();


timer.schedule_repeating(chrono::Duration::milliseconds(5), move || {

139
Machine Translated by Google

9 timers

*count.lock().unwrap() += 1; }) };

// Sleep one second. The callback should be called ~200 times.


thread::sleep(std::time::Duration::new(1, 0)); let count_result =
*count.lock().unwrap(); assert!(190 <= count_result &&
count_result <= 210, "The timer was called {} times", count_result);

// Now drop the guard. This should stop the timer. drop(guard);

thread::sleep(std::time::Duration::new(0, 100));

// Let's check that the count stops increasing. let count_start =


*count.lock().unwrap(); thread::sleep(std::time::Duration::new(1,
0)); let count_stop = *count.lock().unwrap(); assert_eq!(count_start,
count_stop);

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:

pub fn futures_timer_example() { use


futures_timer::Delay; use
std::time::Duration;

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

9.1.3 async- io Timer


async_io::Timer is a Future or Stream that generates events at a specific point in time .

A timer is a Future that outputs a single Instant when triggered .

The timer can also periodically output an Instant Stream.

use async_io::Timer; use


std::time::Duration;

Timer::after(Duration::from_secs(1)).await;

• never generates a timer that never fires:

use async_io::Timer; use


futures_lite::prelude::*; use
std::time::Duration;

async fn run_with_timeout(timeout: Option<Duration>) { let timer = timeout

.map(|timeout|
Timer::after(timeout)) .unwrap_or_else(Timer::never);

run_lengthy_operation().or(timer).await;
}

// Times out after 5 seconds.


run_with_timeout(Some(Duration::from_secs(5))).await; // Does not time
out.
run_with_timeout(None).await;

• after generates a timer that executes after a certain time:

use async_io::Timer; use


std::time::Duration;

Timer::after(Duration::from_secs(1)).await;

• at generates a timer that executes at a certain point in time:

use async_io::Timer; use


std::time::{Duration, Instant};

let now = Instant::now(); let when


= now + Duration::from_secs(1); Timer::at(when).await;

• interval generates a periodic timer:

141
Machine Translated by Google

9 timers

use async_io::Timer; use


futures_lite::StreamExt; use std::time::
{Duration, Instant};

let period = Duration::from_secs(1);


Timer::interval(period).next().await;

• interval_at generates a timer after a certain amount of time:

use async_io::Timer; use


futures_lite::StreamExt; use std::time::
{Duration, Instant};

let start = Instant::now(); let period =


Duration::from_secs(1); Timer::interval_at(start,
period).next().await;

set_xxx will reset the timer or timer.

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:

use smol::Timer; use


std::time::Duration;

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

Ticker to handle this scheduled task.

use ticker::Ticker; use


std::time::Duration;

pub fn ticker_example() { let ticker


= Ticker::new(0..10, Duration::from_secs(1)); for i in ticker { println!("{:?}", i)

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:

use ticker::Ticker; use


std::time::Duration;

pub fn ticker_example() { let ticker


= Ticker::new((0..), Duration::from_secs(1)); for i in ticker { println!("{:?}", i)

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

Area (critical sections). 6.


Unlike the standard library version, Condvar, RwLock and Once can work on Windows XP .
7. RwLock takes advantage of hardware lock elision on processors that support hardware lock elision , which can bring huge performance

improvements to many readers. this must pass

145
Machine Translated by Google

10 parking_lot concurrent library

The hardware-lock-elision feature is enabled.


8. RwLock uses a task fair locking strategy, which avoids reader and writer starvation, while the standard library

Versions are not guaranteed.

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.

11. RwLock supports atomically downgrading write locks


to read locks. 12. Allow raw unlocking of Mutex and RwLock without RAII guard objects .
13. Mutex<()> and RwLock<()> allow raw locking without RAII guard objects.
14. Mutex and RwLock support eventual fairness, they can be long-term fair without sacrificing performance.

15. The ReentrantMutex type supports recursive locking. 16. An

experimental deadlock detector effective for Mutex, RwLock and ReentrantMutex . This function defaults to

Disabled, can be enabled via the deadlock_detection feature.

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

customized as needed to balance various trade-offs.

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

mutex it just released), it may starve other threads.

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

length of the critical section .

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.

pub fn mutex_example() { const


N: usize = 10;

let data = Arc::new(Mutex::new(0)); let data2 =


&data.clone();

let (tx, rx) = channel(); for _ in


0..10 { let (data, tx) =
(Arc::clone(&data), tx.clone()); thread::spawn(move || { // let mut
data = data.lock(); // *data += 1;
if
*data == N { tx.send(()).unwrap(); MutexGuard

} //
});
}

rx.recv().unwrap();

println!("mutex_example: {}", data2.lock()); //


}

It also provides try_lock functionality:

const N: usize = 10;

let mutex = Arc::new(Mutex::new(()));

let handles: Vec<_> = (0..N) .map(|i| {

let mutex = Arc::clone(&mutex);


thread::spawn(move || {

147
Machine Translated by Google

10 parking_lot concurrent library

match mutex.try_lock()
{ Some(_guard) => println!("thread {} got the lock", i), None => println!("thread
{} did not get the lock", i),
}

})

}) .collect();

for handle in handles {


handle.join().unwrap();
}

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.

Here is an example using Mutex ’s force_unlock method:

use parking_lot::Mutex;

let mutex = Mutex::new(1);

// 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.

use parking_lot::Mutex; use


std::mem;

let mutex = Mutex::new(1);

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

thread cannot lock other threads by quickly reacquiring the lock.

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.

const N: usize = 10;

let data = Arc::new(FairMutex::new(0));

let (tx, rx) = channel(); for _ in


0..10 { let (data, tx) =
(Arc::clone(&data), tx.clone()); thread::spawn(move || { // let mut data
= data.lock();// *data += 1; if

*data == N { tx.send(()).unwrap(); MutexGuard

} //
});
}

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 parking_lot concurrent library

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

may lead to deadlock.

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

access to the data contained in the lock.

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

length of the critical section .

When unlocking a read-write lock , you can also force a fair unlock by calling RwLockReadGuard::unlock_fair or RwLockWriteGuard::unlock_fair

instead of simply discarding the guard.

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

read the shared data and print its value:

const N: usize = 10;

let lock = Arc::new(RwLock::new(5));

let handles: Vec<_> = (0..N) .map(|i| {

let lock = Arc::clone(&lock);


thread::spawn(move || { if i % 2
== 0 { let mut num
= lock.write(); *num += 1; } else { let
num =
lock.read();
println!("thread {} read {}", i,
num);
}

})
})

150
Machine Translated by Google

.collect();

for handle in handles {


handle.join().unwrap();
}

RwLock also provides try_read and try_write functions.

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.

try_write provides try_write, try_write_for, try_write_until and other 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.

mem::forget is used to discard a value without triggering its destructor.

mem::forget is usually used in the following situations:

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

scope or thread, but the original

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

situations, the life cycle of values needs to be manually

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

for excessive use in normal code.

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

10 parking_lot concurrent library

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 type ReentrantMutex<T> = ReentrantMutex<RawMutex, RawThreadId, T>;


Here is an example using ReentrantMutexGuard :

pub fn reentrantmutex_example() {
let lock = ReentrantMutex::new(());

reentrant(&lock, 10);

println!("reentrantMutex_example: done");
}

fn reentrant(lock: &ReentrantMutex<()>, i: usize) { if i == 0 { return;

let _lock = lock.lock();


reentrant(lock, i - 1);
}

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.

Different from the standard library Once :

static mut VAL: usize = 0; static


INIT: Once = Once::new(); fn get_cached_val()
-> usize { unsafe { INIT.call_once(||
{ println!
("initializing once");
thread::sleep(std::time::Duration::from_secs(1));
VAL = 100;

});
VAL
}

152
Machine Translated by Google

let handle = thread::spawn(|| {


println!("thread 1 get_cached_val: {}", get_cached_val());
});

println!("get_cached_val: {}", get_cached_val());

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

determined that the thread must block.

use std::sync::Condvar; use


std::sync::Mutex;

let pair = Arc::new((Mutex::new(false), Condvar::new())); let pair2 =


Arc::clone(&pair);

thread::spawn(move || { let (lock,


cvar) = &*pair2; let mut started =
lock.lock().unwrap(); *started = true; cvar.notify_one();

});

let (lock, cvar) = &*pair; let mut


started = lock.lock().unwrap(); while !*started { started
=
cvar.wait(started).unwrap(); // block until notified
}

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 .

This crate provides a set of tools for concurrent programming:

• Atomic

operations – AtomicCell, a thread-safe mutable memory location. (no_std) –


AtomicConsume, used to read in “consume” order from the original atomic type

Pick. (no_std) •
Data structure

– deque, used to build the task scheduler’s work-stealing deque.


– ArrayQueue, a bounded MPMC queue with a fixed-capacity buffer allocated at build time. (al-
place)

– 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.

– Parker, a thread parking primitive.


– ShardedLock, a sharded read-write lock with fast concurrent reads.
– WaitGroup, used to synchronize the start or end of a calculation. •
Utility – Backoff,

for exponential backoff in spin loops. (no_std) – CachePadded, used to


pad and align values to the cache line length. (no_std)

155
Machine Translated by Google

11 crossbeam concurrency library

– 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.

Let's introduce them one by one.

11.1 Atomic operations

AtomicCell is an atomic data structure, which ensures that read and write operations on internal data are atomic.

The main features and usage are as follows:

• 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.

The following example demonstrates the use of AtomicCell in a single-threaded situation :

pub fn atomic_cell_example() {
let a = AtomicCell::new(0i32);

a.store(1);
assert_eq!(a.load(), 1);

assert_eq!(a.compare_exchange(1, 2), Ok(1)); assert_eq!


(a.fetch_add(1), 2); assert_eq!(a.load(),
3); assert_eq!(a.swap(100), 3);
assert_eq!(a.load(), 100); assert_eq!
(a.into_inner(), 100);

let a = AtomicCell::new(100i32); let v =


a.take(); assert_eq!(v,
100); assert_eq!(a.load(),
0);
}

The following example demonstrates the use of AtomicCell in a multi-threaded environment :

use crossbeam::atomic::AtomicCell;

156
Machine Translated by Google

11.2 Data structure

let atomic_count = AtomicCell::new(0);

// 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.

11.2 Data structure

11.2.1 Bidirectional queue deque

Concurrent work stealing deque.

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.

Here is an implementation of this work-stealing strategy:

use crossbeam_deque::{Injector, Stealer, Worker}; use std::iter;

157
Machine Translated by Google

11 crossbeam concurrency library

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)

// .or_else(|| stealers.iter().map(|s| s.steal()).collect())


})

// .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 Data structure

11.2.2 ArrayQueue

ArrayQueue is a bounded multi-producer multi-consumer queue.

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

makes this queue slightly faster than SegQueue .

Here is an example using ArrayQueue :

use crossbeam::queue::ArrayQueue;

let queue = ArrayQueue::new(100);

//
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

dynamically allocate segments as elements are pushed .

The following is an example of using SegQueue to demonstrate multiple producers and multiple consumers:

use crossbeam_queue::SegQueue; use


std::thread;

159
Machine Translated by Google

11 crossbeam concurrency library

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

elements can be pushed and popped.

11.3 Memory management

There are very few usage scenarios and will be ignored for now. This module mainly provides a garbage collection capability.

11.4 Thread synchronization

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

11.4 Thread synchronization

Channel provides the capabilities of mpmc .

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.

Create a limited-capacity channel:

use crossbeam_channel::bounded;

// 5

let (s, r) = 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.

You can replace the example with unbounded channels:

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

Collection operations must occur simultaneously:

use std::thread;
use crossbeam_channel::bounded;

161
Machine Translated by Google

11 crossbeam concurrency library

//
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!").

Concurrency example, using clone to obtain cloned objects:

use std::thread; use


crossbeam_channel::bounded;

let (s1, r1) = bounded(0); let (s2, r2)


= (s1.clone(), r1.clone());

// ,

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

11.4 Thread synchronization

news. Send and receive operations on disconnected channels will not be blocked .

use crossbeam_channel::{unbounded, RecvError};

//
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;

let (s, r) = unbounded();

thread::spawn(move || {
s.send(1).unwrap();

163
Machine Translated by Google

11 crossbeam concurrency library

s.send(2).unwrap();
s.send(3).unwrap();
drop(s); //
});

`collect`
// // let v: Vec<_> = r.iter().collect();

assert_eq!(v, [1, 2, 3]);

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.

use std::thread; use


std::time::Duration; use
crossbeam_channel::{select, unbounded};

let (s1, r1) = unbounded(); let (s2, r2)


= unbounded();

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 :

use crossbeam_channel::{Receiver, RecvError, Select};

fn recv_multiple<T>(rs: &[Receiver<T>]) -> Result<T, RecvError> {


//

164
Machine Translated by Google

11.4 Thread synchronization

let mut sel = Select::new(); for r in rs


{ sel.recv(r);

//
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

use std::time::{Duration, Instant}; use


crossbeam_channel::{after, select, tick};

let start = Instant::now(); let ticker =


tick(Duration::from_millis(50)); let timeout =
after(Duration::from_secs(1));

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.

Parker in crossbeam provides a thread Parking primitive.

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

can be "locked" and "unlocked" through park and unpark .

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

11 crossbeam concurrency library

use std::time::Duration; use


crossbeam_utils::sync::Parker;

let p = Parker::new(); let u =


p.unparker().clone();

//
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.

The usage is similar to RwLock :

use crossbeam_utils::sync::ShardedLock;

let lock = ShardedLock::new(5);

//
{
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

let mut w = lock.write().unwrap(); *w += 1;


assert_eq!
(*w, 6); } //

11.4.4 WaitGroup

WaitGroup enables threads to synchronize the start or end of a calculation.

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

such as wg that provide similar functions.

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.

Here is an example using WaitGroup :

use crossbeam_utils::sync::WaitGroup; use


std::thread;

//
let wg = WaitGroup::new();

for _ in 0..4 { // let

wg = wg.clone();

thread::spawn(move || { //

//
drop(wg);
});
}

//
wg.wait();

11.5 Utilities

167
Machine Translated by Google

11 crossbeam concurrency library

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.

use crossbeam_utils::Backoff; use


std::sync::atomic::AtomicUsize; use
std::sync::atomic::Ordering::SeqCst;

fn fetch_mul(a: &AtomicHelp, b: help) -> help {


let backoff = Backoff::new(); loop { let
val =
a.load(SeqCst); if
a.compare_exchange(val, val.wrapping_mul(b), SeqCst, SeqCst).is_ok() {
return val;

} 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

original value is returned, otherwise Backoff is used for spin backoff.

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:

use crossbeam_utils::Backoff; use


std::sync::atomic::AtomicBool; use
std::sync::atomic::Ordering::SeqCst; use std::thread;

fn blocking_wait(ready: &AtomicBool) { let backoff


= Backoff::new(); while !
ready.load(SeqCst) {
if backoff.is_completed()
{ thread::park(); }
else
{ backoff.snooze();
}

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

that the cache line length is 128 bytes.

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;

let array = [CachePadded::new(1i8), CachePadded::new(2i8)]; let addr1 =


&*array[0] as *const i8 as usize; let addr2 = &*array[1] as
*const i8 as usize;

assert!(addr2 - addr1 >= 64); assert_eq!


(addr1 % 64, 0); assert_eq!(addr2
% 64, 0);
This code creates an array containing two i8 types. Each element is processed by CachePadded::new to ensure that their layout in

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

11 crossbeam concurrency library

use crossbeam_utils::CachePadded; use


std::sync::atomic::AtomicUsize;

struct Queue<T>
{ head: CachePadded<AtomicUsize>, tail:
CachePadded<AtomicUsize>, buffer: *mut
T,
}

11.5.3 Scope

Create a scope for spawning threads .

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;

let var = thing![1, 2, 3];

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.

use crossbeam_skiplist::SkipMap; use


crossbeam_utils::thread::scope;

let person_ages = SkipMap::new();

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);

assert_eq!(person_ages.get("Spike Garrett").unwrap().value(), &22);


});
s.spawn(|_|
{ person_ages.insert("Bryon Conroy", 65);
person_ages.insert("Lauren Reilly", 2);

}); }).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.

12.1 Parallel collections

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

of data , you can consider using the rayan library.

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.

Here is an example using scope :

let mut value_a = None; let mut


value_b = None; let mut value_c
= None; rayon::scope(|s|
{ s.spawn(|s1| { // `s`

ˆ
`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

value_c, and initialized to None.


2. rayon::scope(|s| { ... }); creates a Rayon parallel scope. In this scope, parallel tasks can be generated. 3. s.spawn(|s1|
{ ... }); generates a task and

passes a scope handle s1. this

174
Machine Translated by Google

12.3 Thread pool

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.

12.3 Thread pool


ThreadPool represents a user-created thread pool. Use ThreadPoolBuilder to specify the number and/or names of
threads in the thread pool . After calling ThreadPoolBuilder::build() , you can use ThreadPool::install() to explicitly
execute a function in that thread pool. In contrast, top-level Rayon functions (such as join()) will be executed
implicitly in the current thread pool.

fn fib(n: usize) -> usize { if n == 0 ||


n == 1 { return n;

} let (a, b) = rayon::join(|| fib(n - 1), || fib(n - 2)); // runs inside of `pool return a + b;

let pool = rayon::ThreadPoolBuilder::new()

.num_threads(4) .build() .unwrap();

let n = pool.install(|| fib(20));

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.

any remaining work and terminate automatically.

broadcast performs the operation op in each thread of the thread pool . Then, any attempt to use join, scope or

Parallel iterator operations will be performed in this thread pool.

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.

use std::sync::atomic::{AtomicUsize, Ordering};

fn main() {
// 5

let pool = rayon::ThreadPoolBuilder::new().num_threads(5).build().unwrap();

//
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!

("Final Result: {}", final_result);

fn expensive_operation1() -> i32 {


//
42
}

fn expensive_operation2() -> i32 {


//
58
}

Here is an example of quick sort:

let mut v = vec![5, 1, 8, 22, 0, 44]; quick_sort(&mut v);


assert_eq!(v, vec![0, 1, 5, 8,
22, 44]);

fn quick_sort<T: PartialOrd + Send>(v: &mut [T]) { if v.len() > 1 { let mid =


partition(v); let (lo, hi) =
v.split_at_mut(mid); rayon::join(||
quick_sort(lo), || quick_sort(hi));

// Partition // // // fn `<=`

" "

partition<T: PartialOrd + Send>(v: &mut [T]) -> usize {


let pivot = v.len() - 1; let mut i = 0; for
j in 0..pivot { if v[j] <=
v[pivot] { v.swap(i, j);

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.

13.1 Asynchronous runtime

Already introduced in Chapter 3.

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

let output = output.await?;

assert!(output.status.success()); assert_eq!
(output.stdout, b"hello world\n"); Ok(())

Commonly used line-by-line processing functions:

use tokio::io::{BufReader, AsyncBufReadExt}; use


tokio::process::Command;

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());

let mut child = cmd.spawn() .expect("failed


to spawn command");

let stdout = child.stdout.take() .expect("child


did not have a handle to stdout");

let mut reader = BufReader::new(stdout).lines();

//
tokio::spawn(async move { let
status = child.wait().await .expect("child
process encountered an error");

println!("child status was: {}", status);


});

while let Some(line) = reader.next_line().await? { println!("Line: {}", line);

Ok(())
}

180
Machine Translated by Google

13.2 Synchronization primitives

Use the output of one command as input to another command:

use tokio::join; use


tokio::process::Command; use
std::process::Stdio;

#[tokio::main] async
fn main() -> Result<(), Box<dyn std::error::Error>> { let mut echo =
Command::new("echo") .arg("hello

world!") .stdout(Stdio::piped()) .spawn() .expect("failed to spawn echo");

let tr_stdin: Stdio = echo


.stdout

.take() .unwrap() .try_into() .expect("failed to convert to Stdio");

let tr = Command::new("tr") .arg("a-


z") .arg("A-

Z") .stdin(tr_stdin) .stdout(Stdio::piped()) .spawn() .expect("failed to spawn tr");

let (echo_result, tr_output) = join!(echo.wait(), tr.wait_with_output());

assert!(echo_result.unwrap().success());

let tr_output = tr_output.expect("failed to await tr"); assert!(tr_output.status.success());

assert_eq!(tr_output.stdout, b"HELLO WORLD!\n");

Ok(())
}

13.2 Synchronization primitives

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,

and the lock guard is designed to be held across .await points .

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

those methods. The mini-redis example provides an example of this pattern.

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

messaging to communicate with that task.

Here is an example of a Mutex :

use tokio::sync::Mutex; use


std::sync::Arc;

#[tokio::main]
async fn main() { let
data1 = Arc::new(Mutex::new(0)); let data2 =
Arc::clone(&data1);

tokio::spawn(async move { let


mut lock = data2.lock().await; *lock += 1;

});

let mut lock = data1.lock().await; *lock += 1;

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

lock, as shown in lines 13 and 20 .

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

13.2 Synchronization primitives

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);

// many reader locks can be held at once {

let r1 = lock.read().await; let r2 =


lock.read().await; assert_eq!(*r1,
5); assert_eq!(*r2, 5);

} // read locks are dropped at this point

// only one write lock may be held, however {

let mut w = lock.write().await; *w += 1;


assert_eq!
(*w, 6); } // write lock is
dropped here

183
Machine Translated by Google

13 tokio library

13.2.3 Barrier
Barriers allow multiple tasks to synchronize the start of a computation.

use tokio::sync::Barrier; use


std::sync::Arc;

let mut handles = Vec::with_capacity(10); let barrier =


Arc::new(Barrier::new(10)); for _ in 0..10 { let c = barrier.clone(); //
The same messages will
be printed together.

// You will NOT see any interleaving.


handles.push(tokio::spawn(async move { println!("before
wait"); let wait_result = c.wait().await;
println!("after wait"); wait_result }));

// 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;

// Exactly one barrier will resolve as the "leader" assert_eq!(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

signal another task to perform an action.

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

13.2 Synchronization primitives

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.

use tokio::sync::Notify; use


std::sync::Arc;

#[tokio::main]
async fn main() { let
notify = Arc::new(Notify::new()); let notify2 =
notify.clone();

let handle = tokio::spawn(async move


{ notify2.notified().await; println!
("received notification");
});

println!("sending notification");
notify.notify_one();

// Wait for task to receive notification.


handle.await.unwrap();
}

notify_waiters notifies all waiting tasks.

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.

use tokio::sync::Notify; use


std::sync::Arc;

#[tokio::main]
async fn main() { let
notify = Arc::new(Notify::new()); let notify2 =
notify.clone();

let notified1 = notify.notified(); let notified2 =


notify.notified();

185
Machine Translated by Google

13 tokio library

let handle = tokio::spawn(async move { println!


("sending notifications"); notify2.notify_waiters();

});

notified1.await;
notified2.await; println!
("received notifications");
}

Does it feel similar to Condvar ?

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

multiple concurrent callers to access a shared resource simultaneously.

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.

use tokio::sync::{Semaphore, TryAcquireError};

#[tokio::main]
async fn main() { let
semaphore = Semaphore::new(3);

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


= semaphore.acquire_many(2).await.unwrap();

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

let permit_attempt = semaphore.try_acquire(); assert_eq!


(permit_attempt.err(), Some(TryAcquireError::NoPermits));
}

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;

async fn some_computation() -> u32 { 1 + 1

static ONCE: OnceCell<u32> = OnceCell::const_new();

#[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;

static ONCE: OnceCell<u32> = OnceCell::const_new();

async fn get_global_integer() -> &'static u32 { ONCE.get_or_init(||


async { 1 + 1

}).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

by sending messages. The advantage of this is that it avoids shared state.

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

different independent tasks can receive messages.

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

to tasks or receive the results of multiple calculations.

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;

async fn some_computation(input: u32) -> String { format!("the result of


computation {}", input)
}

#[tokio::main] async
fn main() { let (tx, mut rx)
= mpsc::channel(100);

tokio::spawn(async move { for i in 0..10


{ let res =
some_computation(i).await; tx.send(res).await.unwrap(); //
10
}

});

while let Some(res) = rx.recv().await { // println!("got = {}", res); 10 ,

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

async fn some_computation() -> String {


"represents the result of the computation".to_string()
}

#[tokio::main]
async fn main() { let
(tx, rx) = oneshot::channel();

tokio::spawn(async move { let res


= some_computation().await;
tx.send(res).unwrap(); //
});

//

//
let res = rx.await.unwrap();
}

13.3.3 broadcast (mpmc)


Broadcast channels support sending multiple values from multiple producers to multiple consumers. Every consumer will receive every

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

13.3.4 watch (spmc)


Observation channels support sending multiple values from a single producer to multiple consumers. However, only the latest value is stored in the channel. When new values

are sent, the consumer is notified, but there is no guarantee that the consumer will see all values.

An observation channel is similar to a broadcast channel with a capacity of 1 .

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

configuration change is signaled to the consumer.

use tokio::sync::watch; use


tokio::time::{self, Duration, Instant};

use std::io;

#[derive(Debug, Clone, Eq, PartialEq)] struct


Config { timeout:
Duration,
}

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());

// Spawn a task to monitor the file.


tokio::spawn(async move { loop
{ // Wait
10 seconds between checks

190
Machine Translated by Google

13.3 channels

time::sleep(Duration::from_secs(10)).await;

// Load the configuration file let new_config


= Config::load_from_file().await.unwrap();

// 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;

});

let mut handles = vec![];

// 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();

let handle = tokio::spawn(async move { // Start the


initial operation and pin the future to the stack.
// Pinning to the stack is required to resume the operation // across multiple calls
to `select!` let op = my_async_operation(); tokio::pin!
(op);

// Get the initial config value let mut conf =


rx.borrow().clone();

let mut op_start = Instant::now(); let sleep =


time::sleep_until(op_start + conf.timeout); tokio::pin!(sleep);

loop
{ tokio::select! {
_
= &mut sleep => {

191
Machine Translated by Google

13 tokio library

// The operation elapsed. Restart it


op.set(my_async_operation());

// Track the new start time op_start =


Instant::now();

// Restart the timeout


sleep.set(time::sleep_until(op_start + conf.timeout));
}
_
= rx.changed() => {
conf = rx.borrow_and_update().clone();

// 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);
}

for handle in handles.drain(..) {


handle.await.unwrap();
}

13.4 Time related

A useful tool for tracking time.

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

13.4 Time related

These types are sufficient to handle many scenarios involving time.

These types must be used within the context of Tokio 's Runtime .

13.4.1 Sleep
Wait until the duration has elapsed.

Equivalent to sleep_until(Instant::now() + duration). It is the asynchronous counterpart of std::thread::sleep .

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

with greater resolution than 1 millisecond.

To run something on a regular schedule, see interval.

The maximum duration of sleep is 68719476734 milliseconds (approximately 2.2 years). If it takes
longer, use tokio::time::delay_for.

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main()
{ sleep(Duration::from_millis(100)).await; println!("100
ms have elapsed");
}

sleep_until waits until the specified time point.

use tokio::time::{sleep_until, Instant, Duration};

#[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.

This function is equivalent to interval_at(Instant::now(), period).

use tokio::time::{self, Duration};

#[tokio::main]
async fn main() {

193
Machine Translated by Google

13 tokio library

let mut interval = time::interval(Duration::from_millis(10));

interval.tick().await; // ticks immediately interval.tick().await; //


ticks after 10ms interval.tick().await; // ticks after 10ms

// approximately 20ms have elapsed.


}

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.

use tokio::time::{interval_at, Duration, Instant};

#[tokio::main]
async fn main() { let
start = Instant::now() + Duration::from_millis(50); let mut interval =
interval_at(start, Duration::from_millis(10));

interval.tick().await; // ticks after 50ms interval.tick().await; //


ticks after 10ms interval.tick().await; // ticks after 10ms

// approximately 70ms have elapsed.


}

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

and exceed the timeout without returning an error.

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

variant, regardless of the supplied duration.

use tokio::time::timeout; use


tokio::sync::oneshot;

use std::time::Duration;

194
Machine Translated by Google

13.4 Time related

let (tx, rx) = oneshot::channel();

// 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 tokio::time::{Instant, timeout_at}; use


tokio::sync::oneshot;

use std::time::Duration;

let (tx, rx) = oneshot::channel();

// Wrap the future with a `Timeout` set to expire 10 milliseconds into the // future. if let Err(_) =

timeout_at(Instant::now() + Duration::from_millis(10), rx).await {


println!("did not receive value within 10 ms");
}

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 previous chapters, so I have listed them in a separate chapter.

14.1 Process lock

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

and delete the file when dropping .

Here is a simple example using process_lock :

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

14Other concurrency libraries

if start.elapsed() > Duration::from_millis(500) {


println!("lock timeout"); break;

} 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:

use named_lock::NamedLock; use


named_lock::Result;

fn main() -> Result<()> {


let lock = NamedLock::create("foobar")?; let _guard =
lock.lock()?;

// Do something...

Ok(())
}

14.2 oneshots

The oneshot library in Rust provides an implementation of one-shot channels.

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

Here is an example using oneshot :

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

with asynchronous programming models

catty is also a oneshot library, which is faster, simpler and lighter than futures::oneshot . one example:

pub fn catty_example() { let


(sender, mut receiver) = ::oneshot(); let sender =
thread::spawn(move || { sender.send(1).unwrap();

});

199
Machine Translated by Google

14Other concurrency libraries

let receiver = thread::spawn(move || {


let v = receiver.try_recv().unwrap(); if v.is_some()
{ println!("get value
{}", v.unwrap());
}

});
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:

pub fn hashmap_example() { let


map = Arc::new(DashMap::new());

let map1 = map.clone(); let


whandle = thread::spawn(move || { map1.insert(1,
2); map1.insert(2, 3);

});

let map2 = map.clone(); let


rhandle = thread::spawn(move || { loop { if let
Some(v)
= map2.get(&1) { println!("get value {} for
key 1", *v); break;

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:

pub fn flurry_hashmap() { let map


= flurry::HashMap::new();

assert_eq!(map.pin().insert(37, "a"), None); assert_eq!


(map.pin().is_empty(), false);
}

pub fn flurry_hashset() { // Initialize


a new hash set. let books =
flurry::HashSet::new(); let guard = books.guard();

// Add some books


books.insert("Fight Club", &guard);
books.insert("Three Men In A Raft", &guard); books.insert("The
Book of Dust", &guard); books.insert("The Dry", &guard);

// Check for a specific one. if !


books.contains(&"The Drunken Botanist", &guard) { println!("We don't
have The Drunken Botanist.");
}

// Remove a book.
books.remove(&"Three Men In A Raft", &guard);

// Iterate over everything. for book


in books.iter(&guard) { println!("{}", book);

one example:

201
Machine Translated by Google

14Other concurrency libraries

pub fn evmap_example() { let


(book_reviews_r, mut book_reviews_w) = evmap::new();

let readers: Vec<_> = (0..4) .map(|_| {

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();

// you can read through the write handle assert_eq!


(book_reviews_w.len(), 4);

// the original read handle still works too assert_eq!


(book_reviews_r.len(), 4);

// all the threads should eventually see .len() == 4 for r in


readers.into_iter() { assert!(r.join().is_ok());

202
Machine Translated by Google

14.4 Some synchronization primitives

14.4 Some synchronization primitives

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));

let lock1 = lock.clone();


smol::block_on(async { let mut
guard = lock1.lock().await; *guard += 1;

});

let lock2 = lock.clone();


smol::block_on(async { let
guard = lock2.lock().await; println!("lock2
{}", *guard);
});
}

pub fn async_lock_rwlock() { let lock =


Arc::new(RwLock::new(0));

let lock1 = lock.clone();


smol::block_on(async { let mut
guard = lock1.write().await; *guard += 1;

});

let lock2 = lock.clone();


smol::block_on(async { let
guard = lock2.read().await; println!("lock2
{}", *guard);
});
}

pub fn async_lock_barrier() { let barrier


= Arc::new(Barrier::new(5));

thread::scope(|s| {

203
Machine Translated by Google

14Other concurrency libraries

for _ in 0..5 { let


barrier = barrier.clone(); s.spawn(move ||
{ smol::block_on(async
{ println!("before wait");
barrier.wait().await; println!("after
wait");

});
});
}

});
}

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:

pub fn wg_example() { use


std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc;
use std::thread::{sleep,
spawn}; use std::time::Duration;

204
Machine Translated by Google

14.4 Some synchronization primitives

use wg::WaitGroup;

let wg = WaitGroup::new(); let ctr =


Arc::new(AtomicUsize::new(0));

for _ in 0..5 { let ctrx


= ctr.clone(); let t_wg = wg.add(1);
spawn(move || { // mock some
time consuming task
sleep(Duration::from_millis(50)); ctrx.fetch_add(1,
Ordering::Relaxed);

// mock task is finished t_wg.done();

});
}

wg.wait();
assert_eq!(ctr.load(Ordering::Relaxed), 5);
}

`awaitgroup` WaitGroup
```rust
pub fn awaitgroup_example() { use
awaitgroup::WaitGroup;

smol::block_on(async { let mut


wg = WaitGroup::new(); for _ in 0..5 { //
Create a new worker.
let worker = wg.worker();

let _ = smol::spawn(async { // Do
some work...

// This task is done all of its work. worker.done();

});
}

// Block until all other tasks have finished their work.

205
Machine Translated by Google

14Other concurrency libraries

wg.wait().await;
});
}

14.5 Event notification

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));

// Set the flag.


flag.store(true, Ordering::SeqCst);

// Notify all listeners that the flag has been set. event.notify(usize::MAX);

});

// Wait until the flag is set. loop { // Check


the flag.
if
flag.load(Ordering::SeqCst) { break;

// Start listening for events. let listener =


event.listen();

// Check the flag again after creating the listener. if


flag.load(Ordering::SeqCst) {

206
Machine Translated by Google

14.5 Event notification

break;
}

// Wait for a notification and continue the loop. listener.wait();

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();

let task = smol::spawn(async { // Blocks until


`trigger.trigger()` below listener.await;

println!("Triggered async task");


});

// 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

14Other concurrency libraries

let (tx, rx) = barrage::unbounded(); let rx2 =


rx.clone();
tx.send_async("Hello!").await.unwrap(); assert_eq!
(rx.recv_async().await, Ok("Hello!")); assert_eq!
(rx2.recv_async().await, Ok("Hello!"));
});
}

14.6 Queue

concurrent_queue is a concurrent multi-producer and multi-consumer 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());

let q1 = q.clone(); let


whandle = thread::spawn(move || {
for i in 0..10
{ q1.push(i).unwrap();
}

});

let q2 = q.clone(); let


rhandle = thread::spawn(move || loop { if let Ok(v) =
q2.pop() { println!("get value {}",
v); } else { println!("queue closed");
break;

});

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

crates. - Serde support: features = ["serde"].

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

cache based on HashMap . - TreeIndex is a read-optimized concurrent and asynchronous B+ tree

one example:

pub fn scc_hashmap() {
let hashmap: HashMap<usize, usize, RandomState> = HashMap::with_capacity(1000); assert_eq!
(hashmap.capacity(), 1024);

let ticket = hashmap.reserve(10000); assert!


(ticket.is_some()); assert_eq!
(hashmap.capacity(), 16384); for i in 0..16 { assert!
(hashmap.insert(i,
i).is_ok());

} drop(ticket);

assert_eq!(hashmap.capacity(), 1024);
}

pub fn scc_hashindex() { let


hashindex: HashIndex<u64, u32> = HashIndex::default();

assert!(!hashindex.remove(&1)); assert!
(hashindex.insert(1, 0).is_ok()); assert!
(hashindex.remove(&1));
}

pub fn scc_treeindex() { let


treeindex: TreeIndex<u64, u32> = TreeIndex::new();

assert!(treeindex.insert(1, 10).is_ok()); assert_eq!


(treeindex.insert(1, 11).err().unwrap(), (1, 11)); assert_eq!(treeindex.read(&1, |_k, v|
*v).unwrap(), 10);

209
Machine Translated by Google

14Other concurrency libraries

pub fn scc_hashset() { let


hashset: HashSet<usize, RandomState> = HashSet::with_capacity(1000); assert_eq!
(hashset.capacity(), 1024);

let ticket = hashset.reserve(10000); assert!


(ticket.is_some()); assert_eq!
(hashset.capacity(), 16384); for i in 0..16 { assert!
(hashset.insert(i).is_ok());

} drop(ticket);

assert_eq!(hashset.capacity(), 1024);
}

pub fn scc_queue() { let


queue: Queue<usize> = Queue::default();

queue.push(37);
queue.push(3);
queue.push(1);

assert_eq!(queue.pop().map(|e| **e), Some(37)); assert_eq!


(queue.pop().map(|e| **e), Some(3)); assert_eq!
(queue.pop().map(|e| **e), Some(1)); assert!
(queue.pop().is_none());
}

14.8 Signal amount

tokio::sync::Semaphore has been introduced in the tokio chapter. Here we introduce


async_weighted_semaphore. Example of async_weighted_semaphore :

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());
});
}

async_lock also provides a semaphore implementation, an example:

pub fn async_lock_semaphore() { let s =


Arc::new(async_lock::Semaphore::new(2));

let _g1 = s.try_acquire_arc().unwrap(); let g2 =


s.try_acquire_arc().unwrap();

assert!(s.try_acquire_arc().is_none()); drop(g2); assert!

(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" }));

for fut in futures.into_iter() {


assert_eq!(fut.await, "my-result"); println!("task
finished");
}

});

211
Machine Translated by Google

14Other concurrency libraries

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

task and writing is supplemented, and has consistent performance characteristics.

one example:

pub fn arc_swap_example() { let


value = ArcSwap::from(Arc::new(5)); thread::scope(|
scope| { scope.spawn(|_| { let
new_value =
Arc::new(4); value.store(new_value);

});
for _ in 0..10
{ scope.spawn(|_|
{ loop
{ let v = value.load(); println!
("value is {}", v); return;

});

} }).unwrap()

212

You might also like