[M3devel] Performance issues with Process.Wait under userthreads
Mika Nystrom
mika at async.caltech.edu
Sat Feb 12 21:43:02 CET 2011
Hi again m3devel (especially Tony),
I have finally taken a bite of a problem that has been annoying me
for a very, very long time.
Under user threads, the code for Process.Wait is as follows (see
ThreadPosix.m3):
PROCEDURE WaitProcess (pid: int; VAR status: int): int =
(* ThreadPThread.m3 and ThreadPosix.m3 are very similar. *)
CONST Delay = 0.1D0;
BEGIN
LOOP
WITH r = Uexec.waitpid(pid, ADR(status), Uexec.WNOHANG) DO
IF r # 0 THEN RETURN r END;
END;
Pause(Delay);
END;
END WaitProcess;
It inserts a 0.1 second delay after each failed waitpid. This is extremely
annoying for programs that start a long sequence of child processes and
wait for them in sequence. Namely, the compiler itself. As a result
the cm3 compiler (and PM3's m3build) are normally very very slow when
using user threads. For about the last ten years, I've had a hacked
up m3build (for my PM3 installation) that skips the Pause and busy-waits
instead.
Note there is another problem here. Since the Modula-3 runtime ignores
SIGCHLD, no zombie processes are created since the Unix system automatically
reaps the child processes. I can see this would be a problem since PIDs
are eventually reused and ... couldn't Uexec.waitpid wind up referring to
the wrong process??
I will further note that the comment in Process.i3 reads as follows:
PROCEDURE Wait(p: T): ExitCode;
Wait until the process with handle p terminates, then free the operating system resources associated with the process and return an exit code indicating the reason for its termination. It is a checked runtime error to call Wait twice on the same process handle.
I am going to take this as fair warning that Process.Create *may* use
resources that are not going to be released until Process.Wait has been
called.
I have modified (in my local copy of CM3) the system as follows.
I have come up with a semi-general mechanism for immediately unblocking
a thread on the receipt of a unix signal.
1. The system relies on changing
ThreadPosix.XPause such that if a signal is allowed to wake up a threads,
that fact is recorded in a new field in the thread's descriptor
record (of type ThreadPosix.T).
2. On receipt of a waited-for unix signal, a mask is set and control
is passed to the thread scheduler which maintains the non-zero mask for
exactly one iteration through the thread ring.
3. If a thread is paused and waiting for EITHER a signal or some time,
the thread is released for running and the thread's waiting state is
cleared.
The changes are more or less as follows:
1. I have added a new field of type "int" to ThreadPosix.T:
(* if state = pausing, the time at which we can restart *)
waitingForTime: Time.T;
+ (* if state = pausing, the signal that truncates the pause *)
+ waitingForSig: int := -1;
+
(* true if we are waiting during an AlertWait or AlertJoin
or AlertPause *)
alertable: BOOLEAN := FALSE;
2. Modifications to pause:
+ PROCEDURE SigPause(n: LONGREAL; sig: int)=
+ <*FATAL Alerted*>
+ VAR until := n + Time.Now ();
+ BEGIN
+ XPause(until, FALSE, sig);
+ END SigPause;
+
PROCEDURE AlertPause(n: LONGREAL) RAISES {Alerted}=
VAR until := n + Time.Now ();
BEGIN
XPause(until, TRUE);
END AlertPause;
! PROCEDURE XPause (READONLY until: Time.T; alertable := FALSE; sig:int := -1)
! RAISES {Alerted} =
BEGIN
INC (inCritical);
self.waitingForTime := until;
self.alertable := alertable;
+ IF sig # -1 THEN
+ self.waitingForSig := sig
+ END;
ICannotRun (State.pausing);
DEC (inCritical);
InternalYield ();
3. The received-signals mask:
! CONST MaxSigs = 64;
! TYPE Sig = [ 0..MaxSigs-1 ];
!
! (* in order to listen to other signals, they have to be enabled in
! allow_sigvtalrm as well *)
! VAR (*CONST*) SIGCHLD := ValueOfSIGCHLD();
!
! gotSigs := SET OF Sig { };
!
ValueOfSIGCHLD() is a C function used to get the value of the SIGCHLD
constant without guessing at it (in ThreadPosixC.c).
4. changes to the signal handler:
! PROCEDURE switch_thread (sig: int) RAISES {Alerted} =
BEGIN
allow_sigvtalrm ();
!
! INC(inCritical);
! (* mark signal as being delivered *)
! IF sig >= 0 AND sig < MaxSigs THEN
! gotSigs := gotSigs + SET OF Sig { sig }
! END;
! DEC(inCritical);
!
! IF inCritical = 0 AND heapState.inCritical = 0 THEN
! InternalYield ()
! END;
END switch_thread;
Note that I don't know if INC/DEC(inCritical) does exactly the right
thing here.
5. changes to the scheduler:
a. thread wakeup
IF t.alertable AND t.alertPending THEN
CanRun (t);
EXIT;
+
+ ELSIF t.waitingForSig IN gotSigs THEN
+ t.waitingForSig := -1;
+ CanRun(t);
+ EXIT;
ELSIF t.waitingForTime <= now THEN
CanRun (t);
EXIT;
!
b. clearing the mask
END;
END;
+ gotSigs := SET OF Sig {};
+
IF t.state = State.alive AND (scanned OR NOT someBlocking) THEN
IF perfOn THEN PerfRunning (t.id); END;
(* At least one thread wants to run; transfer to it *)
6. changes to WaitProcess (Process.Wait):
PROCEDURE WaitProcess (pid: int; VAR status: int): int =
(* ThreadPThread.m3 and ThreadPosix.m3 are very similar. *)
! CONST Delay = 10.0D0;
BEGIN
LOOP
WITH r = Uexec.waitpid(pid, ADR(status), Uexec.WNOHANG) DO
IF r # 0 THEN RETURN r END;
END;
! SigPause(Delay,SIGCHLD);
END;
END WaitProcess;
7. install signal handler even if program is single-threaded:
BEGIN
+ (* we need to call set up the signal handler for other reasons than
+ just thread switching now *)
+ setup_sigvtalrm (switch_thread);
END ThreadPosix.
8. modify signal handler in ThreadPosixC.c to catch SIGCHLD:
sigemptyset(&ThreadSwitchSignal);
sigaddset(&ThreadSwitchSignal, SIG_TIMESLICE);
+ sigaddset(&ThreadSwitchSignal, SIGCHLD);
act.sa_handler = handler;
act.sa_flags = SA_RESTART;
sigemptyset(&(act.sa_mask));
if (sigaction (SIG_TIMESLICE, &act, NULL)) abort();
+ if (sigaction (SIGCHLD, &act, NULL)) abort();
I'll send the complete diff in a separate message for those who want
to study it more closely.
I propose the above changes for inclusion in the current CM3 repository.
Mika
More information about the M3devel
mailing list