Search code examples
c#.netmemory-management.net-coretearing

Is struct tearing an issue for Memory<T>?


First things first:

  • I know what Span<T> and Memory<T> are

  • I know why Span<T> must reside on the Stack only

  • I (conceptionally) know what struct tearing is

What remains unclear to me: Isn't struct tearing also an issue for Memory<T>? As far as I understood, basically every type bigger than WORD-size can/will be affected by that. Even further, when such a type can be used in a multithreaded reader-writer-scenario it could lead to race conditions as described in the link below.

To get to the point: Wouldn't this example also rise the issue of an potentially inconsistent Memory<T> object when used instead of Span<T>:

internal class Buffer {
    Memory<byte> _memory = new byte[1024];

    public void Resize(int newSize) {
        _memory = new byte[newSize]; // Will this update atomically?
    }

    public byte this[int index] => _memory.Span[index]; // Won't this also possibly see partial update?
}

According to the implementation of CoreFX Memory<T> also sequentially lays out a (managed object) reference, its length and an index. Where's the difference to Span<T> I'm missing, that makes Memory<T> suitable for those scenarios?


Solution

  • From reading the comments in Memory<T>, it looks like it can absolutely be torn.

    However, there seem to be two places where this actually matters: Memory<T>.Pin() and Memory<T>.Span.

    The important thing to note is that (as far as I can work out) we don't care about tearing in a way which means we still point to somewhere in the object we refer to -- although our caller might get some strange data that it wasn't expecting, that's safe in the sense that they won't get an AccessViolationException. They will just have a race condition which produces unexpected results, as a consequence of having unsynchronized threaded access to a field.


    Memory<T>.Span gets a Span<T> from the Memory<T>. It has this comment:

    If the Memory or ReadOnlyMemory instance is torn, this property getter has undefined behavior. We try to detect this condition and throw an exception, but it's possible that a torn struct might appear to us to be valid, and we'll return an undesired span. Such a span is always guaranteed at least to be in-bounds when compared with the original Memory instance, so using the span won't AV the process.

    So, we can absolutely have a torn Memory<T>, and then try to create a Span<T> from it. In this case, there's a check in the code which throws an exception if the Memory<T> has torn in such a way that it now refers to some memory outside of the object referred to by the Memory<T>.

    If it has torn in a way that it still refers to somewhere in the original object, then that's OK - our caller might not be reading the thing it was expecting to read, but at least they won't get an AccessViolationException, which is what we're trying to avoid.

    Note that Span<T> is unable to implement this same check (even if it wanted to). Memory<T> keeps references to the object, the start offset, and the length. Span<T> only keeps references to some memory address inside the object and the length.


    Memory<T>.Pin() is an unsafe method, which has this comment:

    It's possible that the below logic could result in an AV if the struct is torn. This is ok since the caller is expecting to use raw pointers, we're not required to keep this as safe as the other Span-based APIs.

    Again, we can tear in a way that we no longer refer to somewhere inside the object we refer to. However this method is unsafe, and we don't care.