Author: Xie Jingwei, known as “Brother Dao”, 20 years OF IT veteran, data communication network expert, telecom network architect, currently serves as the development director of Netwarps. Brother Dao has years of practical experience in operating system, network programming, high concurrency, high throughput, high availability and other fields, and has a strong interest in new technologies in network and programming.

Modern CPUS are basically multi-core structure, in order to make full use of multi-core capabilities, multithreading is an unavoidable topic. Either synchronous or asynchronous programming, multi-threaded related problem has always been difficult and error-prone, essentially because of the complexity of the multi-threaded programs, especially the error of competitive conditions, make errors have a certain randomness, and with the scale of the application is more and more big, the difficulty to solve the problem is more and more high.

In other languages

C/C++ leaves synchronization mutexes and thread communication all up to the programmer. Key to a Shared resource, such as general need through the Mutex/Semaphone/CondVariable synchronization primitives to ensure safety. Simply put, it needs to be locked. However, how to add, where to add, how to release, is the programmer’s freedom. You can run without it, and most of the time, you won’t have a problem. When the load comes up, the program crashes unintentionally, and the painful process of finding the problem ensues.

Go provides a messaging mechanism through channels to normalize communication between coroutines, but for shared resources it is no different than C/C++. Of course, the problems are similar.

Rust is

Like Go, Rust proposes a channel mechanism for communication between threads. Due to Rust ownership, it is impossible to hold multiple mutable references at the same time, so a channel is split into RX and TX, which are less intuitive and easier to use than Go. In fact, the internal implementation of a channel is also an encapsulation of a shared resource using atomic operations and synchronization primitives. So again, the root of the problem is how Rust operates on shared resources. Rust provides a different way to solve the problem through ownership and Type system. Synchronization and mutual exclusion of shared resources are no longer an option for programmers. Synchronization and mutual exclusion related concurrent errors in Rust code are compile-time errors, forcing programmers to write correct code at the time of development. This is far better than the pressure of troubleshooting problems in a production environment. Let’s take a look at how this is done.

What exactly is Send, Sync

The Rust language layer provides Send and Sync traits via STD :: Marker. In general, the Send tag indicates that type ownership can be passed between threads, and the Sync tag indicates that a type that implements Sync can safely have references to its value across multiple threads. This paragraph is confusing, and to better understand Send and Sync, you need to take a look at how these two constraints are used. Here is an implementation of STD :: Thread ::spawn() in the standard library:

    pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
    where
        F: FnOnce() -> T,
        F: Send + 'static, T: Send + 'static,
    {
        unsafe { self.spawn_unchecked(f) }
    }
Copy the code

As you can see, to create a thread, you need to provide a closure. The constraint on the closure is Send, that is, it needs to be able to be transferred to the thread. The constraint on the closure return value T is also Send. For example, the following code does not compile.

    let a = Rc::new(100);
    let h = thread::spawn(move|| {
        let b = *a+1;

    });

    h.join(a);Copy the code

STD ::rc:: rc

cannot be sent between Threads safely. The reason is that, internally, the compiler creates an anonymous structure to store the captured variables in. The above code closure is roughly translated as:

struct {
	a: Rc::new(100),... }Copy the code

Rc

is a data type that does not support Send, so the anonymous structure, the closure, does not support Send and does not satisfy the F constraint of STD :: Thread ::spawn().

The code above compiles by using Arc

, because Arc

is a data type that supports Send. However, Arc

does not allow mutable references to be shared, and if you want to modify shared resources between multiple threads, you need to use Mutex

to wrap the data. The code will look like this:



    let mut a = Arc::new(Mutex::new(100));
    let h = thread::spawn(move|| {
        let mut shared = a.lock().unwrap(a); *shared =101;

    });
    h.join(a);Copy the code

Why can Mutex

do this? Can RefCell

do the same? The answer is no. Let’s look at the limitations of these data types:

unsafe impl<T: ? Sized + Sync + Send> SendforArc<T> {} unsafe impl<T: ? Sized + Sync + Send> SyncforArc<T> {} unsafe impl<T: ? Sized> SendforRefCell<T> where T: Send {} impl<T: ? Sized> ! SyncforRefCell<T> {} unsafe impl<T: ? Sized + Send> SendforMutex<T> {} unsafe impl<T: ? Sized + Send> Syncfor Mutex<T> {}
Copy the code

Arc

can Send if its wrapped T supports both Send and Sync. Obviously Arc

> does not satisfy this condition because RefCell

does not support Sync. Mutex

supports both Send and Sync on the premise that the T of its package supports Send. In effect, what Mutex

does is convert a common data structure that supports Send to Sync, which can then be passed into the thread via Arc

. We know that multi-threaded access to shared resources requires a lock, so Mutex::lock() is just such an operation. After locking (), we get a mutable reference to the internal data. From the above analysis, we can see that Rust solves the problem of multi-threaded resource sharing at compile time by using ownership and the Type system, which is indeed a clever design.





Asynchronous code, coroutines

The asynchronous code synchronization mutex problem is not fundamentally different from synchronous multithreaded code. The asynchronous runtime typically provides a way to create a coroutine/task similar to STD :: Thread ::spawn(). The following is the API for creating a coroutine/task async-STD:

pub fn spawn<F, T>(future: F) -> JoinHandle<T>
where
    F: Future<Output = T> + Send + 'static, T: Send + 'static,
{
    Builder::new().spawn(future).expect("cannot spawn task")}Copy the code

As you can see, much like STD :: Thread ::spawn(), the closure is replaced by the Future, which requires the Send constraint. This means that the future parameter must be able to Send. As we know, async syntax generates a state machine-driven Future through Generaror, which, like closures, captures variables and puts them into an anonymous data structure. So the variable must also be Send to satisfy the Send constraint for the Future. Attempts to transfer an Rc

into an async block are still rejected by the compiler. The following code will not compile:

    let a = Rc::new(100);
    let h = task::spawn(async move {
    	let b = a;
    });
Copy the code

In addition, in asynchronous code, you should, in principle, avoid using synchronous operations that affect the efficiency of asynchronous code. Imagine that if STD ::mutex:: Lock was called in the Future, the current thread would be suspended and Executor would no longer have the opportunity to perform other tasks. To this end, asynchronous runtimes typically provide various synchronization primitives similar to standard libraries. These synchronization primitives do not suspend threads, but rather return Poll::Pending when a resource cannot be acquired, and executors suspend the current task to perform other tasks.

Is it perfect? The deadlock problem

Rust solves the problem of multithreaded synchronization mutexes in an elegant way, but it doesn’t solve the program’s logic errors. Thus, one of the most troubling deadlocks in multithreaded programs can still be found in Rust code. So Rust is “fearless of concurrency” for a reason. At least for now, no compiler is smart enough to analyze and solve human logic errors. Of course, the job of programmer should not exist then…


Shenzhen Netwarps Technology Co., LTD. (Netwarps) focuses on the research and development and application of Internet secure storage technology, and is an advanced secure storage infrastructure provider. Its main products include decentralized file system (DFS), blockchain infrastructure platform (SNC), and blockchain operating system (BOS).

Wechat official account: Netwarps