-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
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:
- a call to
yieldin one of theBase.iteratemethods - with a
yieldplaced in thefinallyclause of thetake_unbufferedmethod fromjulia/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
arrThe 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
arrwhich 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
7Apparently, 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:
- Uncommenting the call to
yield()in the MWE'sBase.iterate(node::AbstractNode, state)allows for full iteration. - The following modification to
take_unbuffered, by adding a call toyield()in thefinallyclause, 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.