2022-41: Rust Drop sharing

Original link: https://xuanwo.io/reports/2022-41/

Rust uses RAII (Resource Acquisition Is Initialization) to manage resources: object initialization causes resource initialization, and object deallocation causes resource release.

Take Mutex as an example:

 {  let guard = m.lock();  // do something } // guard freed out of scope. {  // we can acquire this lock again. let guard = m.lock(); } 

When the guard leaves the current scope, rust will ensure that the guard ‘s drop is automatically called:

 #[stable(feature = "rust1" , since = "1.0.0" )] impl < T: ? Sized > Drop for MutexGuard < '_ , T > {  #[inline]  fn drop ( & mut self) {  unsafe {  self.lock.poison.done( & self.poison);  self.lock.inner.raw_unlock();  }  } } 
  • If the corresponding type has its own Drop implementation, rust will call Drop::drop()
  • Otherwise recursively perform an auto-generated drop implementation for each field

Drop’s trait is defined as follows:

 pub trait Drop {  fn drop ( & mut self); } 

It is very simple, but it is still easy to step on the pit in the actual use process. Today’s weekly report combines some actual bugs to talk about my experience of stepping on pits.

Behavior differences between _ and _var

let _ = abc; let _var = abc;

  • let _ = abc; will cause abc to be released directly after the end of this statement;
  • And let _var = abc; will create a new object, his life cycle will continue until the end of the current scope.

We can try a sample like this ( Try in playground ):

 struct Test ( & 'static str );  impl Drop for Test {  fn drop ( & mut self) {  println ! ( "Test with {} dropped" , self. 0 )  } }  fn main () {  {  println ! ( "into scope" );  let _ = Test( "_" );  println ! ( "leave scope" );  }   println ! ();   {  println ! ( "into scope" );  let _abc = Test( "_abc" );  println ! ( "leave scope" );  } } 

Its execution result is as follows:

 into scope Test with _ dropped leave scope into scope leave scope Test with _abc dropped

We can see that Test("_") is dropped at the end of the current statement, before the leave scope , and Test("_abc") is dropped after the leave scope .

In actual business logic, we often ignore this point. Take a bug recently fixed by Databend as an example: Bug: runtime spawn_batch does not release permit correctly . In order to control the concurrency of IO, Databend uses semaphore to control the parallelism of tasks. Originally expected to be released after the task is executed, but _ is used in the code, which causes the permits to be released by the task at the very beginning, which in turn causes the task’s concurrency control to not meet expectations:

 let handler = self.handle.spawn(async move { // take the ownership of the permit, (implicitly) drop it when task is done - let _ = permit; + let _pin = permit; fut.await });

How to call drop manually

For obvious reasons, Drop::drop() is not allowed to be called manually, otherwise it is very easy to have double free problems, and Rust will complain about such calls in the compiler. If you want to control the drop of a variable, you can use the std::mem::drop function, its principle is very simple: Move the variable, then return nothing.

 #[inline] #[stable(feature = "rust1" , since = "1.0.0" )] #[cfg_attr(not(test), rustc_diagnostic_item = "mem_drop" )] pub fn drop < T > (_x: T ) {} 

Essentially equivalent to:

 let x = Test {};  {  x; } 

Note, however, that calling drop is meaningless for types that implement Copy :

  • The compiler will maintain the data of the Copy type on the stack by itself, and the Drop trait cannot be implemented for the Copy type
  • Calling drop on a Copy type always copies the current variable and then releases it

References

This article is reproduced from: https://xuanwo.io/reports/2022-41/
This site is for inclusion only, and the copyright belongs to the original author.