7

I'm attempting to write a lock-free version of a call queue I use for message passing. This is not for anything serious, just to learn about threading.

I'm relatively sure my code is correct, except if the instructions are re-ordered or done in registers. I know I can use memory barriers to stop re-ordering, but how can I ensure values are written to memory immediately?

Public Class CallQueue
    Private first As New Node(Nothing) 'owned by consumer'
    Private last As Node = first 'owned by producers'
    Private Class Node
        Public ReadOnly action As Action
        Public [next] As Node
        Public Sub New(ByVal action As Action)
            Me.action = action
        End Sub
    End Class

    Private _running As Integer
    Private Function TryAcquireConsumer() As Boolean
        Threading.Thread.MemoryBarrier()

        'Dont bother acquiring if there are no items to consume'
        'This unsafe check is alright because enqueuers call this method, so we never end up with a non-empty idle queue'
        If first.next Is Nothing Then Return False

        Threading.Thread.MemoryBarrier()

        'Try to acquire'
        Return Threading.Interlocked.Exchange(_running, 1) = 0
    End Function
    Private Function TryReleaseConsumer() As Boolean
        Do
            Threading.Thread.MemoryBarrier()

            'Dont release while there are still things to consume'
            If first.next IsNot Nothing Then Return False

            Threading.Thread.MemoryBarrier()

            'Release'
            _running = 0

            Threading.Thread.MemoryBarrier()

            'It is possible that a new item was queued between the first.next check and releasing'
            'Therefore it is necessary to check if we can re-acquire in order to guarantee we dont leave a non-empty queue idle'
            If Not TryAcquireConsumer() Then Return True
        Loop
    End Function

    Public Sub QueueAction(ByVal action As Action)
        'Enqueue'
        'Essentially, this works because each node is returned by InterLocked.Exchange *exactly once*'
        'Each node has its .next property set exactly once, and also each node is targeted by .next exactly once, so they end up forming a valid tail'
        Dim n = New Node(action)
        Threading.Interlocked.Exchange(last, n).next = n

        'Start the consumer thread if it is not already running'
        If TryAcquireConsumer() Then
            Call New Threading.Thread(Sub() Consume()).Start()
        End If
    End Sub
    Private Sub Consume()
        'Run until queue is empty'
        Do Until TryReleaseConsumer()
            first = first.next
            Call first.action()
        Loop
    End Sub
End Class
Rasmus Faber
  • 48,631
  • 24
  • 141
  • 189
Craig Gidney
  • 17,763
  • 5
  • 68
  • 136

6 Answers6

11

There's no equivalent of C#'s volatile keyword in VB.NET. Instead what's often recommended is the use of MemoryBarrier. Helper methods could also be written:

Function VolatileRead(Of T)(ByRef Address As T) As T
    VolatileRead = Address
    Threading.Thread.MemoryBarrier()
End Function

Sub VolatileWrite(Of T)(ByRef Address As T, ByVal Value As T)
    Threading.Thread.MemoryBarrier()
    Address = Value
End Sub

Also there's a useful blog post on this subject.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 2
    Useful, but I'm still confused why the read memory barrier comes after instead of before, and vice versa for writes. – Craig Gidney May 30 '09 at 15:58
  • @Strilanc: from the document on the answer below: Every read which occurs after a volatile read in the instruction sequence occurs after the volatile read in the memory model too - they can't be reordered to before the volatile read. A volatile write goes the other way round - every write which occurs before a volatile write in the instruction sequence occurs before the volatile write in the memory model too. – EFraim Jul 27 '09 at 08:36
  • @CraigGidney: this looks like it might give you acquire/release ordering semantics, where you need to keep the load before the "critical section" and the release-store after. A sequential-consistency store needs a full memory barrier *after* the store (like `mfence` on x86, not just a compiler-reordering barrer). http://preshing.com/20120913/acquire-and-release-semantics/ – Peter Cordes Jun 20 '18 at 14:07
6

Use Thread.VolatileRead() and VolatileWrite() methods from the BCL.

http://msdn.microsoft.com/en-us/library/system.threading.thread.volatileread.aspx

Mike Chamberlain
  • 39,692
  • 27
  • 110
  • 158
4

Starting in .NET 4.5, they added two new methods to the BCL to simulate the volatile keyword: Volatile.Read and Volatile.Write. They should be totally equivalent to reading/writing a volatile field. You can clearly use them in VB.NET . They are better (where better == faster) than the Thread.VolatileRead/Thread.VolatileWrite because they use half fences instead of full fences.

xanatos
  • 109,618
  • 12
  • 197
  • 280
3

I am not an expert on this subject so hopefully someone else will correct me if I am wrong. From what I understand, the issue of memory optimizations is presently a theoretical one and not necessarily something that will occur in reality. But having said that, I think that by using the Interlocked API for your memory access (regardless of the MemoryBarrier) you would not be affected.

Unfortunately there is not an equivalent for volatile in VB.NET. It's not decorated with a normal attribute, but is rather a special compiler generated modifier. You would need to use Reflection to emit a type with this kind of field.

Here is a resource I often refer to when I have questions about threading in the .NET framework. It's very long but hopefully you will find it useful.

http://www.yoda.arachsys.com/csharp/threads/printable.shtml

Josh
  • 68,005
  • 14
  • 144
  • 156
  • Theoretical one? Like, you mean, critical sections are not absolute performance killers for 512+ CPU machines? – EFraim Jul 27 '09 at 08:22
0

The Mono.Cecil reader code makes the FieldType As RequiredModifierType with the ModifierType as System.Runtime.CompilerServices.IsVolatile.

Brian
  • 3
  • 4
-1

You can also write an attribute for "Volatile" using Thread.VolatileRead() and Thread.VolatileWrite() and make all properties/variables with that attribute like:

<Volatile()>
Protected Property SecondsRemaining as Integer

Wrote this somewhere but can't seem to find it right now...

Denis
  • 11,796
  • 16
  • 88
  • 150