mirror of
https://git.savannah.gnu.org/git/guile.git
synced 2025-05-28 16:00:22 +02:00
futures: Allow nested futures; put the main thread to work.
* module/ice-9/futures.scm (%futures-waiting, %within-future?, %future-prompt): New variables. (let/ec): New macro. (process-future!): Run FUTURE's thunk in a prompt; capture FUTURE's continuation when it aborts, and add it to %FUTURES-WAITING. Set %WITHIN-FUTURE? in the dynamic extent of the call FUTURE's thunk. (process-futures): Move loop body to... (process-one-future): ... here. New procedure. (notify-completion): New procedure. (touch)[work, loop]: New procedures. When %WITHIN-FUTURE? and FUTURE is started, abort; if not %WITHIN-FUTURE, call `work' while waiting. When FUTURE is queued, call `work' too. * test-suite/tests/future.test ("nested futures"): New tests.
This commit is contained in:
parent
ab975cf592
commit
3e529bf02a
2 changed files with 177 additions and 80 deletions
|
@ -20,8 +20,10 @@
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
#:use-module (srfi srfi-9)
|
#:use-module (srfi srfi-9)
|
||||||
#:use-module (srfi srfi-9 gnu)
|
#:use-module (srfi srfi-9 gnu)
|
||||||
|
#:use-module (srfi srfi-11)
|
||||||
#:use-module (ice-9 threads)
|
#:use-module (ice-9 threads)
|
||||||
#:use-module (ice-9 q)
|
#:use-module (ice-9 q)
|
||||||
|
#:use-module (ice-9 match)
|
||||||
#:export (future make-future future? touch))
|
#:export (future make-future future? touch))
|
||||||
|
|
||||||
;;; Author: Ludovic Courtès <ludo@gnu.org>
|
;;; Author: Ludovic Courtès <ludo@gnu.org>
|
||||||
|
@ -47,7 +49,7 @@
|
||||||
(define-record-type <future>
|
(define-record-type <future>
|
||||||
(%make-future thunk state mutex completion)
|
(%make-future thunk state mutex completion)
|
||||||
future?
|
future?
|
||||||
(thunk future-thunk)
|
(thunk future-thunk set-future-thunk!)
|
||||||
(state future-state set-future-state!) ; done | started | queued
|
(state future-state set-future-state!) ; done | started | queued
|
||||||
(result future-result set-future-result!)
|
(result future-result set-future-result!)
|
||||||
(mutex future-mutex)
|
(mutex future-mutex)
|
||||||
|
@ -76,10 +78,20 @@ touched."
|
||||||
;;; Future queues.
|
;;; Future queues.
|
||||||
;;;
|
;;;
|
||||||
|
|
||||||
|
;; Global queue of pending futures.
|
||||||
|
;; TODO: Use per-worker queues to reduce contention.
|
||||||
(define %futures (make-q))
|
(define %futures (make-q))
|
||||||
|
|
||||||
|
;; Lock for %FUTURES and %FUTURES-WAITING.
|
||||||
(define %futures-mutex (make-mutex))
|
(define %futures-mutex (make-mutex))
|
||||||
(define %futures-available (make-condition-variable))
|
(define %futures-available (make-condition-variable))
|
||||||
|
|
||||||
|
;; A mapping of nested futures to futures waiting for them to complete.
|
||||||
|
(define %futures-waiting '())
|
||||||
|
|
||||||
|
;; Whether currently running within a future.
|
||||||
|
(define %within-future? (make-parameter #f))
|
||||||
|
|
||||||
(define-syntax-rule (with-mutex m e0 e1 ...)
|
(define-syntax-rule (with-mutex m e0 e1 ...)
|
||||||
;; Copied from (ice-9 threads) to avoid circular dependency.
|
;; Copied from (ice-9 threads) to avoid circular dependency.
|
||||||
(let ((x m))
|
(let ((x m))
|
||||||
|
@ -88,6 +100,22 @@ touched."
|
||||||
(lambda () (begin e0 e1 ...))
|
(lambda () (begin e0 e1 ...))
|
||||||
(lambda () (unlock-mutex x)))))
|
(lambda () (unlock-mutex x)))))
|
||||||
|
|
||||||
|
(define-syntax-rule (let/ec k e e* ...) ; TODO: move to core
|
||||||
|
(let ((tag (make-prompt-tag)))
|
||||||
|
(call-with-prompt
|
||||||
|
tag
|
||||||
|
(lambda ()
|
||||||
|
(let ((k (lambda args (apply abort-to-prompt tag args))))
|
||||||
|
e e* ...))
|
||||||
|
(lambda (_ res) res))))
|
||||||
|
|
||||||
|
|
||||||
|
(define %future-prompt
|
||||||
|
;; The prompt futures abort to when they want to wait for another
|
||||||
|
;; future.
|
||||||
|
(make-prompt-tag))
|
||||||
|
|
||||||
|
|
||||||
(define (register-future! future)
|
(define (register-future! future)
|
||||||
;; Register FUTURE as being processable.
|
;; Register FUTURE as being processable.
|
||||||
(lock-mutex %futures-mutex)
|
(lock-mutex %futures-mutex)
|
||||||
|
@ -96,97 +124,146 @@ touched."
|
||||||
(unlock-mutex %futures-mutex))
|
(unlock-mutex %futures-mutex))
|
||||||
|
|
||||||
(define (process-future! future)
|
(define (process-future! future)
|
||||||
;; Process FUTURE, and update its result.
|
"Process FUTURE. When FUTURE completes, return #t and update its
|
||||||
(set-future-result! future
|
result; otherwise, when FUTURE touches a nested future that has not
|
||||||
(catch #t
|
completed yet, then suspend it and return #f. Suspending a future
|
||||||
(lambda ()
|
consists in capturing its continuation, marking it as `queued', and
|
||||||
(call-with-values (future-thunk future)
|
adding it to the waiter queue."
|
||||||
(lambda results
|
(let/ec return
|
||||||
|
(let* ((suspend
|
||||||
|
(lambda (cont future-to-wait)
|
||||||
|
;; FUTURE wishes to wait for the completion of FUTURE-TO-WAIT.
|
||||||
|
;; At this point, FUTURE is unlocked and in `started' state,
|
||||||
|
;; and FUTURE-TO-WAIT is unlocked.
|
||||||
|
(with-mutex %futures-mutex
|
||||||
|
(with-mutex (future-mutex future)
|
||||||
|
(set-future-thunk! future cont)
|
||||||
|
(set-future-state! future 'queued))
|
||||||
|
|
||||||
|
(with-mutex (future-mutex future-to-wait)
|
||||||
|
;; If FUTURE-TO-WAIT completed in the meantime, then
|
||||||
|
;; reschedule FUTURE directly; otherwise, add it to the
|
||||||
|
;; waiter queue.
|
||||||
|
(if (eq? 'done (future-state future-to-wait))
|
||||||
|
(begin
|
||||||
|
(enq! %futures future)
|
||||||
|
(signal-condition-variable %futures-available))
|
||||||
|
(set! %futures-waiting
|
||||||
|
(alist-cons future-to-wait future
|
||||||
|
%futures-waiting))))
|
||||||
|
|
||||||
|
(return #f))))
|
||||||
|
(thunk (lambda ()
|
||||||
|
(call-with-prompt %future-prompt
|
||||||
|
(lambda ()
|
||||||
|
(parameterize ((%within-future? #t))
|
||||||
|
((future-thunk future))))
|
||||||
|
suspend))))
|
||||||
|
(set-future-result! future
|
||||||
|
(catch #t
|
||||||
|
(lambda ()
|
||||||
|
(call-with-values thunk
|
||||||
|
(lambda results
|
||||||
|
(lambda ()
|
||||||
|
(apply values results)))))
|
||||||
|
(lambda args
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(apply values results)))))
|
(apply throw args)))))
|
||||||
(lambda args
|
#t)))
|
||||||
(lambda ()
|
|
||||||
(apply throw args))))))
|
(define (process-one-future)
|
||||||
|
"Attempt to pick one future from the queue and process it."
|
||||||
|
;; %FUTURES-MUTEX must be locked on entry, and is locked on exit.
|
||||||
|
(or (q-empty? %futures)
|
||||||
|
(let ((future (deq! %futures)))
|
||||||
|
(lock-mutex (future-mutex future))
|
||||||
|
(case (future-state future)
|
||||||
|
((done started)
|
||||||
|
;; Nothing to do.
|
||||||
|
(unlock-mutex (future-mutex future)))
|
||||||
|
(else
|
||||||
|
;; Do the actual work.
|
||||||
|
|
||||||
|
;; We want to release %FUTURES-MUTEX so that other workers can
|
||||||
|
;; progress. However, to avoid deadlocks, we have to unlock
|
||||||
|
;; FUTURE as well, to preserve lock ordering.
|
||||||
|
(unlock-mutex (future-mutex future))
|
||||||
|
(unlock-mutex %futures-mutex)
|
||||||
|
|
||||||
|
(lock-mutex (future-mutex future))
|
||||||
|
(if (eq? (future-state future) 'queued) ; lost the race?
|
||||||
|
(begin ; no, so let's process it
|
||||||
|
(set-future-state! future 'started)
|
||||||
|
(unlock-mutex (future-mutex future))
|
||||||
|
|
||||||
|
(let ((done? (process-future! future)))
|
||||||
|
(when done?
|
||||||
|
(with-mutex %futures-mutex
|
||||||
|
(with-mutex (future-mutex future)
|
||||||
|
(set-future-state! future 'done)
|
||||||
|
(notify-completion future))))))
|
||||||
|
(unlock-mutex (future-mutex future))) ; yes
|
||||||
|
|
||||||
|
(lock-mutex %futures-mutex))))))
|
||||||
|
|
||||||
(define (process-futures)
|
(define (process-futures)
|
||||||
;; Wait for futures to be available and process them.
|
"Continuously process futures from the queue."
|
||||||
(lock-mutex %futures-mutex)
|
(lock-mutex %futures-mutex)
|
||||||
(let loop ()
|
(let loop ()
|
||||||
(when (q-empty? %futures)
|
(when (q-empty? %futures)
|
||||||
(wait-condition-variable %futures-available
|
(wait-condition-variable %futures-available
|
||||||
%futures-mutex))
|
%futures-mutex))
|
||||||
|
|
||||||
(or (q-empty? %futures)
|
(process-one-future)
|
||||||
(let ((future (deq! %futures)))
|
|
||||||
(lock-mutex (future-mutex future))
|
|
||||||
(case (future-state future)
|
|
||||||
((done started)
|
|
||||||
;; Nothing to do.
|
|
||||||
(unlock-mutex (future-mutex future)))
|
|
||||||
(else
|
|
||||||
;; Do the actual work.
|
|
||||||
|
|
||||||
;; We want to release %FUTURES-MUTEX so that other workers can
|
|
||||||
;; progress. However, to avoid deadlocks, we have to unlock
|
|
||||||
;; FUTURE as well, to preserve lock ordering.
|
|
||||||
(unlock-mutex (future-mutex future))
|
|
||||||
(unlock-mutex %futures-mutex)
|
|
||||||
|
|
||||||
(lock-mutex (future-mutex future))
|
|
||||||
(if (eq? (future-state future) 'queued) ; lost the race?
|
|
||||||
(begin ; no, so let's process it
|
|
||||||
(set-future-state! future 'started)
|
|
||||||
(unlock-mutex (future-mutex future))
|
|
||||||
|
|
||||||
(process-future! future)
|
|
||||||
|
|
||||||
(with-mutex (future-mutex future)
|
|
||||||
(set-future-state! future 'done))
|
|
||||||
|
|
||||||
(broadcast-condition-variable (future-completion future)))
|
|
||||||
(unlock-mutex (future-mutex future))) ; yes
|
|
||||||
|
|
||||||
(lock-mutex %futures-mutex)))))
|
|
||||||
|
|
||||||
;; Look for more work.
|
|
||||||
(loop)))
|
(loop)))
|
||||||
|
|
||||||
|
(define (notify-completion future)
|
||||||
|
"Notify futures and callers waiting that FUTURE completed."
|
||||||
|
;; FUTURE and %FUTURES-MUTEX are locked.
|
||||||
|
(broadcast-condition-variable (future-completion future))
|
||||||
|
(let-values (((waiting remaining)
|
||||||
|
(partition (match-lambda ; TODO: optimize
|
||||||
|
((waitee . _)
|
||||||
|
(eq? waitee future)))
|
||||||
|
%futures-waiting)))
|
||||||
|
(set! %futures-waiting remaining)
|
||||||
|
(for-each (match-lambda
|
||||||
|
((_ . waiter)
|
||||||
|
(enq! %futures waiter)))
|
||||||
|
waiting)))
|
||||||
|
|
||||||
(define (touch future)
|
(define (touch future)
|
||||||
"Return the result of FUTURE, computing it if not already done."
|
"Return the result of FUTURE, computing it if not already done."
|
||||||
(lock-mutex (future-mutex future))
|
(define (work)
|
||||||
(case (future-state future)
|
;; Do some work while waiting for FUTURE to complete.
|
||||||
((done)
|
(lock-mutex %futures-mutex)
|
||||||
(unlock-mutex (future-mutex future)))
|
(if (q-empty? %futures)
|
||||||
((started)
|
(begin
|
||||||
;; Wait for completion.
|
(unlock-mutex %futures-mutex)
|
||||||
(wait-condition-variable (future-completion future)
|
(with-mutex (future-mutex future)
|
||||||
(future-mutex future))
|
(unless (eq? 'done (future-state future))
|
||||||
(unlock-mutex (future-mutex future)))
|
(wait-condition-variable (future-completion future)
|
||||||
((queued)
|
(future-mutex future)))))
|
||||||
(begin
|
(begin
|
||||||
;; Do the actual work. Unlock FUTURE first to preserve lock
|
(process-one-future)
|
||||||
;; ordering.
|
(unlock-mutex %futures-mutex))))
|
||||||
|
|
||||||
|
(let loop ()
|
||||||
|
(lock-mutex (future-mutex future))
|
||||||
|
(case (future-state future)
|
||||||
|
((done)
|
||||||
|
(unlock-mutex (future-mutex future)))
|
||||||
|
((started)
|
||||||
(unlock-mutex (future-mutex future))
|
(unlock-mutex (future-mutex future))
|
||||||
|
(if (%within-future?)
|
||||||
(lock-mutex %futures-mutex)
|
(abort-to-prompt %future-prompt future)
|
||||||
(q-remove! %futures future)
|
(begin
|
||||||
(unlock-mutex %futures-mutex)
|
(work)
|
||||||
|
(loop))))
|
||||||
(lock-mutex (future-mutex future))
|
(else
|
||||||
(if (eq? (future-state future) 'queued) ; lost the race?
|
(unlock-mutex (future-mutex future))
|
||||||
(begin ; no, so let's process it
|
(work)
|
||||||
(set-future-state! future 'started)
|
(loop))))
|
||||||
(unlock-mutex (future-mutex future))
|
|
||||||
|
|
||||||
(process-future! future)
|
|
||||||
|
|
||||||
(with-mutex (future-mutex future)
|
|
||||||
(set-future-state! future 'done))
|
|
||||||
|
|
||||||
(broadcast-condition-variable (future-completion future)))
|
|
||||||
(begin ; yes, so try again
|
|
||||||
(unlock-mutex (future-mutex future))
|
|
||||||
(touch future))))))
|
|
||||||
((future-result future)))
|
((future-result future)))
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,3 +311,7 @@ touched."
|
||||||
(define-syntax-rule (future body)
|
(define-syntax-rule (future body)
|
||||||
"Return a new future for BODY."
|
"Return a new future for BODY."
|
||||||
(make-future (lambda () body)))
|
(make-future (lambda () body)))
|
||||||
|
|
||||||
|
;;; Local Variables:
|
||||||
|
;;; eval: (put 'with-mutex 'scheme-indent-function 1)
|
||||||
|
;;; End:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;; Ludovic Courtès <ludo@gnu.org>
|
;;;; Ludovic Courtès <ludo@gnu.org>
|
||||||
;;;;
|
;;;;
|
||||||
;;;; Copyright (C) 2010 Free Software Foundation, Inc.
|
;;;; Copyright (C) 2010, 2012 Free Software Foundation, Inc.
|
||||||
;;;;
|
;;;;
|
||||||
;;;; This library is free software; you can redistribute it and/or
|
;;;; This library is free software; you can redistribute it and/or
|
||||||
;;;; modify it under the terms of the GNU Lesser General Public
|
;;;; modify it under the terms of the GNU Lesser General Public
|
||||||
|
@ -22,7 +22,8 @@
|
||||||
#:use-module (test-suite lib)
|
#:use-module (test-suite lib)
|
||||||
#:use-module (ice-9 futures)
|
#:use-module (ice-9 futures)
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
#:use-module (srfi srfi-26))
|
#:use-module (srfi srfi-26)
|
||||||
|
#:use-module (system base compile))
|
||||||
|
|
||||||
(define specific-exception-key (gensym))
|
(define specific-exception-key (gensym))
|
||||||
|
|
||||||
|
@ -90,3 +91,18 @@
|
||||||
(pass-if-exception "exception"
|
(pass-if-exception "exception"
|
||||||
specific-exception
|
specific-exception
|
||||||
(touch (future (throw specific-exception-key 'test "thrown!")))))
|
(touch (future (throw specific-exception-key 'test "thrown!")))))
|
||||||
|
|
||||||
|
(with-test-prefix "nested futures"
|
||||||
|
|
||||||
|
(pass-if-equal "simple" 2
|
||||||
|
(touch (future (1+ (touch (future (1+ (touch (future 0)))))))))
|
||||||
|
|
||||||
|
(pass-if-equal "loop" (map - (iota 1000))
|
||||||
|
;; Compile to avoid stack overflows.
|
||||||
|
(compile '(let loop ((list (iota 1000)))
|
||||||
|
(if (null? list)
|
||||||
|
'()
|
||||||
|
(cons (- (car list))
|
||||||
|
(touch (future (loop (cdr list)))))))
|
||||||
|
#:to 'value
|
||||||
|
#:env (current-module))))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue