[m-users.] An example for thread communication
Volker Wysk <[email protected]> Mon, 06 Nov 2023 18:22:02 +0100
| Newsgroups | gmane.comp.lang.mercury.general |
|---|---|
| Message-ID | <[email protected]> |
Hi! For learning thread programming, I've written an example program. This is finished, and I think it would be a good addition for the samples/concurrency folder in the Mercury sources. I have a number of worker threads, which are feeded work over a channel. The workers send the results back via another channel. A feeder thread sends all the work and a collector thread (the main program) collects the results. I'm using a semaphore for synchronisation, in order to avoid to send all the work items to the work channel immediately, at the beginning. I would polish it some more, and add a description, if it is accepted. I've attached the current version. Cheers, Volker _______________________________________________ users mailing list [email protected] https://lists.mercurylang.org/listinfo/users
thr3.m
(text/x-matlab, 8.2 KB)
% An inter-thread communication and synchronisation example.
%
% ...
:- module thr3.
:- interface.
:- import_module io.
:- pred main(io::di, io::uo) is cc_multi.
:- implementation.
:- import_module string, list, int, uint64, maybe.
:- import_module thread, thread.channel, thread.semaphore.
:- import_module random, random.sfc64, random.system_rng, require.
:- pragma require_feature_set([concurrency]).
% A message on the worker channel.
:- type workermsg
---> work(uint64)
; end.
% A message on the results channel.
:- type mastermsg
---> result(uint64)
; end.
% Number of worker threads to spawn.
:- func workercount = int.
workercount = 3.
% Number of work items to send.
:- func workcount = int.
workcount = 10.
main(!IO) :-
% Set up the two channels and the semaphore.
thread.channel.init(WorkChannel, !IO),
thread.channel.init(ResChannel, !IO),
thread.semaphore.init(0, Sem, !IO),
% Spawn the worker threads and the feeder thread.
spawn_workers(0, workercount, WorkChannel, ResChannel, Sem, !IO),
thread.spawn(feeder(workcount, WorkChannel, Sem), !IO),
% Collect the work results from the workers. Exit when all worker threads have terminated themselves.
collector(workercount, 0, ResChannel, !IO).
% Master: Collect the results from the results channel.
:- pred collector(
int::in, % Number of still active workers
int::in, % Number of work results received.
thread.channel.channel(mastermsg)::in, % Result channel
io::di, io::uo
) is cc_multi.
collector(Count, ResNum, ResChannel, !IO) :-
thread.channel.take(ResChannel, Msg, !IO),
pflush("Collector: received " ++ string(Msg) ++ "\n", !IO),
(
Msg = result(_),
collector(Count, ResNum + 1, ResChannel, !IO)
;
Msg = end,
(
if Count - 1 = 0
then pflush("Collector: All workers finished. Finishing too.\n" ++
"Collector: Received " ++ string(ResNum) ++ " work results.\n",
!IO)
else collector(Count - 1, ResNum, ResChannel, !IO)
)
).
% Feeder: The feeder thread. Send the work to the feeders. Then send "end" messages to the workers, so they
% terminate themselves.
:- pred feeder(
int::in, % How many work items to send.
thread.channel.channel(workermsg)::in, % Work channel
thread.semaphore.semaphore::in, % Semaphore
io::di, io::uo
) is cc_multi.
feeder(Num, WorkChannel, Sem, !IO) :-
(
if Num = 0
then
pflush("Feeder: done.\n", !IO),
term(0, workercount, WorkChannel, !IO)
else
pflush("Feeder: Waiting for semaphore.\n", !IO),
thread.semaphore.wait(Sem, !IO),
rand64(Rand, !IO),
Wait = Rand `uint64.mod` 1000000000u64,
Msg = work(Wait),
pflush("Feeder: sending " ++ string(Msg) ++ "\n", !IO),
thread.channel.put(WorkChannel, work(Wait), !IO),
feeder(Num - 1, WorkChannel, Sem, !IO)
).
% Feeder: Send an end message for each worker to the work channel. This is called after all work messages have been
% sent.
:- pred term(int::in, int::in,
thread.channel.channel(workermsg)::in, % Arbeitskanal
io::di, io::uo
) is cc_multi.
term(Num, Count, WorkChannel, !IO) :-
(
if
Num = Count
then
true
else
pflush("Feeder: sending end.\n", !IO),
thread.channel.put(WorkChannel, end, !IO),
term(Num + 1, Count, WorkChannel, !IO)
).
% The worker thread.
:- pred worker(
int::in,
thread.channel.channel(workermsg)::in,
thread.channel.channel(mastermsg)::in,
thread.semaphore.semaphore::in,
io::di, io::uo
) is cc_multi.
:- pragma no_determinism_warning(pred(worker/6)).
worker(Num, WorkChannel, ResChannel, Sem, !IO) :-
pflush(string(Num) ++ " Worker-beginning. Incrementing the semaphore.\n", !IO),
thread.semaphore.signal(Sem, !IO),
handle_work(Num, WorkChannel, ResChannel, Sem, !IO),
pflush("Worker " ++ string(Num) ++ ": sending end\n", !IO),
thread.channel.put(ResChannel, end, !IO),
pflush(string(Num) ++ " Worker-end. Decrementing the semaphore.\n", !IO),
thread.semaphore.wait(Sem, !IO).
% Worker thread: receive and process work items, and send the result to master, until an "end" is received.
:- pred handle_work(
int::in,
thread.channel.channel(workermsg)::in,
thread.channel.channel(mastermsg)::in,
thread.semaphore.semaphore::in,
io::di, io::uo
) is cc_multi.
handle_work(Num, WorkChannel, ResChannel, Sem, !IO) :-
thread.channel.take(WorkChannel, Msg, !IO),
pflush("Worker " ++ string(Num) ++ ": received " ++ string(Msg) ++ "\n", !IO),
(
Msg = work(Wait),
nanosleep(0u64, Wait, !IO),
pflush("Worker " ++ string(Num) ++ ": sending result " ++ string(uint64.cast_to_int(Wait)) ++ "\n", !IO),
thread.channel.put(ResChannel, result(Wait), !IO),
thread.semaphore.signal(Sem, !IO),
handle_work(Num, WorkChannel, ResChannel, Sem, !IO)
;
Msg = end
).
% Spawn all the worker threads.
:- pred spawn_workers(
int::in, % Must be 0, when called from the outside
int::in, % Number of worker threads to be spawned
thread.channel.channel(workermsg)::in, % Work channel
thread.channel.channel(mastermsg)::in, % Result channel
thread.semaphore.semaphore::in, % Semaphore
io::di, io::uo
) is cc_multi.
spawn_workers(Num, Count, WorkChannel, ResChannel, Sem, !IO) :-
(
if
Num = Count
then
true
else
io.flush_output(stdout_stream, !IO),
thread.spawn(worker(Num, WorkChannel, ResChannel, Sem), !IO),
spawn_workers(Num + 1, Count, WorkChannel, ResChannel, Sem, !IO)
).
% Output some text atomically. We convert the text to a string first, so it can be written out in one piece.
:- pred pflush(string::in, io::di, io::uo).
pflush(Str, !IO) :-
io.write_string(Str, !IO),
io.flush_output(stdout_stream, !IO).
%----------------------------------------------------------------------------------------------------
% nanosleep
:- pragma foreign_code("C",
"
#include <time.h>
").
:- pred nanosleep(uint64::in, uint64::in, io::di, io::uo) is det.
:- pragma foreign_proc("C",
nanosleep(Secs::in, NSecs::in, IOin::di, IOout::uo),
[ will_not_call_mercury, promise_pure, thread_safe, will_not_throw_exception ],
"
struct timespec req;
req.tv_sec = (time_t) Secs;
req.tv_nsec = (time_t) NSecs;
nanosleep(&req, 0);
IOout = IOin;
").
%----------------------------------------------------------------------------------------------------
% Random number generation
:- pred rand64(uint64::out, io::di, io::uo) is det.
rand64(Random, !IO) :-
get_maybedummy(MaybeDummy, !IO),
(
% Already seeded the random generator
MaybeDummy = yes(Dummy)
;
% Seed the random generator
MaybeDummy = no,
open_system_rng(MaybeHandle, !IO),
(
MaybeHandle = maybe.ok(Handle),
random.system_rng.generate_uint64(Handle, Seed1, !IO),
random.system_rng.generate_uint64(Handle, Seed2, !IO),
random.system_rng.generate_uint64(Handle, Seed3, !IO),
random.sfc64.seed(Seed1, Seed2, Seed3, P, RandState),
make_io_urandom(P, RandState, Dummy, !IO),
set_maybedummy(yes(Dummy), !IO)
;
MaybeHandle = maybe.error(Msg),
unexpected($pred, "Error opening system random number generator:\n" ++ Msg)
)
),
random.generate_uint64(Dummy, Random, !IO).
% Mutable variable for storing the dummy value which is needed for the random.generate_uint64 predicate. So we
% don't need to carry this value around. See section "10.6 Module-local mutable variables" in the Language
% Reference Manual.
:- mutable(
maybedummy,
maybe(random.io_urandom(random.sfc64.params, random.sfc64.ustate)),
no,
ground,
[attach_to_io_state, untrailed, thread_local]
).