Multicore-friendly struct layout vs locking

I have a heavily multithreaded C code employing a bunch of tricks for good scalability and I don't see how to do an equivalent in Rust.

I have tons of objects which in pseudo-code look like this:

struct foo {
    u64 sequence_counter;
    /* read-mostly section */
    /* end of the cacheline */
    mutex_t lock;
    /* mixed section */
};

The read-mostly section contains fields which almost never change. Modifications are only legal with the mutex held and they adjust the sequence_counter. Readers (most common usage) don't take the lock, instead they use sequence_counter to validate they got everything right. Note everything they need is placed in one cacheline and is separated from the lock. The write-free aspect of this most common usage is of paramount importance.

The mixed section requires the lock held for any access and compared to the above sees little usage.

How does one create an equivalent in Rust, preferably while retaining the safety features? In this case most notably ensuring that any stores to the read-mostly section only occur with the lock held, but not requiring it for read access.

I only found examples which wrap the target object and there is no attention paid to things like cacheline placement.

How does this work if they read the data while the writer is still writing to it but hasn't yet updated the counter?

It's also not clear from your example how the access to the read-mostrly section is synchronized. Using a plain u64 (or equivalently a uint64_t in C) would not allow concurrent modifications (it would be UB), you need at least a AtomicU64 (_Atomic uint64_t in C) with relaxed operations, and the same for the rest of the read-mostly section.


For the cache line: you can either:

  • put each section in a struct aligned to the size of a cache line (see for example crossbeam_utils::CachePadded);
  • or make your struct #[repr(C)] (to fix the fields order), again align it to the cache line size and also manually insert padding to make the two sections fall under two different cache lines.

The counter is updated before and after, it's a well known idea and I even found a crate which implements it: seqlock - Rust . However, in contrast to the crate, the actual lock in my case is elsewhere.

The most important part of the question is how to get Rust to require the lock to modify these fields.

I forgot about that. Yeah, it technically work but it depends on the read/write of the data being atomic. The seqlock crate appears to use read_volatile to fake that, which should be UB. It needs something like an atomic memcpy, but we don't have that yet.

Generally the approach is to not expose those fields, and instead have a method for reading and one for writing, optionally returning a guard if you need a mutable reference for writing. Internally this will have to be unsafe, but it can be made safe to use from the outside.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.