Pin + Arc + Mutex

Arcs are pointers to some heap allocated ArcInners that contains the strong and weak counters, and the actual data, while Mutexs contain their data directly, but prevent moving their sys::Mutex using a box, so Arc<Mutex<T>> requires two allocations. We could combine them into some faster ArcMutex<T> type with only one allocation, but..

An ArcMutex<T> would work because Arc pins its allocation, so could we instead create a MustPinMutex<T> type that works only when pinned? ArcMutex<T> then becomes Arc<Pin<MustPinMutex<T>>>.

struct MustPinMutex<T: ?Sized> {
    inner: sys::Mutex,
    // I'd hope `sys::Mutex: !Unpin` but just in case: 
    notpin: std::marker::PhantomPinned,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

We'd have methods on Pin<MustPinMutex<T>> resembling Mutex::*, but almost nothing on MustPinMutex<T> itself.

Note that Pin goes on the pointer, not the data. So it would be Pin<Arc<Mutex>>

2 Likes

Which implies that the thread title should be edited to read "Pin + Arc + Mutex".

We could provide a type Mutex2 (bikeshed name)

struct Mutex2<T: ?Sized> {
    notpin: std::marker::PhantomPinned,
    inner: sys::Mutex,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

impl<T> Mutex2<T> {
    pub fn new(value: T) -> Self { ... }
}

impl<T: ?Sized> Mutex2<T> {
    pub fn lock(self: Pin<&Self>) -> Result<Mutex2Guard<'_, T>, PoisonError<T>> { ... }
    pub fn try_lock(self: Pin<&Self>) -> Result<Mutex2Guard<'_, T>, TryLockError<T>> { ... }
}

Then you can seemless use Pin<Arc<Mutex2<T>>> as a single allocation, or even with stack pinning at zero-allocations!

2 Likes

We cannot currently obtain Pin<&Mutex2<T>> from the Pin<Arc<Mutex2<T>> though. We do obtain &Mutex2<T> via impl<P: Deref> Deref for Pin<P>, but that seems useless. I'd think Arc could provide pin projection however:

impl<T: ?Sized> Arc<T> {
    fn pinned(self: Pin<&Self>) -> Pin<&T> { .. }
}

If Arc handles its pin projection then we could cover the case Pin<Arc<Foo>> where

pub struct Foo {
    x: Mutex2<X>,
    y: Mutex2<Y>,
}

with pin projection methods like

impl Foo {
    fn x(self: Pin<&Foo>) -> Pin<Mutex2<&X>> { .. }
    fn y(self: Pin<&Foo>) -> Pin<Mutex2<&Y>> { .. }
}

although they'd require unsafe code perhaps.

I'd suppose PinnedMutex and PinnedMutexGuard work well for the names.

You can use Pin::as_ref to get a Pin<&Mutex2<T>> from &Pin<Arc<Mutex2<T>>>

You can't project through a mutex like that. I think you meant,

impl Foo {
    fn x(self: Pin<&Foo>) -> Pin<&Mutex2<X>> { .. }
    fn y(self: Pin<&Foo>) -> Pin<&Mutex2<Y>> { .. }
}

Yes, this will require some unsafe code.

Yeah, that or ImmovableMutex* (Im for short, but that may be confused for immutable)

1 Like

Afaik std::sync::Mutex requiring an extra layer of boxing is an implementation detail. You can avoid double boxing by using another Mutex type. As far as I understand parking_lot::Mutex does not require another layer of boxing.

It thereby does what you want to achieve without adding anything new.

3 Likes

This would allow accessing the system mutexes without unnecessary (double) indirection. parking_lot uses it's own custom implementation.

pin-project should handle the unsafe code for those x and y methods.

I suppose Mutex2 must be implemented in std since it requires access to sys::Mutex, no? Arc<..Mutex<T>..> happens lots so this should provide significant savings. As an example, futures-timer contains two Mutex inside and Arc.

I suppose Mutex2 could be implemented on nightly outside std using #![feature(arbitrary_self_types)] for lock and try_lock on Pin<&Mutex2<T>>, and #![feature(libstd_sys_internals)] for access to sys::Mutex, yes?

This isn't needed for Pin<&_>

Yes, we need access to sys

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.