Why do Tasks scheduled not print in the order they are called or not update the global variables in successive prints when called?

I am confused as to why one versions of my MWE is not able to alter a global variable value within subsequent calls to a function which is scheduled as a Task but the other version is, when the original Task calls for sleep.
Here this MWE alters the value of the global variable but it does not appear in the sequence of the println statements, but only afterwards upon inspection:

julia> ind = 0
0
julia> function f2()
       global ind
       println("ind=$(ind)"); ind += 1
       end
f2 (generic function with 1 method)
julia> tmpTasks=Task.(repeat([f2],5))
5-element Array{Task,1}:
 Task (runnable) @0x00007faa95009390
 Task (runnable) @0x00007faa95009600
 Task (runnable) @0x00007faa95009870
 Task (runnable) @0x00007faa95009ae0
 Task (runnable) @0x00007faa95009d50
julia> for t in 1:length(tmpTasks)
       schedule(tmpTasks[t])
       end
ind=0

julia> 
ind=0
ind=0
ind=0

julia> ind
5

but if I include a sleep function call in the loop, the println is updated with the new ind iteration counter values:

for t in 1:length(tmpTasks)
       schedule(tmpTasks[t]); sleep(0.2)
       end
ind=5
ind=6
ind=7
ind=8
ind=9

Why is that sleep function necessary? If I include a yield() the behavior changes again:

julia> for t in 1:length(tmpTasks)
       schedule(tmpTasks[t]);
       yield()
       end
ind=10
ind=11
ind=12

julia> ind=10
julia> ind=11

is it because the sleep() function forces the scheduler to yield to the println statement in the order the sleep is called? It works with even sleep(0.0)

1 Like

From https://docs.julialang.org/en/v1/manual/control-flow/#Tasks-and-events-1:
“Most task switches occur as a result of waiting for events such as I/O requests, and are performed by a scheduler included in Julia Base.”

1 Like

In your MWE, one of the tasks reads ind, gets a zero and calls the print function with that value, then the scheduler switches to another task, which does the same, apparently through all 5 tasks. Only after all the prints have executed do the tasks get restarted and they all perform the increments. I think it is valid and correct even if it might be surprising.

I’m not up to speed yet with what is and isn’t thread-safe [or Task-safe] in Julia. Specifically I’m not sure in Julia how to know that read-modify-write statements (or the similar reassignment of a variable as happens with immutable values like ind in your example) are guaranteed to not get corrupted by one task reading, then another reading, then each writing back their adjusted values, thereby erasing one of the increments.

Looks like some of the needed info [for Threads, but you are only using Tasks] is here: https://docs.julialang.org/en/v1/base/multi-threading/#Base.Threads.Atomic.

1 Like

Here’s a version that exhibits corruption (at least for me) (and BTW, I don’t believe this is a bug, but rather a demo of how NOT to write code):

ind = 0

function f2()
       global ind
       tmp = ind
       println("ind=$(tmp)")
       ind = tmp+1
end

tmpTasks=Task.(repeat([f2],5))
for t in 1:length(tmpTasks)
       schedule(tmpTasks[t])
end
sleep(0.1)
ind

produces

ind=0
ind=0
ind=0
ind=0
ind=0

1

And here’s a version which uses locks to prevent the corruption:

ind = 0
indlock = ReentrantLock()

function f2()
       global ind
       global indlock
       lock(indlock)
       tmp = ind
       println("ind=$(tmp)")
       ind = tmp+1
       unlock(indlock)
end

tmpTasks=Task.(repeat([f2],5))
for t in 1:length(tmpTasks)
       schedule(tmpTasks[t])
end
sleep(0.1)
ind
1 Like