Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,22 @@ julia> function hello(x)
julia> block = @objcblock(hello, Cint, (Cint,))
```

This object can now be passed to Objective-C methods that take blocks as arguments. Note
that before Julia 1.9, blocks should only ever be called from Julia-managed threads, or else
your application will crash.

If you need to use blocks that may be called from unrelated threads on Julia 1.8 or earlier,
you can use the `@objasyncblock` macro instead. This variant takes an `AsyncCondition` that
will be executed on the libuv event loop after the block has been called. Note that there
may be some time between the block being called and the condition being executed, and libuv
may decide to coalesce multiple conditions into a single execution, so it is preferred to
use `@objcblock` whenever possible. It is also not possible to pass any arguments to the
condition, but you can use a closure to capture any state you need:
This object can now be passed to Objective-C methods that take blocks as arguments. The
callable runs synchronously on the thread that invokes the block. Since Julia 1.9 a foreign
thread is adopted into the runtime when it enters Julia, so the block may be invoked from any
thread; before Julia 1.9 it could only be called from Julia-managed threads, or else the
application would crash. Even with adoption, the callable still runs synchronously on the
invoking thread, so it must not task-switch (yield, wait, do I/O) when another thread may be
blocked waiting for the block to return — doing so can deadlock.

For fire-and-forget callbacks where no synchronous result is required, use the
`@objcasyncblock` macro instead. Rather than running Julia code on the invoking thread, it
signals an `AsyncCondition` on the libuv event loop and returns immediately, so the handler
runs asynchronously on a Julia-managed thread and the invoking (possibly foreign) thread is
never blocked. Note that there may be some time between the block being called and the
condition being executed, and libuv may coalesce multiple signals into a single execution.
It is also not possible to pass any arguments to the condition, but you can use a closure to
capture any state you need:

```julia-repl
julia> counter = 0
Expand Down
38 changes: 28 additions & 10 deletions src/blocks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,16 @@ a value of type `rettyp`, similar to how `@cfunction` works.

The callback may be a closure, and does not need any special syntax for that case.

!!! warn

Note that on Julia 1.8 or earlier, the block may only be called from Julia threads.
If this is a problem, you can use `@objcasyncblock` instead.
The callable runs **synchronously, on the thread that invokes the block**. Since
Julia 1.9 a foreign thread (e.g. a Grand Central Dispatch worker) is adopted into
the runtime when it enters Julia, so the block may be invoked from any thread.
Note, however, that adoption only makes it *safe* to run Julia code there; it does
not make it safe to *task-switch* (yield, wait, perform I/O, take a contended
lock) when another thread may be blocked waiting for the block to return — e.g. an
Objective-C method that does not return until its callbacks have run. In that
situation, yielding deadlocks: the callback waits for the scheduler while the
scheduler is held on the blocked thread. Use [`@objcasyncblock`](@ref) for such
fire-and-forget callbacks where no synchronous result is required.

Also see: [`@cfunction`](@ref), [`@objcasyncblock`](@ref)
"""
Expand Down Expand Up @@ -205,15 +211,27 @@ end
"""
@objcasyncblock(cond::AsyncCondition)

Returns an Objective-C block (as an `NSBlock` object) that schedules an async condition
object `cond` for execution on the libuv event loop.
Returns an Objective-C block (as an `NSBlock` object) that, when invoked, signals the
`Base.AsyncCondition` `cond` via `uv_async_send` and returns immediately, without
running any Julia code on the invoking thread.

Use this for fire-and-forget callbacks where no synchronous result is required and the
work should happen outside the caller's stack — on a Julia-managed thread driven by the
libuv event loop. Compared to [`@objcblock`](@ref), which runs the callable synchronously
on whatever thread invokes the block, this defers handling to a task waiting on `cond`.
That has two benefits:

!!! note
- the invoking thread (often an OS-owned Grand Central Dispatch worker) returns
instantly, so it is never blocked by Julia work; and
- because no Julia code runs on the foreign thread, there is no risk of deadlocking by
task-switching from a callback that another thread is synchronously waiting on (see
the note in [`@objcblock`](@ref)).

This macro is intended for use on Julia 1.8 and earlier. On Julia 1.9, you can always
use `@objcblock` instead.
The trade-off is that the block cannot return a value to its caller, and the handler runs
asynchronously rather than inline. Set up `cond` with a callback, or `wait` on it, to
react to the signal.

Also see: [`Base.AsyncCondition`](@ref)
Also see: [`@objcblock`](@ref), [`Base.AsyncCondition`](@ref)
"""
macro objcasyncblock(cond)
quote
Expand Down