Rust uses RAII (Resource Acquisition Is Initialization) to manage resources: object initialization results in the initialization of a resource, and object release results in the release of a resource.
Take Mutex
as an example.
When guard
leaves the current scope, rust ensures that drop
of guard
is called automatically.
- If the corresponding type has its own
Drop
implementation, rust callsDrop::drop()
. - Otherwise, the automatically generated drop implementation is executed recursively for each field.
Drop’s trait is defined as follows.
It’s very simple, but it’s still easy to encounter pitfalls in the actual use process.
Differences in behavior between _
and _var
The semantics of let _var = abc;
is very clear: a new binding is created, and its lifetime lasts until the end of the current scope.
The result of its execution is as follows.
But the semantics of let _ = abc;
is a bit more obscure: don’t bind the subsequent expression to anything. It’s just a match expression that doesn’t cause a drop per se; the reason we observe a drop is that the value it matches is itself temporary.
Many people interpret this as equivalent to drop(abc)
or abc;
is wrong, here is a counterexample.
The result of its execution is as follows.
Interpreting this as a no-op is also one-sided. We can likewise identify a counterexample.
The result of its execution is as follows.
Here are some more interesting examples.
The result of its execution is as follows.
The lifecycle of Test("b")
continues until the end of this match statement.
In summary, let _ = abc;
is a continuation of the match pattern, and its actual behavior is governed by the specific expression. We should not rely on let _ = abc;
to implement any drop logic, and its only reasonable use is to mark variables as no longer in use, to dispense with the #[must_use]
warning.
In the actual business logic, we often overlook this, taking a bug that Databend recently fixed: Bug: runtime spawn_batch does not release permit correctly. Databend uses a semaphore to control the parallelism of tasks in order to control the number of concurrent IOs. It is expected to release the semaphore after the task is executed, but the code uses let _ = permit
, which causes the permit to be released at a time that is not expected, and thus the concurrency of the task is not controlled as expected.
How to call drop manually
For obvious reasons, Drop::drop()
is not allowed to be called manually, otherwise it is very prone to double free problems, and Rust’s compiler will report an error for such a call. If you want to control the drop of a variable, you can use the std::mem::drop function, which is very simple: Move the variable and return nothing.
Essentially equivalent to the following.
Note, however, that for types that implement Copy
, it makes no sense to call drop
.
- The compiler maintains the Copy type’s data on the stack itself, and cannot implement the
Drop
trait for the Copy type. - Calling drop on a Copy type always copies the current variable and releases