Threads, ASync and Tasks

Last updated on 2025-02-25 | Edit this page

Overview

Questions

  • How do we distribute work across threads?

Objectives

  • Change for-loops to run parallel.
  • Launch tasks and generate results asynchronously.

JULIA

#| file: src/GrayScott.jl
module GrayScott

using MacroTools
using OffsetArrays
using StaticArrays

abstract type BoundaryType{dim} end

struct Periodic{dim} <: BoundaryType{dim} end
struct Constant{dim, val} <: BoundaryType{dim} end
struct Reflected{dim} <: BoundaryType{dim} end

@inline get_bounded(::Type{Periodic{dim}}, arr, idx) where {dim} =
    checkbounds(Bool, arr, idx) ? arr[idx] : 
    arr[mod1.(Tuple(idx), size(arr))...]

@inline get_bounded(::Type{Constant{dim, val}}, arr, idx) where {dim, val} =
    checkbounds(Bool, arr, idx) ? arr[idx] : val

@inline get_bounded(::Type{Reflected{dim}}, arr, idx) where {dim} =
    checkbounds(Bool, arr, idx) ? arr[idx] : 
    arr[modflip.(Tuple(idx), size(arr))...]

function stencil!(f, ::Type{BT}, ::Size{sz}, out::AbstractArray, inp::AbstractArray...) where {dim, sz, BT <: BoundaryType{dim}}
    @assert all(size(a) == size(out) for a in inp)
    center = CartesianIndex((div.(sz, 2) .+ 1)...)
    for i in eachindex(IndexCartesian(), out)
        nb = (SArray{Tuple{sz...}}(
                get_bounded(BT, a, i - center + j)
                for j in CartesianIndices(sz))
              for a in inp)
        out[i] = f(nb...)
    end
    return out
end

stencil!(f, ::Type{BT}, sz) where {BT} =
    (out, inp...) -> stencil!(f, BT, sz, out, inp...)

struct Reader{T}
    func::T
end

macro reader(formals, func)
    env = (:($f = r.$f) for f in formals.args)
    if @capture(shortdef(func), name_(args__) = body_)
        return :($name($(args...)) = Reader(function (r)
            $(env...)
            $(body)
        end))
    end
    if @capture(shortdef(func), name_(args__) where {T_} = body_)
        return :($name($(args...)) where {$T} = Reader(function (r)
            $(env...)
            $(body)
        end))
    end
end

@reader [Δx] dx(u) = (u[1, 0] - u[-1, 0]) / (2Δx)
@reader [Δx] dy(u) = (u[0, 1] - u[-1, 0]) / (2Δx)
@reader [Δx] Δ(u) = (u[1, 0] + u[0, 1] + u[-1, 0] + u[0, -1] - 4u[0, 0]) / Δx^2

@inline (a::Reader{A})(r) where {A} = a.func(r)
@inline Base.map(f, a::Reader{A}, b::Reader{B}) where {A, B} = Reader(r->f(a(r), b(r)))
@inline Base.map(f, a::Reader{A}) where {A} = Reader(r->f(a))
@inline Base.:+(f, a::Reader{A}, b::Reader{B}) where {A, B} = map((+), a, b)
@inline Base.:-(f, a::Reader{A}, b::Reader{B}) where {A, B} = map((-), a, b)
@inline Base.:*(f, a::Reader{A}, b::Reader{B}) where {A, B} = map((*), a, b)
@inline Base.:*(f, a, b::Reader{B}) where {B} = map(x->a*x, b)
@inline Base.:*(f, a::Reader{A}, b) where {A} = map(x->x*b, a)

struct State
    u::Matrix{Float64}
    v::Matrix{Float64}
end

struct Delta
    du::Matrix{Float64}
    dv::Matrix{Float64}
end

abstract type AbstractInput{BT} end

function gray_scott_model(F, k, D_u, D_v)
    function (r::AbstractInput{BT}) where BT
        u_t(u, v) = D_u * r.Δ(u) - u[0, 0] * v[0, 0]^2 + F * (1 - u[0, 0])
        v_t(u, v) = D_v * r.Δ(v) + u[0, 0] * v[0, 0]^2 - (F + k) * v[0, 0]

        d = Delta(Matrix{Float64}(undef, r.size...), Matrix{Float64}(undef, r.size...))
        function (s)
            stencil!(u_t, BT, Size(3, 3), d.du, s.u, s.v)
            stencil!(v_t, BT, Size(3, 3), d.dv, s.u, s.v)
            return d
        end
    end |> Reader
end


end

I’ve tried integrating the Gray-Scott model using libraries from the SciML toolkit. However, the following code, directly modified from their documentation doesn’t give anything within reasonable time. In my opinion this is symptomatic for most of the SciML stack. It never really works and you never discover why.

JULIA

using ModelingToolkit, MethodOfLines, DomainSets, OrdinaryDiffEq

function gray_scott_model(F, k, Du, Dv, N, order=2)
    @parameters x y t
    @variables u(..) v(..)

    Dt = Differential(t)
    Dx = Differential(x)
    Dy = Differential(y)
    Dxx = Dx^2
    Dyy = Dy^2

    Δ(u) = Dxx(u) + Dyy(u)

    x_min = y_min = t_min = 0.0
    x_max = y_max = 1.0
    t_max = 10.0

    u0(x, y) = 0.01 * exp((-(x - 0.5)^2 - (y - 0.5)^2) / (2 * 0.1^2)) / (sqrt(2pi) * 0.1)
    v0(x, y) = 1.0 - u0(x, y)

    eqs = let v = v(x, y, t),
              u = u(x, y, t)
        [Dt(u) ~ -u * v^2 + F * (1 - u) + Du * Δ(u),
         Dt(v) ~ u * v^2 - (F + k) * v + Dv * Δ(v)]
    end

    domains = [x in Interval(x_min, x_max),
               y in Interval(y_min, y_max),
               t in Interval(t_min, t_max)]

    bcs = [u(x,y,0) ~ u0(x,y),
           u(0,y,t) ~ u(1,y,t),
           u(x,0,t) ~ u(x,1,t),

           v(x,y,0) ~ v0(x,y),
           v(0,y,t) ~ v(1,y,t),
           v(x,0,t) ~ v(x,1,t)]

    @named pdesys = PDESystem(eqs, bcs, domains, [x, y, t], [u(x, y, t), v(x, y, t)])
    discretization = MOLFiniteDifference([x=>N, y=>N], t, approx_order=order)
    return discretize(pdesys, discretization)
end

problem = gray_scott_model(0.055, 0.062, 0.02, 0.04, 256)
solve(problem, TRBDF2(), saveat=0.1)

Key Points

  • Basic parallel for-loops are done with the @threads macro.
  • Julia has built-in support for atomics.
  • Channels are primary means of asynchronous communication.