|
10 | 10 | module Experimental
|
11 | 11 |
|
12 | 12 | using Base: Threads, sync_varname, is_function_def, @propagate_inbounds
|
| 13 | +using Base: GenericCondition |
13 | 14 | using Base.Meta
|
14 | 15 |
|
15 | 16 | """
|
@@ -577,4 +578,112 @@ function task_wall_time_ns(t::Task=current_task())
|
577 | 578 | return end_at - start_at
|
578 | 579 | end
|
579 | 580 |
|
| 581 | +# wait_with_timeout |
| 582 | +# |
| 583 | +# A version of `wait(c::Condition)` that additionally allows the |
| 584 | +# specification of a timeout. This is experimental as it will likely |
| 585 | +# be dropped when a cancellation framework is added. |
| 586 | +# |
| 587 | +# The parallel behavior of wait_with_timeout is specified here. There |
| 588 | +# are three concurrent entities that can interact: |
| 589 | +# 1. Task W: the task that calls wait_with_timeout. |
| 590 | +# 2. Task T: the task created to handle a timeout. |
| 591 | +# 3. Task N: the task that notifies the Condition being waited on. |
| 592 | +# |
| 593 | +# Typical flow: |
| 594 | +# - W enters the Condition's wait queue. |
| 595 | +# - W creates T and stops running (calls wait()). |
| 596 | +# - T, when scheduled, waits on a Timer. |
| 597 | +# - Two common outcomes: |
| 598 | +# - N notifies the Condition. |
| 599 | +# - W starts running, closes the Timer, sets waiter_left and returns |
| 600 | +# the notify'ed value. |
| 601 | +# - The closed Timer throws an EOFError to T which simply ends. |
| 602 | +# - The Timer expires. |
| 603 | +# - T starts running and locks the Condition. |
| 604 | +# - T confirms that waiter_left is unset and that W is still in the |
| 605 | +# Condition's wait queue; it then removes W from the wait queue, |
| 606 | +# sets dosched to true and unlocks the Condition. |
| 607 | +# - If dosched is true, T schedules W with the special :timed_out |
| 608 | +# value. |
| 609 | +# - T ends. |
| 610 | +# - W runs and returns :timed_out. |
| 611 | +# |
| 612 | +# Some possible interleavings: |
| 613 | +# - N notifies the Condition but the Timer expires and T starts running |
| 614 | +# before W: |
| 615 | +# - W closing the expired Timer is benign. |
| 616 | +# - T will find that W is no longer in the Condition's wait queue |
| 617 | +# (which is protected by a lock) and will not schedule W. |
| 618 | +# - N notifies the Condition; W runs and calls wait on the Condition |
| 619 | +# again before the Timer expires: |
| 620 | +# - W sets waiter_left before leaving. When T runs, it will find that |
| 621 | +# waiter_left is set and will not schedule W. |
| 622 | +# |
| 623 | +# The lock on the Condition's wait queue and waiter_left together |
| 624 | +# ensure proper synchronization and behavior of the tasks involved. |
| 625 | + |
| 626 | +""" |
| 627 | + wait_with_timeout(c::GenericCondition; first::Bool=false, timeout::Real=0.0) |
| 628 | +
|
| 629 | +Wait for [`notify`](@ref) on `c` and return the `val` parameter passed to `notify`. |
| 630 | +
|
| 631 | +If the keyword `first` is set to `true`, the waiter will be put _first_ |
| 632 | +in line to wake up on `notify`. Otherwise, `wait` has first-in-first-out (FIFO) behavior. |
| 633 | +
|
| 634 | +If `timeout` is specified, cancel the `wait` when it expires and return |
| 635 | +`:timed_out`. The minimum value for `timeout` is 0.001 seconds, i.e. 1 |
| 636 | +millisecond. |
| 637 | +""" |
| 638 | +function wait_with_timeout(c::GenericCondition; first::Bool=false, timeout::Real=0.0) |
| 639 | + ct = current_task() |
| 640 | + Base._wait2(c, ct, first) |
| 641 | + token = Base.unlockall(c.lock) |
| 642 | + |
| 643 | + timer::Union{Timer, Nothing} = nothing |
| 644 | + waiter_left::Union{Threads.Atomic{Bool}, Nothing} = nothing |
| 645 | + if timeout > 0.0 |
| 646 | + timer = Timer(timeout) |
| 647 | + waiter_left = Threads.Atomic{Bool}(false) |
| 648 | + # start a task to wait on the timer |
| 649 | + t = Task() do |
| 650 | + try |
| 651 | + wait(timer) |
| 652 | + catch e |
| 653 | + # if the timer was closed, the waiting task has been scheduled; do nothing |
| 654 | + e isa EOFError && return |
| 655 | + end |
| 656 | + dosched = false |
| 657 | + lock(c.lock) |
| 658 | + # Confirm that the waiting task is still in the wait queue and remove it. If |
| 659 | + # the task is not in the wait queue, it must have been notified already so we |
| 660 | + # don't do anything here. |
| 661 | + if !waiter_left[] && ct.queue == c.waitq |
| 662 | + dosched = true |
| 663 | + Base.list_deletefirst!(c.waitq, ct) |
| 664 | + end |
| 665 | + unlock(c.lock) |
| 666 | + # send the waiting task a timeout |
| 667 | + dosched && schedule(ct, :timed_out) |
| 668 | + end |
| 669 | + t.sticky = false |
| 670 | + Threads._spawn_set_thrpool(t, :interactive) |
| 671 | + schedule(t) |
| 672 | + end |
| 673 | + |
| 674 | + try |
| 675 | + res = wait() |
| 676 | + if timer !== nothing |
| 677 | + close(timer) |
| 678 | + waiter_left[] = true |
| 679 | + end |
| 680 | + return res |
| 681 | + catch |
| 682 | + q = ct.queue; q === nothing || Base.list_deletefirst!(q::IntrusiveLinkedList{Task}, ct) |
| 683 | + rethrow() |
| 684 | + finally |
| 685 | + Base.relockall(c.lock, token) |
| 686 | + end |
| 687 | +end |
| 688 | + |
580 | 689 | end # module
|
0 commit comments