Using a lock array to prevent multi thread corruption

I was reading through @Dan’s answer here to lock a struct using a spinlock. Lets say we are in a situation where we have a struct that in its entirety needs to be updated. Could we use something like:

struct Entry
    var::UInt32
end

function test() 
    n = 10

    lock_vector = [Base.Threads.SpinLock() for i in 1:n]
    data = [Entry(0) for i in 1:n]

    @Threads.threads for i in 1:100
        j = rand(1:n)
        openm lock(lock_vector[j])
            data[j] = Entry(Threads.threadid())
        end
    end
end

test()

It seems quite intuitive to me to have a layer locking data. Especially in cases where variables depend on each other. For example:

function test() 
    n = 10

    lock_vector = [Base.Threads.SpinLock() for i in 1:n]
    data = [Entry(1) for i in 1:n]

    @Threads.threads for i in 1:10
       j = rand(1:n)

       # We can now easily lock a range
       target_range = 1:j
       lock.(view(lock_vector, target_range))
       println(islocked.(lock_vector))
       prefix_sum = sum(e.var for e in data[target_range])
       data[j] = Entry(prefix_sum)
       unlock.(view(lock_vector, target_range))

    end

    println(data)
end

Showing the locks:

Bool[1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
Bool[1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
Bool[1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
Bool[1, 1, 1, 1, 1, 1, 1, 0, 0, 0]
Bool[1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
Bool[1, 1, 1, 1, 1, 1, 1, 0, 0, 0]
Bool[1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
Bool[1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
Bool[1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
Bool[1, 1, 1, 1, 1, 1, 1, 0, 0, 0]

I know ideally locking should be avoided, but lets say it cannot be avoided. Would this approach have “dangerous” side effects? (for example, since not the actual data is locked could the compiler rearrange the code rendering this useless)

1 Like
  1. Are you sure about spinlock?
help?> Base.Threads.SpinLock
  SpinLock()

  Create a non-reentrant, test-and-test-and-set spin lock. Recursive use will
  result in a deadlock. This kind of lock should only be used around code that
  takes little time to execute and does not block (e.g. perform I/O). In
  general, ReentrantLock should be used instead.

This is no joke. If the critical section is long then this can blow up in your face.
2. Beware of deadlock! For example the following can deadlock:

Threads.@threads for i=1:10
j = rand(1:n)
k = rand(1:n)
j == k && continue
locks = lock_vector[[j,k]]
lock.(locks)
...

In your specific example you only need to take the first lock (since every range starts at 1). You avoid deadlock by making sure that you always take the locks in the same order, from lowest to highest.

1 Like

Hey @foobar_lv2 thanks for the heads up. The spinlock performance seems fine for my usecase (will compare it to reentrant). Is there any reference for “takes little time” we talk ns I suppose?

avoid deadlock by making sure that you always take the locks in the same order

Aah yeah I always take my ranges from low to high, and then lock. (not the best example with prefix sum, suffix sum would be better :sweat_smile:)


Is there actually a way to check if a thread is waiting in Julia?

1 Like