Process allocation control: the parallel smileys example

This note has been updated since its first release to correct some problems on Linux platforms.

When an application uses multiple processes (see multiprocessing.htm), two issues can arise: how to allocate processor time among the processes and how often to switch between one process and another. These two issues are essentially one: allocation is controlled, to the first order, by the frequency of switching.

The example in the note uses a simple animation to demonstrate visually a couple of ways to control the frequency of process switching in an application. Specifically, it shows how you can use a smaller process-quantum for more rapid switching, or frequent calls to process-allow-schedule for even more rapid switching.

For most applications, the programmer may not need to worry about how frequently the various processes are allowed to run. For a non-interactive program, it usually doesn't matter because no user is waiting on any particular process. At the other extreme, an interactive Common Graphics application usually performs short activities that the user waits on before doing the next one.

If an interactive Common Graphics application needs to respond to mouse and keyboard gestures promptly while something else is busy running in the background, then you could simply use a higher process-priority for the Common Graphics process that creates the windows and handles their events, so that it will always take priority over the background process. The Integrated Development Environment (IDE) does exactly this, in fact: the IDE GUI process that created the IDE windows has a priority of 8 (higher than the default of zero), so that it responds to user gestures even when user code is busy running in one of the IDE Listener processes. You could test this trivially by evaluating (dotimes (j 2000000)) in an IDE listener, and then clicking on various IDE windows before the call returns.

But the example in this note (which works on Windows and Linux running the IDE and Common Graphics) addresses a more special case, where multiple processes all need to run frequently. It uses a variation of the Smiley example from the IDE's Navigator dialog, where each one of several processes animates its own copy of the Smiley image. The movement of the various Smileys reveals when each of the various processes is running. While this is a contrived example (where there would be no need to use multiple processes at all), the multiple Smileys help you visualize how frequently multiple processes can switch.

The first thing you need to do is to load the program itself. The code is available for download by clicking here. Download it and save it in some convenient location. Use the File | Compile and Load menu command to compile and load the file. You could go ahead and read some of the comments or code if you like, though it's probably best to do that after trying the example calls further below, to investigate how each one worked.

(in-package :cg-user)

;;; Set up some general size parameters.
(defconstant mp-smiley-radius 36)
(defconstant mp-smiley-margin 4)
(defconstant mp-smiley-centre
    (make-position (+ mp-smiley-radius mp-smiley-margin)
                   (+ mp-smiley-radius mp-smiley-margin)))
(defconstant mp-smiley-box-side (* (+ mp-smiley-radius mp-smiley-margin) 2))

(defun mp-smiley-bitmap-stream ()
  
  ;; This function draws the smiley image a single time on a bitmap-stream.
  ;; Later the image will be animated quickly by simply copying the pixel
  ;; block from the bitmap-stream to the visible window at different positions.
  (let ((bw (open-stream 'bitmap-stream nil nil
              :page-width mp-smiley-box-side
              :page-height mp-smiley-box-side))
        (left-eye (position+ mp-smiley-centre (make-position -21 -11)))
        (right-eye (position+ mp-smiley-centre (make-position 21 -11)))
        (left-pupil (position+ mp-smiley-centre (make-position -21 -6)))
        (right-pupil (position+ mp-smiley-centre (make-position 21 -6))))
    (setf (foreground-color bw) magenta)
    (fill-circle bw mp-smiley-centre mp-smiley-radius)
    (erase-circle-arc bw mp-smiley-centre
                      (round (* 0.7 mp-smiley-radius))
                      30 120)
    (setf (foreground-color bw) white)
    (fill-circle bw left-eye 10)
    (fill-circle bw right-eye 10)
    (setf (foreground-color bw) blue)
    (fill-circle bw left-pupil 5)
    (fill-circle bw right-pupil 5)
    bw))

(defun mp-next-centre (centre smiley-stream step)
  
  ;; This utility function decides how to move smiley by a single
  ;; pixel, reversing its direction when it hits a wall.  Since
  ;; the multiple smileys move in only one direction, this is
  ;; simpler than the regular smiley example.
  (without-interrupts
    (with-boxes (box1)
      (let* ((bounding-box (nvisible-box smiley-stream box1)))
        (cond
         ((minusp (box-left centre))
          (setq step (nmake-position step 1 (position-y step))))
         ((> (box-right centre) (box-right bounding-box))
          (setq step (nmake-position step -1 (position-y step)))))
        (nbox-move centre step)))))
  
