%---------------------------------------------------------------------------% % vim: ft=mercury ts=4 sw=4 et %---------------------------------------------------------------------------% % Copyright (C) 2005, 2014 Mission Critical IT. % Copyright (C) 2014-2015, 2018, 2022 The Mercury team. % This file is distributed under the terms specified in COPYING.LIB. %---------------------------------------------------------------------------% % % File: thread.barrier.m % Original author: Peter Ross % Stability: low % % This module provides a barrier implementation. % % A barrier is a position in a program that any thread (of N threads) must % be suspended at until all the other threads (of N) reach the same % position. % % Barriers are represented by calls to barrier/3 (defined below). Different % code locations can belong to the same conceptual barrier using values of % type barrier. The same code location can also be used by multiple % barriers by supplying different values. % %---------------------------------------------------------------------------% %---------------------------------------------------------------------------% :- module thread.barrier. :- interface. :- import_module io. :- type barrier. % init(N, Barrier, !IO) % % Create a barrier for N threads. % :- pred init(int::in, barrier::out, io::di, io::uo) is det. % wait(Barrier, !IO) % % Indicate that the current thread has reached the barrier. Throws a % software_error/1 exception if this barrier has been used by more than % N threads. % :- pred wait(barrier::in, io::di, io::uo) is det. % release_barrier(Barrier, !IO) % % Release all the threads waiting at the barrier regardless of whether % or not N threads have arrived at the barrier. This can be called by % any thread, it does not have to be a thread that would normally call % wait/3. % :- pred release(barrier::in, io::di, io::uo) is det. %---------------------------------------------------------------------------% %---------------------------------------------------------------------------% :- implementation. % TODO: % % In some grades it may be possible to improve performance by writing % this natively rather than using mvar. % % A semaphore may be better for the "go" signal than an mvar. :- import_module int. :- import_module require. :- import_module thread.mvar. %---------------------------------------------------------------------------% :- type barrier ---> barrier( % How many threads we are still waiting on? b_waiting_for :: mvar(int), % Can we go yet? b_go :: mvar(why_can_we_go) ). % We use this type to say why execution may proceed after reaching the % barrier. If it is because the counter reached zero or because % release/3 was called. % :- type why_can_we_go ---> can_go_normal ; can_go_release_called. %---------------------------------------------------------------------------% init(N, barrier(WaitingOn, Go), !IO) :- init(WaitingOn, !IO), init(Go, !IO), put(WaitingOn, N, !IO). %---------------------------------------------------------------------------% wait(barrier(WaitingOn, Go), !IO) :- take(WaitingOn, N, !IO), StillWaitingFor = N - 1, ( if StillWaitingFor > 0 then % There are still outstanding threads. % Unlock the counter. put(WaitingOn, StillWaitingFor, !IO), % Wait on the barrier then unlock another thread. take(Go, WhyGo, !IO), put(Go, WhyGo, !IO) else if StillWaitingFor = 0 then % The last thread at the barrier, so signal that we can go. put(Go, can_go_normal, !IO), put(WaitingOn, StillWaitingFor, !IO) else put(WaitingOn, 0, !IO), % Go is always updated before WaitingOn, so if this branch is being % executed (either because release was called or because the barrier % was called excessively) then we know that this call to take will % not block, in either of those cases there will always be a value % in Go. take(Go, WhyGo, !IO), put(Go, WhyGo, !IO), ( WhyGo = can_go_normal, unexpected($file, $pred, "Too many threads called barrier/3 on this barrier.") ; WhyGo = can_go_release_called ) ). release(barrier(WaitingOn, Go), !IO) :- % Allow all the threads at the barrier to go. put(Go, can_go_release_called, !IO), % We must set WaitingOn to zero so that the StillWaitingFor = 0 branch % above is not executed more than once, if it is it will block when it % tries to write a value to Go as Go already has a value. Instead we % set it to zero, which means that StillWaitingOn will be -1, we use a % special value of can_go_release_called for Go so that this branch does % not raise an error. % % This algorithm has the nice benefit that if release/3 is not % considered an alternative to calling barrier, so that a barrier can be % canceled by a thread that would not normally call wait/3 itself. take(WaitingOn, _N, !IO), put(WaitingOn, 0, !IO). %---------------------------------------------------------------------------% :- end_module thread.barrier. %---------------------------------------------------------------------------%