Overriding Functions Top The Main LibraryConcurrency Contents

Concurrency

Scam provides for concurrency using lightweight threads. The following subsections describes the built-in concurrency and concurrency control functions and give details on their use.

Built-in support for threads

The following functions can be used to control threads and their interactions:

thread
This one-argument function takes in an expression and evaluates in parallel to the calling thread and returns the thread id of the new thread. The code below:
    (thread (println "Hello World 1"))
    (thread (println "Hello World 2"))
    (thread (println "Hello World 3"))

yields something similar to the following output:

    HellHelo Worllod  3
    World 1
    Hello World 2

The problem here is that threads are executing in parallel. This means that when you print out from one thread to the console another thread could be as well; and you get the overlap seen above. To get around you can either use the built in semaphore or the function displayAtomic.

gettid
This no-argument function returns the thread ID of the current thread. The code below:
    (thread (println (gettid)))

yields something similar to the following output:

    2
lock
This no-argument function acquires the built-in semaphore.
unlock
This no-argument function releases the built-in semaphore. The code below demonstrates both lock and unlock:
    (thread (begin (lock) (println "Hello World 1") (unlock)))
    (thread (begin (lock) (println "Hello World 2") (unlock)))
    (thread (begin (lock) (println "Hello World 3") (unlock)))

yields something similar to the following output:

    Hello World 2
    Hello World 3
    Hello World 1

Note: these three threads will be executed in parallel, but if you do not join on the threads using tjoin then the main process may terminate before the threads terminate. Joining on the threads can be accomplished by saving the thread id's of each thread, and then calling tjoin on the thread id's, as in the following example:

    (define t1 (thread (begin (lock) (println "Hello World 1") (unlock))))
    (define t2 (thread (begin (lock) (println "Hello World 2") (unlock))))
    (define t3 (thread (begin (lock) (println "Hello World 3") (unlock))))

    (tjoin t1)
    (tjoin t2)
    (tjoin t3)
tjoin
This one-argument function causes the current thread to wait until a particular thread terminates. If the desired thread has already terminated, the function immediately returns. The desired thread is specified by passing its thread ID to the function. The code below:
    (define firstTID (thread (println (fib 25))))
    (tjoin firstTID)
    (thread (println (fib 10)))

yields something similar to the following output:

   75025
   55

Note that the thread which is evaluating fibonacci of 10 must wait until the thread which is evaluating fibonacci of 25 has finished.

displayAtomic
This variadic function is similar to display; however, it ensures that there will be no overlap of output if the user only uses displayAtomic for printing. The code below:
    (thread (displayAtomic "Hello World 1\n"))
    (thread (displayAtomic "Hello World 2\n"))
    (thread (displayAtomic "Hello World 3\n"))

yields something similar to the following output:

    Hello World 2
    Hello World 1
    Hello World 3

Thread pools

In order to avoid the sometimes inevitable system-level restrictions on the maximum number of lightweight threads allowed per process, Scam supports the creation of thread pools. A thread pool pre-allocates a fixed number of threads, and maintains a queue of expressions. Expressions may be pushed onto the queue, and the thread pool will automatically pop the expressions off in order to be executed concurrently. The code below illustrates the use of a pool:

        (define pool (tpool 10))
        ((pool 'push) (fib 10))
        ((pool 'push) (fib 11))
        ((pool 'push) (fib 12))
        ((pool 'shutdown))
    

Note: You must call the shutdown method of your thread pool. If you fail to do this then the threads in the pool may not finish before the main process terminates.

The following constructors and methods are associated with pools:

tpool
This one-argument constructor creates a new thread pool object. The single argument specifies the number of concurrent threads allowed.
push
This variadic function takes in an expression and an optional number of call-back functions. The call-back functions are called with the result of the evaluated expression.
        ...
        ((pool 'push) (fib 12) println)
        ...
    

Would result in calling the function `println', passing the return value of `fib'.

push*
This variadic function takes in an expression, a environment, and an optional number of expressions. The required expression is evaluated under the given environment. The optional expressions are then evaluated with the return value of the evaluated required expression.
empty?

This no-argument function returns true if there are no expressions in the work queue or in the running queue, otherwise it returns false.

join
This no-argument function turns off acceptance of new expressions until the running queue is empty.
shutdown
This no-argument function waits for the active threads to finish before letting the thread pool expire.

Parallel execution of expressions

For compatibility with MIT Scheme, expressions in Scam can be evaluated in parallel with the variadic function pexecute:

    (pexecute expr1 expr2 .... exprN)

which is equivalent to the following:

    ; store the thread id's
    (define tids nil)
    (begin
        (set! tids (cons (thread expr1) tids))
        (set! tids (cons (thread expr2) tids))
        ...
        (set! tids (cons (thread exprN) tids))
        ; join on the threads
        (while (!= tids nil)
           (tjoin (car tids))
           (set! tids (cdr tids))
           )
        )

Each of the expressions will execute in parallel in separate processes. The expressions passed to pexecute are calls to no argument functions, or lambdas.

    (pexecute f g (lambda () ...))

In the above example, the functions f and g and the body of the lambda will all be executed in parallel.

Another function, pexecute*, is similar to pexecute, except that it serializes each expression, in the order given. The call:

    (pexecute* expr1 expr2 .... exprN)

is equivalent to the following:

    (begin
        (expr1)
        (expr2)
        ...
        (exprN)
        )

The pexecute* function is useful for debugging concurrency problems.

Debugging concurrency problems

Locking and unlocking of threads can be difficult to debug. For this reason, Scam has a built-in function for determining which thread has the current lock. This function, debugSemaphore enables and disables the debugging mechanism.

    (debugSemaphore #t)
    (debugSemaphore #f)

The first call turns debugging on while the second turns debugging off. When on, attempts to acquire the semaphore produce output (on stderr) of the form:

    thread XXXX is acquiring...

where XXXX is replaced by the process id of the acquiring process. If the semaphore is actually acquired, debugging emits:

    thread XXXX has acquired.

On the releasing side, debugging emits messages of the form:

    thread XXXX is releasing...
    thread XXXX has released.

When a process executing in parallel throws an exception, pexecute will produce an error message similar to:

    file philosophers.scm,line 356: parallel execution of thread 3 failed
    try using pexecute* for more information

If a thread terminates with an exception, calling pexecute* may reveal the exception that caused the failure. The pexecture* call simulates concurrency, but actually runs the given expressions sequentially in the parent process.

lusth@cs.ua.edu


Overriding Functions Top The Main LibraryConcurrency Contents