I'm currently trying to get per-thread-object
to work for my program, which works with a scoped function like .with(FnOnce(&T) -> R) -> R
, allowing &T
to "not escape" the thread bounds.
However, I require &T
to live for longer than that, see the following simple bit of code;
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = TupleOfBytes> + 'a> {
let guard: &'a Connection = self.engine.iter_reader();
self.iter_with_guard(guard)
}
This is part of implementing a trait, so rewriting it isn't feasable.
However, we'd still like thread-locals, to conserve resources on initiating and maintaining multiple connections;
pub struct Engine {
writer: Mutex<Connection>,
// rusqlite::Connection: Send + !Sync
read_conn_tls: ThreadLocal<Connection>,
read_iterator_conn_tls: ThreadLocal<Connection>,
...
}
std::thread_local
and thread-local
have already been considered and rejected for different reasons (std
doesn't free all objects on program exit, which is required for some durability. And thread-local
never frees TLS objects after their threads die.), so that brings me to per-thread-object
, with a following small modification;
// This borrows a TLS with a lifetime only as long as the TLS itself,
// this assures it does not outlive the TLS (or the structure its borrowed from),
//
// SAFETY: ThreadLocal assures that the value is never dropped as long as it or the thread lives.
//
// However, that means that T needs to be !Sync, there is no stable way of conveying this,
// so extra caution needs to be taken on the caller's side to signal this.
unsafe fn robbin_hood_tls<'tls, T: Send, I: FnOnce() -> T>(
tls: &'tls ThreadLocal<T>,
init: I,
) -> &'tls T {
tls.with_or(
|b| unsafe {
let borrow: &'tls T = std::mem::transmute(b);
return borrow;
},
init,
)
}
This unsafe function has a massive "gotcha"; manually checking if T: !Sync
, else the borrowed value can have a use-after-free (if it gets sent across threads, and the original thread dies).
It is only used in the following bit of code in Engine
;
fn iter_reader<'a>(&'a self) -> &'a Connection {
let init = || Self::prepare_conn(&self.path, self.cache_size_per_thread).unwrap();
// This is required to coerce a "good" lifetime for iterators,
// which need a borrow to the connection to stay alive as long as they are.
//
// Unfortunately, we're giving away ownership of those iterators to the caller,
// and so with a normal ThreadLocal we can't scope with `with()`.
//
// SAFETY: Connection is !Sync, so the borrow is !Send,
// which assures it stays in the same thread, preventing use-after-free.
unsafe { robbin_hood_tls(&self.read_iterator_conn_tls, init) }
}
...but, understandably, i'd be more happier if this didn't require so many hazmat gloves, and i could let the compiler check itself if T: !Sync
for this required "contract".
So my question is; Is there any way, existing in stable, existing in nightly, or up-and-coming, that could let me require that T: !Sync
on robbin_hood_tls
?