Skip to content

Iteration stalling for custom Base.iterate methods using an unbuffered Channel #58959

@depial

Description

@depial

In the code below, my intention was to be able to iterate across a tree, while using a Channel to handle the tree traversal. This implementation works fine if iterated "manually" with a while loop or if print/@show is used in a for loop, but using other functions (e.g. push!) in a for loop stalls after the first item.

After digging around, I've found that this problem can be solved with:

  1. a call to yield in one of the Base.iterate methods
  2. with a yield placed in the finally clause of the take_unbuffered method from julia/base/channels.jl.

I'm unsure if the second fix would have other, undesirable, ramifications, but there seems to be a yield in the complementary put_unbuffered, whose purpose is to:

immediately give taker a chance to run, but don't block the current task

and this seems to be what is needed in take_unbuffered to make iteration work as expected.

MWE:

abstract type AbstractNode end

function traverse(node::AbstractNode, c::Channel)
    put!(c, node.data)
    foreach(branch -> traverse(branch, c), branches(node))
end

function Base.iterate(node::AbstractNode)
    state = Channel(c -> traverse(node, c))
    isready(state) ? (take!(state), state) : nothing
end
function Base.iterate(node::AbstractNode, state)
    # yield()
    isready(state) ? (take!(state), state) : nothing
end

Base.iterate(::AbstractNode, ::Nothing) = nothing
Base.IteratorSize(::AbstractNode) = Base.SizeUnknown()

struct Leaf <: AbstractNode
    data::Int
end

struct Node <: AbstractNode
    data::Int
    leftbranch::Union{Node, Leaf}
    rightbranch::Union{Node, Leaf}
end

data(node::Union{Node, Leaf}) = node.data
branches(node::Node) = (node.leftbranch, node.rightbranch)
branches(node::Leaf) = ()

The following code will return the full array (e.g. [1, 2, 3, 4, 5, 6, 7]):

tree = Node(1, Node(2, Leaf(3), Leaf(4)), Node(5, Leaf(6), Leaf(7)))

arr = []
next = iterate(tree)
while next !== nothing
    (item, state) = next
    push!(arr, item)
    next = iterate(tree, state)
end
arr

The above code is modified from that in Interfaces, where it is stated that it is a translation of the following for loop:

tree = Node(1, Node(2, Leaf(3), Leaf(4)), Node(5, Leaf(6), Leaf(7)))

arr = []
for item in itr 
    push!(arr, item) 
end
arr

which stalls, and only returns an array with the first item (e.g. [5]). This difference in behavior was the reason for opening this issue.

Some extra examples:

julia> tree = Node(1, Node(2, Leaf(3), Leaf(4)), Node(5, Leaf(6), Leaf(7)));

julia> [n for n in tree]
1-element Vector{Any}:
 1

julia> foreach(n -> println(n), tree)
 1
 2
 3
 4
 5
 6
 7

julia> [n for n in Channel(c -> traverse(tree, c))]
7-element Vector{Int64}:
 1
 2
 3
 4
 5
 6
 7

Apparently, one potential cause for this is the for loop being lazily JIT-compiled, where the calls to println will keep iteration flowing, but calls to push! do not. This idea is what led to me using yield to find a work-around.

Specifically, the fixes I've found:

  1. Uncommenting the call to yield() in the MWE's Base.iterate(node::AbstractNode, state) allows for full iteration.
  2. The following modification to take_unbuffered, by adding a call to yield() in the finally clause, also allows for full iteration:
function take_unbuffered(c::Channel{T}) where T
    lock(c)
    try
        check_channel_state(c)
        notify(c.cond_put, nothing, false, false)
        return wait(c.cond_take)::T
    finally
        unlock(c)
        yield() # immediately give getter a chance to run, but don't block the current task
    end
end

(where I've added a complementary comment to that found next to the yield() in the put_unbuffered implementation).

Again, I'm unsure if this is an optimal fix or if it could cause unwanted behavior elsewhere, but I believe the issue of the discrepancy between the above while loop and for loop examples would remain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions