Sleep in async

177 views
Skip to first unread message

Sean McLaughlin

unread,
Dec 5, 2013, 8:32:34 AM12/5/13
to ocaml...@googlegroups.com
Hi,

I'm experimenting with Async, and found the following puzzle.  When I try to do a (blocking) Unix.sleep in an async function, the following program sits there and prints nothing.  When I remove the line, it works as expected.  This happens on both OSX and Linux.  What am I doing wrong here?

Thanks!

Sean

---------------

open Core.Std;;
open Async.Std;;

let doit msg =
  let i = ref 0 in
  Deferred.forever () (fun () ->
    Async.Std.printf "%s: %d\n" msg (!i);
    i := !i + 1;
    Core.Std.Unix.sleep 1;  (* Removing this line or setting it to 0 works. *)
    Deferred.unit)
;;

let run () =
  doit "foo";
  doit "bar";
;;

let () =
  run ();
  never_returns (Scheduler.go ())
;;

Malcolm Matalka

unread,
Dec 5, 2013, 8:43:54 AM12/5/13
to ocaml...@googlegroups.com

Sean McLaughlin

unread,
Dec 5, 2013, 8:52:39 AM12/5/13
to ocaml-core
Thanks.  However, I'm not trying to write a countdown timer.  I'm trying to understand why calling sleep in this setting doesn't work.


--
You received this message because you are subscribed to the Google Groups "ocaml-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ocaml-core+...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

Markus Mottl

unread,
Dec 5, 2013, 9:22:54 AM12/5/13
to ocaml...@googlegroups.com
Because you are blocking the thread that runs the async scheduler. If
the scheduler cannot run, no other async job will be executed.
--
Markus Mottl http://www.ocaml.info markus...@gmail.com

Malcolm Matalka

unread,
Dec 5, 2013, 9:27:52 AM12/5/13
to ocaml...@googlegroups.com
Ah ok.

I assume the issue is that your sleep is blocking the event loop so
things like writes cannot happen. The counter example has how to do a
sleep in it in Async.

Sean McLaughlin

unread,
Dec 5, 2013, 9:31:29 AM12/5/13
to ocaml-core
But doesn't the sleep return and unblock the scheduler after 1 second?

Sean McLaughlin

unread,
Dec 5, 2013, 9:54:01 AM12/5/13
to ocaml-core
For context, I have a long, expensive computation I'd like to run in the middle of my async program.  I wanted to break it up into 1-2 second chunks to allow the scheduler to run other things.   I predicted it would block for a second or two, run other things, schedule another chunk, etc. This doesn't work.

How should I do this?  A separate process?  It seems like I'm trying to implement preemptive scheduling in async, which seems ill-advised. Still, even if misguided, I'm surprised it doesn't work.  

Thanks!

Markus Mottl

unread,
Dec 5, 2013, 9:55:17 AM12/5/13
to ocaml...@googlegroups.com
I haven't checked the newest async implementation in detail, but I
think the problem here is that async may not be scheduling writes as
long as there are new jobs on the queue. Since you keep scheduling
jobs without any actual async sleep in between, it just never gets to
executing your writes.

Not sure it's worthwhile trying to fix this issue within async by
introducing some fairness wrt. I/O. This might degrade performance a
little, and your example is surely not a practical case.

On Thu, Dec 5, 2013 at 9:31 AM, Sean McLaughlin <sea...@gmail.com> wrote:

Markus Mottl

unread,
Dec 5, 2013, 10:00:33 AM12/5/13
to ocaml...@googlegroups.com
If you have expensive computations, you should run them in
async-maintained helper threads. Check out "In_thread.run", which
(among optional other parameters) takes a thunk and returns a deferred
that becomes available once the thunk has been evaluated.

Sean McLaughlin

unread,
Dec 5, 2013, 10:04:01 AM12/5/13
to ocaml-core
Ah, that's what I wanted.  I should have asked for this from the start (though I'm happy I learned about the IO issue).  Thank you!

David House

unread,
Dec 5, 2013, 10:05:12 AM12/5/13
to ocaml...@googlegroups.com
Right. There are "normal priority" jobs and "low priority" jobs. An
async cycle will do all the normal priority jobs, then all the low
ones. Writer's background task to really do the writes is low
priority.

I think there is a maximum number of jobs per priority level per
cycle. Possibly you just have to wait a few hundred seconds. If you
used blocking printf (i.e. Printf.printf "foo\n%!"), it may also just
work.

(I haven't thought about this carefully, so maybe I'm wrong.)

Jeremie Dimino

unread,
Dec 5, 2013, 10:14:01 AM12/5/13
to ocaml...@googlegroups.com
On Thu, Dec 5, 2013 at 10:05 AM, David House <dho...@janestreet.com> wrote:
Right. There are "normal priority" jobs and "low priority" jobs. An
async cycle will do all the normal priority jobs, then all the low
ones. Writer's background task to really do the writes is low
priority.

I think there is a maximum number of jobs per priority level per
cycle. Possibly you just have to wait a few hundred seconds. If you
used blocking printf (i.e. Printf.printf "foo\n%!"), it may also just
work.

(I haven't thought about this carefully, so maybe I'm wrong.)

You are right.  There is a limit which is 500 by default. One way to make this kind of thing work would be to add an explicit "yield" function in async, which would schedule a job for the next cycle. But as Markus said the best for running expensive computations is to use "In_thread.run".

-- 
Jeremie

Markus Mottl

unread,
Dec 5, 2013, 10:18:55 AM12/5/13
to ocaml...@googlegroups.com
I had actually tested his code with Core.Std.printf, and, as one would
expect, the program did print messages in regular intervals then.
It's clearly just a matter of Async I/O scheduling. If Sean's jobs
didn't take so long, writes would have been scheduled within
reasonable time.

If there is one important take-away lesson for async beginners, it's
that the body of async functions should always execute within a tiny
fraction of a second. If that's not possible, use In_thread.run to
execute it outside of the scheduling thread.

On Thu, Dec 5, 2013 at 10:05 AM, David House <dho...@janestreet.com> wrote:

David House

unread,
Dec 5, 2013, 10:21:56 AM12/5/13
to ocaml...@googlegroups.com
Right. This is one reason why it's usual to have your entire program
in async, or the entire program outside. Interfacing between the two
can appear to work but fail in surprising ways, and one needs to be
quite cognizant of the boundaries.

Stephen Weeks

unread,
Dec 5, 2013, 3:40:32 PM12/5/13
to ocaml...@googlegroups.com
Here's some more color on what's going on with Sean's original
program.

The program consists of a job that does [Async.printf] followed by
[Core.Std.Unix.sleep]. That job is repeatedly scheduled via
[Deferred.forever]. [Async.printf] puts bytes in a [Writer] buffer;
there is a background job that will make a write() syscall to give the
bytes to the OS. Async has two priorities of jobs: low and normal.
The user job (doing printf + sleep) runs at normal priority. The
writer background job runs at low priority. In a single async cycle,
the scheduler runs normal priority jobs and then low priority jobs,
limiting the number of each kind of job to
max_num_jobs_per_priority_per_cycle, which defaults to 500 but is
configurable via the command line.

Since the job in Sean's program takes about 1s, and the jobs are
repeatedly scheduled with no intervening OS interaction (timer or
I/O), the program will take 500s to run the 500 normal jobs, at which
point the max_num_jobs_per_priority_per_cycle limit would kick in, the
async scheduler would start running low-priority jobs, and the write()
would happen.

Well, almost. If you run Sean's program and turn the max way down,
like:

ASYNC_CONFIG='((max_num_jobs_per_priority_per_cycle 5))' z.exe

you still won't see the background writes. This happens due to
threading unfairness. There is another thread involved in making the
writes, and it isn't able to acquire the async lock because of
contention with the async scheduler. To explain this, here's what the
async scheduler's main loop does:

loop:
release async lock
check for I/O, with some timeout
acquire async lock
run normal jobs (up to max_num_jobs_per_priority_per_cycle)
run low jobs (up to max_num_jobs_per_priority_per_cycle)
goto loop

The timeout depends on when the next clock event (if any) is, and
whether there are jobs to do (which can happen if the
max_num_jobs_per_priority_per_cycle limit is hit.

In Sean's program, with max_num_jobs_per_priority_per_cycle at 5, each
scheduler cycle runs the 5 normal jobs, realizes it's hit the limit,
and then, because there are still jobs to do (there is always a job to
do, because the [printf;sleep] job is scheduled with no intervening OS
interaction), uses a timeout of zero when checking for I/O. So,
effectively, there is a very small window in which the async scheduler
releases the lock. If you watch what happens with print statements,
you'll see the scheduler release the lock, call epoll, acquire the
lock, and then see the I/O thread fail to acquire the lock. This
happens over and over.

One can fix this by replacing [Deferred.unit] with [after (sec 0.01)],
which will force some fairness and give threads other than the
scheduler a chance to run. This is a bit stronger than the [yield]
function that I think Jeremie had in mind, because if you just delay a
job to the next cycle, the scheduler will still check for I/O with
zero timeout, and thus there will still be the potential for
unfairness to the I/O threads.

Also, [after (sec 0.01)] works with the default
max_num_jobs_per_priority_per_cycle, because there is only one of the
[printf;sleep] jobs per cycle.


After writing all this, it occurs to me that to improve fairness in
async, perhaps the scheduler should be changed to use a very small
timeout rather than zero.

Stephen Weeks

unread,
Dec 6, 2013, 9:45:38 AM12/6/13
to ocaml...@googlegroups.com
> This happens due to threading unfairness. There is another thread
> involved in making the writes, and it isn't able to acquire the
> async lock because of contention with the async scheduler. To
> explain this, here's what the
...
> If you watch what happens with print statements, you'll see the
> scheduler release the lock, call epoll, acquire the lock, and then
> see the I/O thread fail to acquire the lock. This happens over and
> over.

We talked at Jane Street some more about the problem with I/O threads
being unable to acquire the async lock, and are going to change
[In_thread.run] so that if it can't acquire the async lock, it
communicates to the scheduler via a [Thread_safe_queue] what needs to
be done. That should make async fairer to I/O in fully loaded
applications.

With that change, Sean's program shows output running it like:

ASYNC_CONFIG='((max_num_jobs_per_priority_per_cycle 5))'

And if one was willing to wait, one would even see output after 500s
with the default of 500.

The change should be publicly released in 109.56.
Reply all
Reply to author
Forward
0 new messages