The Department of Computer Science & Engineering |
STUART C. SHAPIRO: CSE
305
|
I will discuss concurrency by showing examples that illustrate concurrency as independent threads that must coordinate with each other because they share some common resource. I will use the Python thread module for these examples.
Here's a program that shows threads running independently:
#! /util/bin/python import thread # Program thread.py # Demonstrates that multiple threads can execute in an interleaved fashion. Max = 1000000 def count(label,max): for i in range(max): print label + ":", i print "Starting thread.py" thread.start_new_thread(count, ("B", Max)) thread.start_new_thread(count, ("C", Max)) count("A", Max)
If Max
is too small, thread A
will exit
before the other treads get their chances, and, in our system,
all threads will terminate.
#! /util/bin/python import thread # Program threadIO.py # Demonstrates use of IO interrupt to pass control to other threads Max = 100 def count(label,max): for i in range(max): print label + ":", i print label, "is finished." print "Starting threadIO.py" thread.start_new_thread(count, ("B", Max)) thread.start_new_thread(count, ("C", Max)) count("A", Max)
An IO interrupt will block a thread, and give others their chance:
#! /util/bin/python import thread # Program threadIOWait.py # Demonstrates use of IO interrupt to pass control to other threads Max = 100 def count(label,max): for i in range(max): print label + ":", i print label, "is finished." print "Starting threadIOWait.py" thread.start_new_thread(count, ("B", Max)) thread.start_new_thread(count, ("C", Max)) count("A", Max) raw_input("A is finished, OK?")
Threads can share resources, but there can be problems:
#! /util/bin/python import thread # Program threadCount.py # Demonstrates problems when concurrent processes use common resources. counter = 0 Max = 10000 def incCounter(label,delta): global counter counter += 1 print label, "increments counter to: ", counter def count(label,max): global counter for i in range(max): incCounter(label,1) print label, "is finished." print "Starting threadCount.py" thread.start_new_thread(count, ("B", Max)) thread.start_new_thread(count, ("C", Max)) count("A", Max) raw_input("A is finished, OK?") print "Total count is", counter
Resources can be shared more equitably by the use of locks, also called semaphores:
#! /util/bin/python import thread # Program threadCountLocks.py # Demonstrates use of a lock (semaphore) to block resource contention. counter = 0 Max = 10000 lock = thread.allocate_lock() def incCounter(label,delta): global counter lock.acquire() counter += delta lock.release() print label, "increments counter to: ", counter def count(label,max): for i in range(max): incCounter(label,1) print label, "is finished." print "Starting threadCountLocks.py" thread.start_new_thread(count, ("B", Max)) thread.start_new_thread(count, ("C", Max)) count("A", Max) raw_input("A is finished, OK?") print "Total count is", counter
We can combine semaphores with a global active thread counter to have the main subroutine wait until all child threads are done, without doing a needless read:
#! /util/bin/python import thread # Program threadCountLocksWait.py # Demonstrates use of a "private" resource, # a lock (semaphore) to block resource contention, # and a global active thread counter to make sure all threads finish. counter = 0 counterLock = thread.allocate_lock() activeThreads = 0 activeThreadsLock = thread.allocate_lock() Max = 100 def incActiveThreads(delta): global activeThreads activeThreadsLock.acquire() activeThreads += delta activeThreadsLock.release() def incCounter(label,delta): global counter counterLock.acquire() counter += delta print label, "increments counter to: ", counter counterLock.release() def count(label,max): for i in range(max): incCounter(label,1) print label, "is finished." incActiveThreads(-1) print "Starting threadCountLocksWait.py" incActiveThreads(1) thread.start_new_thread(count, ("B", Max)) incActiveThreads(1) thread.start_new_thread(count, ("C", Max)) incActiveThreads(1) count("A", Max) while activeThreads > 0: pass print "Total count is", counter
The major example of this section will be an eager-beaver or
implemented in Python.
First, a short reminder of the use of closures for passing
expressions:
#! /util/bin/python # Program closures.py # Demonstrates the use of closures def evalit(closures): for f in closures: print f, "=", f() def subA(): def subB(): loc = 3 evalit([lambda:loc, lambda:nloc, lambda:glob]) nloc = 7 subB() glob = 9 subA() --------------------------------------------------------------- <timberlake:ConcurrentPrograms:1:264> python closures.py <function <lambda> at 0x2ac0bd6f0320> = 3 <function <lambda> at 0x2ac0bd6f0398> = 7 <function <lambda> at 0x2ac0bd6f0410> = 9
Next, a short-circuit or
implemented in Python:
#! /util/bin/python # Program shortCircuitOr.py # Demonstrates the use of lambda and function application for passing expressions n = 0 def scOr(closures): fcount = 1 for f in closures: print "Evaluating expression", fcount fcount += 1 if f(): return True return False def sub(): m = 7 return scOr([lambda:n>3, lambda:m==7, lambda:False]) n += 1 if sub(): print "It's true" else: print "It's false" -------------------------------------------------------- <timberlake:ConcurrentPrograms:1:265> python shortCircuitOr.py Evaluating expression 1 Evaluating expression 2 It's true
The problem with short-circuit operators is that they evaluate their arguments in order, and it might be that evaluating some early argument is very slow, while evaluating a later argument is very fast:
#! /util/bin/python # Program shortCircuitOrSlow.py # Shows that short circuit or can be slow def scOr(closures): fcount = 1 for f in closures: print "Evaluating expression", fcount fcount += 1 if f(): return True return False def slowFalse(): for i in range(10000000): pass return False def sub(): return scOr([lambda:slowFalse(), lambda:True, lambda:slowFalse]) if sub(): print "It's true" else: print "It's false"
Now for the eager-beaver or
:
#! /util/bin/python import thread # Program eagerBeaverOr.py def ebOr(closures): global activeThreads activeThreads = 0 activeThreadsLock = thread.allocate_lock() def incActiveThreads(delta): global activeThreads activeThreadsLock.acquire() activeThreads += delta activeThreadsLock.release() global orResult orResult = False orResultLock = thread.allocate_lock() def disjoin(bool): global orResult orResultLock.acquire() orResult = orResult or bool orResultLock.release() def evalDisjunct(d): print "Starting a thread" global orResult if not orResult: disjoin(d()) incActiveThreads(-1) print "Quitting a thread." for f in closures: incActiveThreads(1) thread.start_new_thread(evalDisjunct, (f,)) while (not orResult) and (activeThreads>0): pass return orResult def slowFalse(): for i in range(10000000): pass return False def sub(): return ebOr([lambda:slowFalse(), lambda:True, lambda:slowFalse]) if sub(): print "It's true" else: print "It's false"
We'll compare the running times of the eager beaver or
with the short-circuit or
(using the Unix time
command),
both using slowFalse
:
<timberlake:ConcurrentPrograms:1:382> time python shortCircuitOrSlow.py Evaluating expression 1 Evaluating expression 2 It's true 1.393u 0.362s 0:01.77 98.8% 0+0k 0+0io 0pf+0w <timberlake:ConcurrentPrograms:1:383> time python eagerBeaverOr.py Starting a threadStarting a thread Starting a thread Quitting a thread. Quitting a thread. Quitting a thread. It's true 0.012u 0.015s 0:00.03 66.6% 0+0k 0+0io 0pf+0w"This can be interpreted the following way....
1.189u - the time your command spent processing in user mode
0.334s - the time your command spent processing in kernel mode
0:01.62 - the amount of elapsed real time that your command took to complete. Note that this is not the sum of user mode and kernel mode CPU times. The .097 is the time spent for things like interprocess communication, scheduling times, etc...
93.2% - The percentage of CPU time this process got. (1.189+.334)/1.62
0+0k -Average shared text space use by your process plus average size of unshared data which gives the max size which was resident in memory.
0+0io - number of file system input and number of file system outputs.
0pf - Number of page faults both where a read/write had to come from disk and or unclaimed virtual memory pages.
+0w - Number of times the proccess was swapped out of main memory." [Kevin Cleary via email]
Erlang uses a different model of concurrency from Python's. Erlang does not support shared memory, but uses a message-passing model, using
spawn(<module>, <function-name>,
[<arguments>])
returns a ProcessID (Pid).
Pid ! <message>
receive <Pattern> [when <Guard>] -> <expr> {, <expr>} {; <Pattern> [when <Guard>] -> <expr> {, <expr>}} end
Erlang variables are single assignment, and most Erlang data structures are immutable. One of the few mutable data structures is the Erlang Term Storage (ETS).
Combining these ideas, the Erlang version of the counter program is
This can be run by doing-module(processCount). -export([counterResource/0,count/3,processCount/0]). -import(ets,[]). counterResource() -> receive initialize -> io:fwrite("Initializing countTable.~n"), ets:new(countTable,[public,named_table]), ets:insert(countTable,{count,0}), counterResource(); {increment,Delta,Caller} -> NewVal = ets:lookup_element(countTable,count,2)+Delta, io:fwrite("~w is incrementing counter to: ~w~n", [Caller,NewVal]), ets:insert(countTable,{count, NewVal}), counterResource(); {stop, Caller} -> Caller!{done, ets:lookup_element(countTable,count,2)}, counterResource(); finished -> true end. count(Counter,0,Sender) -> Counter!{stop, self()}, receive {done,Count} -> true end, io:fwrite("~w is done counting.~n",[self()]), Sender!{done,Count}; count(Counter,Max,Sender) -> Counter!{increment,1,self()}, count(Counter,Max-1,Sender). processCount() -> Max = 100, io:fwrite("Starting processCount.erl ~w~n",[self()]), Counter = spawn(processCount,counterResource,[]), Counter!initialize, io:fwrite("Spawning ~w,~n", [spawn(processCount,count,[Counter,Max,self()])]), io:fwrite("Spawning ~w,~n", [spawn(processCount,count,[Counter,Max,self()])]), io:fwrite("Spawning ~w,~n", [spawn(processCount,count,[Counter,Max,self()])]), receive {done,_} -> true end, receive {done,_} -> true end, receive {done,FinalCount} -> true end, Counter!finished, io:fwrite("Total count is ~w~n", [FinalCount]), halt().
erl -compile /projects/shapiro/CSE305/ConcurrentPrograms/processCount.erl erl -noshell -s processCount processCount