From the cited Wikipedia article (which may be wrong here, of course):
In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.
I would argue that foo
implicitly allocates memory by pushing char
s and str
s to a static String
. In the context of the overall program, the "memory which is no longer needed is not released".
Not every static
is a leak though, as the memory may be needed. Moreover, the example above leaks memory cumulatively, i.e. if foo
is used in a loop, then you might lose megabytes or gigabytes of memory (which is not immediatly visible by an explicit leak or using circular Rc
s/Arc
s). Not every use of static
will exhibit this possibly desastrous behavior as shown in my example.
What else would be a good demonstration for a leak where the memory is still reachable?
My point is: when we speak of memory leaks, we almost always think on leaks where memory becomes unreachable. But there is also the more general case of leaks by logic errors. These can make you lose megabytes or gigabytes of memory as well.
Speaking of "logic errors", this also brings me to another case where leaks can happen in Rust due to due inconsisent implementations of PartialEq
/Hash
. From the documentation of HashMap
for example:
It is a logic error for a key to be modified in such a way that the key’s hash, as determined by the Hash
trait, or its equality, as determined by the Eq
trait, changes while it is in the map. This is normally only possible through Cell
, RefCell
, global state, I/O, or unsafe code. The behavior resulting from such a logic error is not specified, but will be encapsulated to the HashMap
that observed the logic error and not result in undefined behavior. This could include panics, incorrect results, aborts, memory leaks, and non-termination.
(made the relevant part bold)
That said, I think both cases (growing a Vec
or String
or any other collection indefinitely and/or providing inconsistent Hash
implementations) is a rather rare error to make. But I wanted to list both for a more complete picture, i.e. it is possible in Rust to have memory leaks, even if you don't explicitly leak memory and also when there are no Rc
s or Arc
s involved.
Yet another example for a memory leak which isn't explicit and doesn't use Rc
/Arc
either:
async fn foo(a: i32, b: i32) -> i32 {
let (arg_tx, mut arg_rx) = tokio::sync::mpsc::unbounded_channel::<(i32, i32)>();
let (res_tx, mut res_rx) = tokio::sync::mpsc::unbounded_channel::<i32>();
let arg_tx2 = arg_tx.clone();
tokio::spawn(async move {
println!("Started task");
while !arg_tx2.is_closed() { // moving `arg_tx2` to the `Future` leaks memory!
let Some((a, b)) = arg_rx.recv().await else { break };
res_tx.send(a + b).expect("channel unexpectedly closed");
}
println!("Finished task");
});
arg_tx.send((a, b)).expect("channel unexpectedly closed");
res_rx.recv().await.expect("channel unexpectedly closed")
}
#[tokio::main]
async fn main() {
for i in 0..10 {
assert_eq!(foo(5, i).await, 5 + i);
}
}
(Playground)
Edit: Maybe this example is more than a pure memory leak, as I guess the tokio runtime will also become slower as more and more tasks are spawned, but this is still leaking memory as the spawned Future
s will never be freed.
The leak is fixed if you, for example, do a loop
:
- while !arg_tx2.is_closed() { // moving `arg_tx2` to the `Future` leaks memory!
+ loop {
(Playground)
Which is somewhat counterintuitive perhaps.
Another example for a non-explicit memory leak, which neither involves Rc
or Arc
:
fn main() {
let (query_tx, query_rx) = std::sync::mpsc::channel::<i32>();
let (result_tx, result_rx) = std::sync::mpsc::channel::<i32>();
std::thread::spawn(move ||
while let Ok(query) = query_rx.recv() {
result_tx.send(query + 5).unwrap();
result_tx.send(query + 6).unwrap();
}
);
for i in 0..10 { // imagine an infinite loop here
query_tx.send(i).unwrap();
let _result = result_rx.recv().unwrap();
let _result2 = result_rx.recv().unwrap(); // commenting out this line would result in a leak
// do something with `_result`
}
}
(Playground)
If you push two values into result_tx
but only read one value from result_rx
, and if you repeat this over a long time, you will leak memory. This is similar to my original example of growing a String
further and further, except it's an mpsc
queue which grows here.