(defun animate-smiley (smiley-bitmap-stream smiley-pane index max-steps totals
                                            process-allow-schedule
                                            process-pending-events
                                            ending-time)
  
  ;; This function performs the complete smiley-drawing loop for
  ;; one particular process.
  (do* ((to-box (make-box-relative 0 (* index mp-smiley-box-side)
                                   mp-smiley-box-side mp-smiley-box-side))
        (from-box (make-box 0 0 mp-smiley-box-side mp-smiley-box-side))
        (step (make-position 1 0)))
       
       ;; Stop the animation for this process if it has moved its smiley
       ;; the requested maximum number of steps ...
       ((or (minusp (decf max-steps))
            
            ;; ... or if the maximum number of seconds have gone by
            ;; (since most examples don't allow you to interrupt the test).
            (> (get-universal-time) ending-time)))
    
    (if* (windowp smiley-pane)
            
            ;; This call to ignore-errors is needed only because we
            ;; can't yet call process-pending-events (see below) to
            ;; handle other events during the animation without
            ;; invalidating the multiprocessing test, and so the
            ;; call to windowp above will not realize that the user
            ;; has closed the window in the middle of the animation,
            ;; and will try to draw to it anyway and signal an error.
       then (ignore-errors
             
             ;; This call to without-interrupts will prevent the
             ;; Windows API foreign function that draws smiley
             ;; from releasing the heap, which would invalidate the
             ;; test by causing a process switch after each draw,
             ;; making the animation smoother than it would be otherwise.
             (without-interrupts
               
               ;; This call copies smiley from the unseen
               ;; bitmap-stream to the visible window.
               (copy-stream-area
                smiley-pane smiley-bitmap-stream 
                (mp-next-centre to-box smiley-pane step)
                from-box po-replace)))
            
            ;; If the user has closed the test window, then do nothing.
       else (return))
    
    ;; Count the number of times this process was invoked.
    (incf (nth index totals))
    
    ;; If we call process-allow-schedule each time smiley is drawn,
    ;; it causes each process to move its smiley by only one step
    ;; each time the process runs, making the animation as evenly
    ;; divided between the processes as possible.
    (when process-allow-schedule
      (mp:process-allow-schedule))
    
    ;; This call to cg:process-pending-events-if-event-handler
    ;; each time smiley is drawn allows other mouse clicks and
    ;; key presses to be handled while the animation is running.
    
    ;; This call has no effect on Linux.  On non-OS-threads
    ;; platforms there is only a single event queue, and so
    ;; things get confused if multiple processes handle events
    ;; from the single queue.  So the new function
    ;; process-pending-events-if-event-handler was added to
    ;; always call process-pending-events on native-threads
    ;; platforms (Microsoft Windows), but to call it on 
    ;; non-native-threads platforms (Linux) only when called
    ;; in the single dedicated event-handling process.  This
    ;; example code does not run in the event-handling process,
    ;; and so the call will do nothing on Linux.

    ;; Currently this would invalidate our test even if no other
    ;; events are happening at the time, because it would
    ;; make a heap-releasing call to win:PeakMessage, which would
    ;; cause a process switch each time smiley is drawn, making the
    ;; animation very smooth when it otherwise would not be.  In a
    ;; future release, we expect to change win:PeakMessage and various
    ;; other Windows API functions to no longer release the heap
    ;; (for general efficiency).  That change may require an application
    ;; to make calls to process-allow-schedule that had not been
    ;; necessary previously, if it needs to achieve very rapid
    ;; process-switching between multiple graphics-drawing processes.
    (when process-pending-events
      (process-pending-events-if-event-handler))))

(defun mp-smiley (&key (tracks 3)(max-steps 10000) quantum
                       process-allow-schedule process-pending-events
                       (max-seconds 10))
  
  ;; This is the main function that starts up a Common Graphics
  ;; visualization of process switching, using the "smiley" demo.
  (let* ((smiley-bitmap-stream (mp-smiley-bitmap-stream))
         (smiley-frame (make-window :bitmap-window
                         :class 'bitmap-window
                         :title "Multiprocessing Smiley"
                         :interior
                         (make-box-relative
                          200
                          (+ 40 
                             (if* (use-ide-parent-window
                                   (configuration *ide-system*))
                                then 0
                                else (bottom (find-window
                                              :ide-project-window))))
                          (* 12 mp-smiley-radius)
                          (* tracks mp-smiley-box-side))
                         :scrollbars nil))
         (smiley-pane (frame-child smiley-frame))
         (ending-time (+ (get-universal-time)
                         max-seconds))
         (totals (make-list tracks :initial-element 0)))
    (set-foreground-window smiley-frame)
    (update-window smiley-pane)
    
    ;; On non-OS threads platforms (Linux), this allows the
    ;; *single-cg-event-handling-process* to run so that the
    ;; redisplay event for the window gets handle to draw
    ;; the window frame before the animation begins.
    #-os-threads
    (sleep 0.1)
    
    ;; Run all of the "tracks" but the first one in a new process
    ;; for each track.  (A "track" is one smiley moving horizontally
    ;; in one "row" of the window.)
    (dotimes (j (1- tracks))
      (mp:process-run-function
       (list :name (format nil "~:(~:r~) Smiley" (+ j 2))
             :quantum (or quantum 2)
             :initial-bindings *default-cg-bindings*)
       #'animate-smiley smiley-bitmap-stream smiley-pane
       (1+ j) max-steps totals process-allow-schedule
       process-pending-events ending-time))
    
    ;; Run the first (topmost) track in the current process.
    ;; Temporarily set the quantum of this process as requested,
    ;; using an unwind-protect to ensure that we set it back
    ;; to what it was previously.
    (let* ((old-quantum (mp:process-quantum sys:*current-process*)))
      (unwind-protect
          (progn
            (when quantum
              (setf (mp:process-quantum sys:*current-process*) quantum))
            (animate-smiley smiley-bitmap-stream smiley-pane
                            0 max-steps totals process-allow-schedule
                            process-pending-events ending-time))
        (when quantum
          (setf (mp:process-quantum sys:*current-process*) old-quantum))))
    
    ;; Return a list of the number of times that each process
    ;; was invoked.  (These numbers would differ only if the
    ;; test window were closed before the animation ended, which
    ;; requires the process-pending-events option.)
    totals))

Now that we've loaded the program, we will make a few test calls and observe differences in process-switching frequency. These are arranged from "worst" to "best" order, so please do not get discouraged early on.

The make-smiley function creates a window and displays smiley faces moving back and forth across the window. The number of smileys and how often control switches between smileys is controlled by the parameters. Another parameter controls the maximum time that the example will run. It is set in the examples below to 10 seconds. You can change that value as desired.

You may notice when running these tests that mouse and keyboard events are not handled until a test completes. That is intentional to avoid inadvertently triggering process switches that would invalidate the tests as described. (You cannot easily close the example window until the example code completes. The max-seconds argument allows you to specify how long the example runs.) See the final test for one way to handle other window events during the animation.

This first test uses the default process-switching behavior that gives each process a two-second time slice each time it runs. It's easy to see that this is no good for animating multiple smileys smoothly simultaneously.

(mp-smiley :tracks 2
           :max-steps 30000
           :max-seconds 10
           :quantum 2
           :process-allow-schedule nil)

This second test uses a smaller process-quantum (time slice) of 0.1 seconds. That may be rapid enough for many applications, but still does not make each smiley move smoothly throughout the entire animation. In this test, we have four simultaneous processes this time instead of the two used in the last test. (Note that the 0.1 is the smallest valid process-quantum, and the behavior for smaller values is unpredictable, so this test takes us about as far as the process-quantum feature can take us.

(mp-smiley :tracks 4
           :max-steps 20000
           :max-seconds 10
           :quantum 0.1
           :process-allow-schedule nil)

This third test makes a call to mp:process-allow-schedule each time a smiley is drawn at a new position. This causes the processes to switch after every individual step of the animation, and therefore makes each smiley move as smoothly as possible. The switching is rapid and smooth even with six (or more) processes. Some smileys may get somewhat ahead of others as their processes are called slightly more often, but all of the processes typically will run a comparable number of times (though this is not guaranteed).

(mp-smiley :tracks 6
           :max-steps 10000
           :max-seconds 10
           :quantum 2
           :process-allow-schedule t)

The next and final test causes cg:process-pending-events to be called each time a process draws a smiley. This allows other mouse and keyboard events to be handled during the animation (allowing you to close the window in the middle of the animation, for example). We didn't call cg:process-pending-events in earlier tests because it calls heap-releasing Windows API foreign functions, which would cause process switches that otherwise would not occur, thereby invalidating the above tests. It also slows things down somewhat as the operating system frequently checks system-wide for other processes that may have events queued up.

This example will show a difference only on Microsoft Windows. On Linux, where we have not implemented native threads, it is dangerous to handle events in multiple processes. So the code above actually calls the new function cg:process-pending-events-if-event-handler, which does nothing on non-native-threads platforms when called in any process except the single dedicated event-handling process.

Note that on Windows, if you move the mouse around in the test window or click in it, that the uppermost smiley will slow down or even pause. The reason is that the process that is animating that particular smiley is the same one that created the window. That causes the window's events to be handled in the same process that's animating the topmost smiley, stealing some of its animation time. (You may also see some jerkiness. Code now being tested for a later release does not exhibit this jerkiness. Something to look forward to ...)

(mp-smiley :tracks 6
           :max-steps 10000
           :max-seconds 10
           :quantum 2
           :process-allow-schedule t
           :process-pending-events t)
Copyright © 2023 Franz Inc., All Rights Reserved | Privacy Statement Twitter