Structure for Concurrent reads a single thread writing

Firstly, I am relatively new to julia, but have been enjoying it so far.
I am working on a parallel algorithm that requires multiple threads to read while one thread can update the vector, so my plan would be to use either a Ref or this atomic wrapper struct, but for some reason, both of these solutions end up blocking the main thread somehow which I’d think wouldn’t be possible from my MT experience in java.

In the below examples, it just simulates a bunch of reads and writes in parallel, but what is odd at least with testing here and in my actual case, when I use something like @btime or just run it multiple times with a large enough thread pool, it locks. What structures can I use to accomplish my goal while minimizing blocking?

  • Edit: I should also note that I don’t care if it ends up in the correct ordering, as long as it updates often enough and doesn’t contain any invalid data. Sounds like immutable type stuff to me.
mutable struct AtomicWrapper{T}
    @atomic x::T
end

read(avec::AtomicWrapper) = @atomic avec.x
write(avec::AtomicWrapper, val) = @atomic avec.x = val

function f2()
    a = AtomicWrapper{Vector{Int}}(Int[])
    val = [1, 2, 3]
    write(a, val)

    thread = @spawn begin 
        i = 0
        while true
            if i % 10 == 0
                write(a, [])
            else
                write(a, [read(a)..., i])
            end
            i += 1
        end
    end

    @threads for _ in 1:1000
        println(read(a))
    end

    wait(thread)
end

function f()
    a = Ref{Vector{Int}}(Int[])
    val = [1, 2, 3]
    a[] = val
    worker = @spawn begin # Writes occurring while reads occurring
        i = 0
        # Since this is the only thread writing, no concurrent writes
        while true
            if i % 10 == 0
                a[] = []
            else
                a[] = [a[]..., i]
            end
            i += 1
        end
    end
    @threads for _ in 1:100
        println(a[]) # Concurrent Reads
    end
    wait(worker)
end

for _ in 1:10
    f()
end
1 Like

This line reads weird. What do you want to do?
Why not push!(a.x, i)

In both f and f2 you spawn an infinite loop, then wait for it to complete. It will likely take a very long time.

A new vector is created and put into the Ref. A push! wouldn’t be thread safe. In the spec the assignment isn’t either, but I think it is atomic on all platforms julia runs on.

The manual disagrees that you don’t need an atomic annotation:

Any field in a struct declaration can be decorated with @atomic, and then any write must be marked with @atomic also, and must use one of the defined atomic orderings (:monotonic, :acquire, :release, :acquire_release, or :sequentially_consistent).
Multi-Threading · The Julia Language

Why is the ... collected by a []? I thought ... should only be collected by tuples, particularly in the context of function calls. He could had written a[] = [a[]; i], which looks a lot more normal.

And, this is not assignment either.

julia> a = Ref([1]);

julia> @code_lowered a[] = [2]
CodeInfo(
1 ─      Base.setproperty!(b, :x, x)
│   %2 = b
└──      return %2
)

You see the setproperty! is an in-place operation to the b, i.e. a in this example.
At least from its naming convention…

To me this sounds the use-case for a read-write lock, there is one implemented here: ConcurrentUtilities.jl/src/rwlock.jl at fde04a11e78469d1ab6b5b4f1e74faf80bb50f3d · JuliaServices/ConcurrentUtilities.jl · GitHub

2 Likes