Talk about send and sync | rust learning notes again

Time:2022-5-14

Send and sync are probably the most common constraints for rust multithreading and asynchronous code. The origin of these two constraints was introduced in the previous article on multithreading. However, when writing complex code, we often encounter various incompatibilities of the compiler. Here, I borrow a problem encountered by my colleague to talk about the story of send and sync.

Basic scene

The concept of send / sync does not exist in C / C + +. Data objects can be accessed in multiple threads at will, but programmers need to ensure thread safety, that is, the so-called “locking”. In rust, due to the ownership design, an object cannot be directly divided into two or more parts, one for each thread. Generally, if a piece of data is only used by a child thread, we will transfer the value of the data to the thread, which is also the basic meaning of send. Therefore, the rust code often sees the data clone () and then move to the thread:

let b = aa.clone();

thread::spawn(move || {

b…

})

If the data needs to be shared in multiple threads, the situation will be more complicated. We generally do not use external environment variable references directly in threads. The reason is very simple, life cycle problems. The closure of the thread requires’ static ‘, which will conflict with the life cycle 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

});

Wrapping an arc can solve this problem. Arc is just used to manage the life cycle. The improved code is as follows:

let b = Arc::new(aa);

let b1 = b.clone();

thread::spawn(move || {

b1…

})

Arc provides the ability to share immutable references, that is, the data is read-only. If we need to access multiple threads to access the variable references of shared data, that is, read and write data, we also need to wrap mutex on the original data, similar to refcell, to provide internal variability. Therefore, we can obtain the & mut of internal data and modify the data. Of course, this needs to be operated through mutex:: lock().

let b = Arc::new(Mutex::new(aa));

let b1 = b.clone();

thread::spawn(move || {

let b = b1.lock();

})

Why can’t refcell be used directly to complete this function? This is because refcell does not support sync and cannot load arc. Note the constraints of Arc:

unsafe impl<T: ?Sized + Sync + Send> Send for Arc {}

If arc is send, the condition is t: send + sync. Refcell does not satisfy sync, so arc < refcell < > > 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();

thread::spawn(move || {

^^^^^^^^^^^^^ std::cell::RefCell<AA<T>> cannot be shared between threads safely

let x = b1.borrow_mut();

})

Asynchronous code: crossing await issues

As mentioned above, generally, we will transfer the data value into the thread, so we only need to make the correct send and sync marks, which is intuitive and easy to understand. Typical codes are as follows:

fn test1<T: Send + Sync + ‘static>(t: T) {

let b = Arc::new(t);

let bb = b.clone();

thread::spawn( move|| {

let cc = &bb;

});

}

According to the above analysis, it is not difficult to deduce the context of the condition T: send + sync +’static: close: send +’static ⇒ arc: send +’static ⇒ T: send + sync +’static.

However, there is a common situation in asynchronous co process code, and the derivation process is hidden, which is worth saying. Check the following codes:

struct AA(T);

impl AA {

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;

});

}

In test2, the limit t: send + ‘static is reasonable. The genfuture generated by async FN requires send + ‘static, so the AA captured and placed in the genfuture anonymous structure must also meet send +’ static, and then the AA generic parameter must also meet send + ‘static.

However, when the AA:: run() method is called in a similar way, the compilation fails, and the compiler prompts genfuture that it 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;

});

}

The reason is that the signature of AA:: run() method is & self, so run() is called through the immutable borrowing of AA & AA. Run () is an asynchronous method that executes await, that is, the so-called & AA crosses await. Therefore, genfuture anonymous structure is required to generate & AA in addition to AA. The schematic code is as follows:

struct {

aa: AA

aa_ref: &AA

}

As discussed earlier, the generated genfuture needs to meet send, so both AA and & AA need to meet send. If & AA meets send, it means AA meets sync. This is the true meaning of the sentence mentioned in various rust tutorials:

For any type of T, if & T is send, t is sync

The previous error code is modified to the following form, the sync flag is added, and the compilation is passed.

fn test2<T: Send + Sync + ‘static>(mut aa: AA) {

let ha = async_std::task::spawn(async move {

aa.run().await;

});

}

In addition, it is worth noting that aa:: run is called in the above code_ Mut & mut self does not require sync Tags:

fn test2<T: Send + ‘static>(mut aa: AA) {

let ha = async_std::task::spawn(async move {

aa.run_mut().await;

});

}

This is because & mut self does not require T: sync. See the sync definition code in the following standard library:

mod impls {

[stable(feature = “rust1”, since = “1.0.0”)]

unsafe impl<T: Sync + ?Sized> Send for &T {}

[stable(feature = “rust1”, since = “1.0.0”)]

unsafe impl<T: Send + ?Sized> Send for &mut T {}

}

You can see that & T: send requires t: sync, while & mut t requires t: send.

summary

In a word, the send constraint is introduced by thread:: spawn() or task:: spawn() at its root, because the closure parameters of the two methods must satisfy send. In addition, when you need to share data, using arc requires t: send + sync. To share writable data, arc < mutex > is required. At this time, t: send is enough, and sync is no longer required.

There is no difference between send / sync in asynchronous code and synchronous multithreading code. Just because of the particularity of genfuture, the variable across await must be t: send. At this time, you need to pay attention to the signature of calling asynchronous methods through t. if it is & self, it must meet t: send + sync.

Finally, a little experience sharing: the truth about send / sync is not complicated. More often, it is because the code is deep and the calling relationship is complex, which makes the error prompt of the compiler difficult to understand. On some specific occasions, the compiler may give correction suggestions for complete errors. At this time, we need to carefully consider and trace the source to find the essence of the problem. We can’t rely entirely on the prompt of the compiler.