Controlling Threads Using PhantomData In Rust 👻
By: Robert Barlin
Tags: Code, Concurrency, Programming, Rust

Photo by Tandem X Visuals on Unsplash

I was reading about the PhantomData type and came across the point made that it can be used as a mechanism for controlling lifetimes. From here I started to play around with it a bit and came up with something interesting.

The example itself might be a bit awkward, but bare with me :)

The Scenario 🗺️

In the example below, we have two threads, one reading (Reader) from a file every 100 milliseconds and one writing (Writer) to a file with some pauses in-between writes. The Reader and Writer are independent and have no knowledge of each other.

When the Reader is dropped we want to stop our thread associated with the Reader. This is done in the Drop implementation.

Needs some changes 🚧

There’s a problem with this code. Since the Reader is being declared in the inner block, the Reader will be dropped before it even has a chance to read any of the writes to the file.

The problem is fairly easy to fix in this example, we just remove the inner block and everything is fine. It doesn’t really fix the problem in the bigger picture though. The code base could be big and complex and an issue like this becomes non-trivial.

We want to make sure that the Reader always reads the last write at least once. In other words, we want the Reader to live as long or longer than the Writer.

Looking for a solution 🕵️‍♀️

So how can we make sure that the Writer never outlives the Reader? One solution is to make the Writer hold a reference to the Reader as a field in the Writer struct. This works, but it forces us to increase the size of the Writer. It also smudges the semantics of the Writer a bit. In our made-up scenario, it doesn’t make sense to store a reference to the Reader.

Enter PhantomData 👻!

What’s going on here? 👩‍🔬

The modified version of the code now has PhantomData in the mix. The Writer struct now contains a field with the type PhantomData, more specifically PhantomData<&’a ()>. What’s interesting here is that the important part is the lifetime parameter and not the type parameter, which is just Rust’s empty type “()”.

By declaring the PhantomData field this way, the Writer struct now declares that it can be made dependent on some external lifetime. Since it’s not related to any meaningful type you can think of this field as a mechanism to control how long the Writer should exist. It is not bound to the Reader struct in any way.

The Reward 🎖️

The second version of the example doesn’t compile and that’s cool. The compiler will complain that the Reader does not live long enough, and that’s exactly what we want. The code declares the Reader in an inner block to simulate a shorter lifetime for the Reader, compared to the declared Writer.

This means that when we try to create a new Writer in the inner block, Rust will realize that the Reader will be dropped at the end of the inner block, but not the Writer. The Writer needs to outlive the inner scope and extend its lifespan until the outer block has ended. If you remove the inner block the code will compile and run the way we intended it to do.

What’s nice about this is that, instead of spending time debugging at runtime for possible thread bugs, you can find some of the bugs during compile-time.

Endnote 🗒️

It can be a bit tricky to see all of the “magic” in the code since most of the lifetimes are being inferred by the compiler. I would encourage you to copy the code and mess around with it a bit, just to see what happens when you change things around.

/Robert