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.


Send and Sync are probably the most common constraints in Rust multithreaded and asynchronous code. The origin of these two constraints was described in an earlier article on multithreading. However, when you actually write complex code, you often encounter compiler incompatibility. Here’s another example of the Send and Sync story, using a problem my colleague encountered.

The basic scenario

Send/Sync does not exist in C/C++. Data objects can be accessed from multiple threads, but only if the programmer keeps the thread safe, known as “locking”. In Rust, however, because of the design of ownership, you can’t directly split an object into two or more pieces, one for each thread. Generally, if a piece of data is only used by child threads, we will transfer the value of the data to the thread, which is the basic meaning of Send. As a result, Rust code often sees data clone() and then move to the thread:

let b = aa.clone(a); thread::spawn(move || {
    b...            
})
Copy the code

The situation is more complicated if the data needs to be shared across multiple threads. We generally do not use references to external environment variables directly in threads. The reason is simple: life cycle issues. The thread closure requires’ static ‘, which conflicts with the lifecycle of the borrowed external environment variable. The error code is as follows:

let bb = AA::new(8);
thread::spawn( || {
    let cc = &bb;  //closure may outlive the current function, but it borrows `bb`, which is owned by the current function
});
Copy the code

This problem can be solved by wrapping an Arc, which happens to be used to manage the life cycle. The improved code looks like this:

let b = Arc::new(aa);
let b1 = b.clone(a); thread::spawn(move || {
    b1...
})
Copy the code

Arc provides the ability to share immutable references, that is, data is read-only. If we need to access mutable references to multithreaded access to shared data, i.e. read and write data, then we also need to wrap Mutex

on the raw data first, similar to RefCell

, providing internal variability, so we can get &mut of internal data and modify the data. Of course, this is done through Mutex::lock().

let b = Arc::new(Mutex::new(aa));
let b1 = b.clone(a); thread::spawn(move || {
    let b = b1.lock(a); . })Copy the code

Why can’t RefCell do this directly? This is because RefCell does not support Sync and cannot load Arc. Note the Arc constraint:

unsafe impl<T: ? Sized + Sync + Send> Sendfor Arc<T> {}
Copy the code

If Arc

is Send, the condition is T: Send+Sync. RefCell does not satisfy Sync, so Arc

> does not satisfy Send and cannot be transferred to the thread. The error code is as follows:

let b = Arc::new(RefCell::new(aa));
let b1 = b.clone(a); thread::spawn(move || {
	^^^^^^^^^^^^^ `std::cell::RefCell<AA<T>>` cannot be shared between threads safely
    let x = b1.borrow_mut(a); })Copy the code

Asynchronous code: Crosses the await problem

As mentioned above, we generally transfer the value of the data to the thread, which is straightforward and easy to understand with the correct Send and Sync tags. Typical code would look like this:

fn test1<T: Send + Sync + 'static>(t: T) { let b = Arc::new(t); let bb = b.clone(); thread::spawn( move|| { let cc = &bb; }); }Copy the code

Closure: Send + Sync + ‘static ‘Arc

: Send + ‘static’ Send + Sync + ‘static.

However, there is a common case in asynchronous coroutine code where the derivation is more subtle and worth talking about. Look at the following code:

struct AA<T>(T);

impl<T> AA<T> {
    async fn run_self(self) {}
    async fn run(&self) {}
    async fn run_mut(&mut self) {}
}

fn test2<T: Send + 'static>(mut aa: AA
      
       ) { let ha = async_std::task::spawn(async move { aa.run_self().await; }); }
      Copy the code

T: Send ‘static’; The GenFuture generated by async FN requires Send + ‘static, so AA captured in GenFuture’s anonymous structure must also satisfy Send +’ static, and AA generic parameters must also satisfy Send + ‘static.

However, a similar call to the AA::run() method fails to compile and the compiler says GenFuture does not satisfy Send. The code is as follows:

fn test2<T: Send + 'static>(mut aa: AA
      
       ) { let ha = async_std::task::spawn(async move { ^^^^^^^^^^^^^^^^^^^^^^ future returned by `test2` is not `Send` aa.run().await; }); }
      Copy the code

The reason is that the AA::run() method is signed by &self, so run() is called by the immutable borrowing of AA &AA. Run () is an asynchronous method that executes await, so it requires GenFuture to generate &aa in addition to aa.

struct {
    aa: AA
    aa_ref: &AA	
}
Copy the code

As discussed earlier, the generated GenFuture needs to satisfy Send, so both AA and &AA need to satisfy Send. If &AA satisfies Send, it means that AA satisfies Sync. This is what Rust tutorials are all about:

For any type T, if ampersand is Send, T is synced

Modify the previous error code to the following form, add Sync flag, and compile.

fn test2<T: Send + Sync + 'static>(mut aa: AA
      
       ) { let ha = async_std::task::spawn(async move { aa.run().await; }); }
      Copy the code

Also, it’s worth pointing out that calling AA::run_mut(&mut self) in the code above does not require the Sync flag:

fn test2<T: Send + 'static>(mut aa: AA
      
       ) { let ha = async_std::task::spawn(async move { aa.run_mut().await; }); }
      Copy the code

This is because &mut self does not require T: Sync. See the following standard library code for defining Sync:

mod impls {
    #[stable(feature = "rust1", since = "1.0.0")] unsafe impl<T: Sync + ? Sized> Sendfor &T {}
    #[stable(feature = "rust1", since = "1.0.0")] unsafe impl<T: Send + ? Sized> Sendfor &mut T {}
}
Copy the code

T: Send requires T: Sync and mut T T: Send.

conclusion

In summary, the Send constraint is fundamentally introduced by thread::spawn() or task::spawn(), because the closure parameters of both methods must satisfy Send. In addition, using Arc

when you need to share data requires T: Send + Sync. Arc

> Arc

> Arc

> Arc

> Arc

>





Send/Sync is no different in asynchronous code than in synchronous multithreaded code. It is only because of GenFuture’s quirk that the variable across await must be T: Send. Note that the signature of the asynchronous method called through T, if &self, must satisfy T: Send + Sync.

Finally, a bit of experience: About the Send/Sync reason is not complex, more time because the code level deeper, call relationship is complex, result in a compiler error is very difficult to understand, the compiler may also give certain occasions completely wrong revision Suggestions, this time need to carefully consider, traced back, find the essence of the problem, can’t depend entirely on the compiler.


Shenzhen Netwarps Technology Co., LTD. (Netwarps), focusing on the research and development and application of Internet secure storage technology, is an advanced secure storage infrastructure provider, the main products are decentralized file system (DFS), enterprise Alliance chain platform (EAC), block chain operating system (BOS).

Wechat official account: Netwarps