Principles of Software Construction: Objects, Design, and - - PowerPoint PPT Presentation

principles of software construction objects design and
SMART_READER_LITE
LIVE PREVIEW

Principles of Software Construction: Objects, Design, and - - PowerPoint PPT Presentation

Principles of Software Construction: Objects, Design, and Concurrency Concurrency Part III: Structuring Applications (Design Patterns for Parallel Computation) Michael Hilton Bogdan Vasilescu 17-214 1 Learning Goals Reuse


slide-1
SLIDE 1

1

17-214

Principles of Software Construction: Objects, Design, and Concurrency Concurrency Part III: Structuring Applications (“Design Patterns for Parallel Computation”)

Michael Hilton Bogdan Vasilescu

slide-2
SLIDE 2

2

17-214

Learning Goals

  • Reuse established libraries
  • Apply common strategies to parallelize computations
  • Use the Executor services to effectively schedule tasks
slide-3
SLIDE 3

3

17-214

Administrivia

slide-4
SLIDE 4

4

17-214

Last Tuesday

slide-5
SLIDE 5

5

17-214

Guarded methods

  • What to do on a method if the precondition is not fulfilled (e.g.,

transfer money from bank account with insufficient funds)

  • throw exception (balking)
  • wait until precondition is fulfilled (guarded suspension)
  • wait and timeout (combination of balking and guarded

suspension)

slide-6
SLIDE 6

6

17-214

Monitor Mechanics in Java (Recitation)

  • Object.wait() – suspends the current thread’s execution,

releasing locks

  • Object.wait(timeout) – suspends the current thread’s execution

for up to timeout milliseconds

  • Object.notify() – resumes one of the waiting threads
  • See documentation for exact semantics
slide-7
SLIDE 7

7

17-214

Monitor Example

class SimpleBoundedCounter { protected long count = MIN; public synchronized long count() { return count; } public synchronized void inc() throws InterruptedException { awaitUnderMax(); setCount(count + 1); } public synchronized void dec() throws InterruptedException { awaitOverMin(); setCount(count - 1); } protected void setCount(long newValue) { // PRE: lock held count = newValue; notifyAll(); // wake up any thread depending on new value } protected void awaitUnderMax() throws InterruptedException { while (count == MAX) wait(); } protected void awaitOverMin() throws InterruptedException { while (count == MIN) wait(); } }

slide-8
SLIDE 8

8

17-214

THREAD SAFETY: DESIGN TRADEOFFS

slide-9
SLIDE 9

9

17-214

Synchronization

  • Thread-safe objects vs guarded:

– Thread-safe objects perform synchronization internally (clients can always call safely) – Guarded objects require clients to acquire lock for safe calls

  • Thread-safe objects are easier to use (harder to misuse), but

guarded objects can be more flexible

slide-10
SLIDE 10

10

17-214

Designing Thread-Safe Objects

  • Identify variables that represent the object’s state

– may be distributed across multiple objects

  • Identify invariants that constraint the state variables

– important to understand invariants to ensure atomicity of operations

  • Establish a policy for managing concurrent access to state
slide-11
SLIDE 11

11

17-214

Coarse-Grained Thread-Safety

  • Synchronize all access to all state with the object

@ThreadSafe public class PersonSet { @GuardedBy GuardedBy("this" "this") private final Set<Person> mySet = new HashSet<Person>(); @GuardedBy GuardedBy("this" "this") private Person last = null; public synchronized synchronized void addPerson(Person p) { mySet.add(p); } public synchronized synchronized boolean containsPerson(Person p) { return mySet.contains(p); } public synchronized void setLast(Person p) { this.last = p; } }

slide-12
SLIDE 12

12

17-214

Fine-Grained Thread-Safety

  • “Lock splitting”: Separate state into independent regions with

different locks

@ThreadSafe public class PersonSet { @GuardedBy GuardedBy(“ (“myset myset") ") private final Set<Person> mySet = new HashSet<Person>(); @GuardedBy GuardedBy("this this") private Person last = null; public void addPerson(Person p) { synchronized synchronized (mySet mySet) { ) { mySet mySet.add .add(p); ); } } public boolean containsPerson(Person p) { synchronized synchronized (mySet mySet) { ) { return return mySet mySet.contains .contains(p); ); } } public synchronized synchronized void setLast(Person p) { this.last = p; } }

slide-13
SLIDE 13

13

17-214

Over vs Undersynchronization

  • Undersynchronization -> safety hazard
  • Oversynchronization -> liveness hazard and reduced

performance

slide-14
SLIDE 14

14

17-214

Tradeoffs

  • Strategies:

– Don't share the state variable across threads; – Make the state variable immutable; or – Use synchronization whenever accessing the state variable.

  • Thread-safe vs guarded
  • Coarse-grained vs fine-grained synchronization
  • When to choose which strategy?

– Avoid synchronization if possible – Choose simplicity over performance where possible

slide-15
SLIDE 15

15

17-214

Today

  • Design patterns for concurrency
  • The Executor framework
  • Concurrency libraries
slide-16
SLIDE 16

16

17-214

THE PRODUCER-CONSUMER DESIGN PATTERN

slide-17
SLIDE 17

17

17-214

Pattern Idea

  • Decouple dependency of concurrent producer and consumer of

some data

  • Effects:

– Removes code dependencies between producers and consumers – Decouples activities that may produce or consume data at different rates

slide-18
SLIDE 18

18

17-214

Blocking Queues

  • Provide blocking: put and take methods

– If queue full, put blocks until space becomes available – If queue empty, take blocks until element is available

  • Can also be bounded: throttle activities that threaten to

produce more work than can be handled

  • See https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html
slide-19
SLIDE 19

19

17-214

Example: Desktop Search (1)

public class FileCrawler implements Runnable {

private private final final BlockingQueue BlockingQueue<File> <File> fileQueue fileQueue;

private final FileFilter fileFilter; private final File root; ... public void run() { try { crawl(root); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void crawl(File root) throws InterruptedException { File[] entries = root.listFiles(fileFilter); if (entries != null) { for (File entry : entries) if (entry.isDirectory()) crawl(entry); else if (!alreadyIndexed(entry))

fileQueue fileQueue.put .put(entry entry); );

} } }

The producer

slide-20
SLIDE 20

20

17-214

Example: Desktop Search (2)

public class Indexer implements Runnable { private final BlockingQueue<File> queue; public Indexer(BlockingQueue<File> queue) { this.queue = queue; } public void run() { try { while (true)

indexFile indexFile(queue queue.take .take()); ());

} catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public void indexFile(File file) { // Index the file... }; }

The consumer

slide-21
SLIDE 21

21

17-214

THE FORK-JOIN DESIGN PATTERN

slide-22
SLIDE 22

22

17-214

Pattern Idea

  • Pseudocode (parallel version of the divide and conquer paradigm)

if (my portion of the work is small enough) do the work directly else split my work into two pieces invoke the two pieces and wait for the results

Image from: Wikipedia

slide-23
SLIDE 23

23

17-214

THE MEMBRANE DESIGN PATTERN

slide-24
SLIDE 24

24

17-214

Pattern Idea

Image from: Wikipedia

Multiple rounds of fork-join that need to wait for previous round to complete.

slide-25
SLIDE 25

25

17-214

TASKS AND THREADS

slide-26
SLIDE 26

26

17-214

Executing tasks in threads

  • Common abstraction for server applications

– Typical requirements:

  • Good throughput
  • Good responsiveness
  • Graceful degradation
  • Organize program around task execution

– Identify task boundaries; ideally, tasks are independent

  • Natural choice of task boundary: individual client requests

– Set a sensible task execution policy

slide-27
SLIDE 27

27

17-214

Example: Server executing tasks sequentially

  • Can only handle one request at a time
  • Main thread alternates between accepting connections and

processing the requests

public class SingleThreadWebServer { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { Socket connection = socket.accept(); handleRequest(connection); } } private static void handleRequest(Socket connection) { // request-handling logic here } }

slide-28
SLIDE 28

28

17-214

Better: Explicitly creating threads for tasks

  • Main thread still alternates bt accepting connections and dispatching requests
  • But each request is processed in a separate thread (higher throughput)
  • And new connections can be accepted before previous requests complete (higher

responsiveness)

public class ThreadPerTaskWebServer { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; new Thread(task).start(); } } private static void handleRequest(Socket connection) { // request-handling logic here } }

slide-29
SLIDE 29

29

17-214

Still, what’s wrong?

public class ThreadPerTaskWebServer { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; new Thread(task).start(); } } private static void handleRequest(Socket connection) { // request-handling logic here } }

slide-30
SLIDE 30

30

17-214

Disadvantages of unbounded thread creation

  • Thread lifecycle overhead

– Thread creation and teardown are not free

  • Resource consumption

– When there are more runnable threads than available processors, threads sit idle – Many idle threads can tie up a lot of memory

  • Stability

– There is a limit to how many threads can be created (varies by platform)

  • OutOfMemory error
slide-31
SLIDE 31

31

17-214

THE THREAD POOL DESIGN PATTERN

slide-32
SLIDE 32

32

17-214

Pattern Idea

  • A thread pool maintains multiple threads waiting

for tasks to be allocated for concurrent execution by the supervising program

– Tightly bound to a work queue

  • Advantages:

– Reusing an existing thread instead of creating a new one

  • Amortizes thread creation/teardown over multiple requests
  • Thread creation latency does not delay task execution

– Tune size of thread pool

  • Enough threads to keep processors busy while not having too many to

run out of memory

slide-33
SLIDE 33

33

17-214

EXECUTOR SERVICES

slide-34
SLIDE 34

34

17-214

The Executor framework

  • Recall: bounded queues prevent an overloaded application

from running out of memory

  • Thread pools offer the same benefit for thread management

– Thread pool implementation part of the Executor framework in java.util.concurrent – Primary abstraction is Executor, not Thread – Using an Executor is usually the easiest way to implement a producer-consumer design public interface Executor { void execute(Runnable command); }

slide-35
SLIDE 35

35

17-214

Executors – your one-stop shop for executor services

  • Executors.newSingleThreadExecutor()

– A single background thread

  • newFixedThreadPool(int nThreads)

– A fixed number of background threads

  • Executors.newCachedThreadPool()

– Grows in response to demand

slide-36
SLIDE 36

36

17-214

Web server using Executor

public class TaskExecutionWebServer { private static final int NTHREADS = 100;

private private static static final final Executor exec Executor exec = = Executors.newFixedThreadPool Executors.newFixedThreadPool(NTHREADS); (NTHREADS);

public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } };

exec.execute exec.execute(task); (task);

} } private static void handleRequest(Socket connection) { // request-handling logic here } }

slide-37
SLIDE 37

37

17-214

Easy to specify / change execution policy

  • Thread-per-task server:
  • Single thread server:

public class ThreadPerTaskExecutor implements Executor { public void execute(Runnable r) { new Thread(r).start(); }; } public class WithinThreadExecutor implements Executor { public void execute(Runnable r) { r.run(); }; }

slide-38
SLIDE 38

38

17-214

Execution policies

  • Decoupling submission from execution
  • Specify:

– In what thread will tasks be executed? – In what order (FIFO, LIFO, …)? – How many tasks may execute concurrently? – How many tasks may be queued pending execution? – …

  • Notice the strategy/template method pattern: general

mechanism but highly customizable

slide-39
SLIDE 39

39

17-214

Design goals (and tradeoffs): Task granularity and structure

  • Maximize parallelism

– The smaller the task, the more opportunities for parallelism à better CPU utilization, load balancing, locality, scalability; greater throughput

  • Minimize overhead

– Intrinsically more costly to create and use task objects than stack-frames à coarse-grained tasks

  • Minimize contention

– Maintain as much independence as possible between tasks à ideally, no shared resources, global (static) variables, locks – Some synchronization is unavoidable in fork/join designs

  • Maximize locality

– When parallel tasks all access different parts of a data set (e.g., different regions of a matrix), use partitioning strategies that reduce the need to coordinate across

slide-40
SLIDE 40

40

17-214

Finding exploitable parallelism

  • Executor framework makes it easy to specify an execution

policy if you can describe your task as a Runnable

– A single client request is a natural task boundary in server applications

  • Task boundaries are not always obvious (see next slide)
slide-41
SLIDE 41

41

17-214

Example: HTML page renderer

  • Issues:

– Underutilize CPU while waiting for I/O – User waits long time for page to finish loading

void renderPage(CharSequence source) { renderText(source); List<ImageData> imageData = new ArrayList<ImageData>(); for (ImageInfo imageInfo : scanForImageInfo(source)) imageData.add(imageInfo.downloadImage()); for (ImageData data : imageData) renderImage(data); }

slide-42
SLIDE 42

42

17-214

Result bearing tasks: Callable and Future

  • Runnable.run cannot return value or throw checked

exceptions (although it can have side effects)

  • Many tasks are deferred computations (e.g., fetching a

resource over a network) à Callable is a better abstraction

– Callable.call will return a value and anticipates that it might throw an exception

  • Runnable and Callable describe abstract computational

tasks

  • Future represents the lifecycle of a task (created, submitted,

started, completed)

slide-43
SLIDE 43

43

17-214

Callable and Future interfaces

public interface Callable<V> { V call() throws Exception; } public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException, CancellationException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, CancellationException, TimeoutException; }

slide-44
SLIDE 44

44

17-214

Creating a Future to describe a task

  • Process:

– submit a Runnable or Callable to an executor and get back a Future that can be used to retrieve the result or cancel the task – Or explicitly instantiate a FutureTask for a given Runnable or Callable

slide-45
SLIDE 45

45

17-214

Example: Page renderer with Future

  • Divide into two tasks

– Render text (CPU-bound) – Download all images (I/O-bound)

  • Steps (also go to recitation):

– Create a Callable for download subtask – Submit Callable to ExecutorService – ExecutorService returns Future describing the task’s execution – When main task reaches point where it needs the images, it waits for the result by calling Future.get

  • If lucky, images already downloaded
  • If not, at least we got a head start

1 2 3 4

slide-46
SLIDE 46

46

17-214

Future renderer (1)

public abstract class FutureRenderer { private final ExecutorService executor = ...; void renderPage(CharSequence source) { final List<ImageInfo> imageInfos = scanForImageInfo(source); Callable<List<ImageData>> task = new Callable<List<ImageData>>() { public List<ImageData> call() {

List<ImageData> result = new ArrayList<ImageData>();

for (ImageInfo imageInfo : imageInfos) result.add(imageInfo.downloadImage()); return result; } }; Future<List<ImageData>> future = executor.submit(task); renderText(source); // Continued below

1 2 3

slide-47
SLIDE 47

47

17-214

Future renderer (2)

public abstract class FutureRenderer { ... try { List<ImageData> imageData = future.get(); for (ImageData data : imageData) renderImage(data); } catch (InterruptedException e) { // Re-assert the thread's interrupted status Thread.currentThread().interrupt(); // We don't need the result, so cancel the task too future.cancel(true); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } }

4

slide-48
SLIDE 48

48

17-214

Future renderer analysis

  • Allows text to be rendered concurrently with downloading data
  • When all images are downloaded, they are rendered onto the

page

  • Can we do better?
slide-49
SLIDE 49

49

17-214

Limitations of parallelizing heterogeneous tasks

  • We tried to execute two different types of tasks in parallel—

downloading images, rendering page

  • Does not scale well

– How can we use more than two threads? – Tasks may have disparate sizes

  • If rendering text is much faster than downloading images,

performance is not much different from sequential version

  • Lesson: real performance payoff of dividing a program’s

workload into tasks comes when there are many independent, homogeneous tasks that can be processed concurrently

slide-50
SLIDE 50

50

17-214

  • CompletionService combines the functionality of an

Executor and a BlockingQueue

– submit Callable tasks to CompletionService – use queue-like methods take and poll to retrieve completed results, packaged as Futures, as they become available

Example: Page renderer with CompletionService

slide-51
SLIDE 51

51

17-214

Page renderer with CompletionService

Download images in parallel (1)

public abstract class Renderer { private final ExecutorService executor; ... void renderPage(CharSequence source) { final List<ImageInfo> info = scanForImageInfo(source); CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor); for (final ImageInfo imageInfo : info)

completionService completionService.submit .submit(new Callable<ImageData>() {

public ImageData call() { return imageInfo.downloadImage(); } }); renderText(source); // Continued below

slide-52
SLIDE 52

52

17-214

public abstract class Renderer { ... try { for (int t = 0, n = info.size(); t < n; t++) { Future<ImageData> f = completionService

completionService.take .take(); ();

ImageData imageData = f.get(); renderImage(imageData); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } }

Page renderer with CompletionService

Download images in parallel (2)

slide-53
SLIDE 53

53

17-214

Summary

  • Structuring applications around the execution of tasks can

simplify development and facilitate concurrency

  • The Executor framework permits you to decouple task

submission from execution policy

  • To maximize benefit of decomposing an application into tasks,

identify sensible task boundaries

– Not always obvious

slide-54
SLIDE 54

54

17-214

Recommended Readings

  • Goetz et al. Java Concurrency In Practice. Pearson Education,

2006, Chapters 5 (Building blocks) and 6 (Task executions)

  • Lea, Douglas. Concurrent programming in Java: design principles

and patterns. Addison-Wesley Professional, 2000, Chapter 4.4 (Parallel decoposition)

slide-55
SLIDE 55

55

17-214

REUSE RATHER THAN BUILD: KNOW THE LIBRARIES

slide-56
SLIDE 56

56

17-214

Synchronized Collections

  • Are thread safe:

– Vector – Hashtable – Collections.synchronizedXXX

  • But still require client-side locking to guard compound actions:

– Iteration: repeatedly fetch elements until collection is exhausted – Navigation: find next element after this one according to some order – Conditional ops (put-if-absent)

slide-57
SLIDE 57

57

17-214

Example

  • Both methods are thread safe
  • Unlucky interleaving that throws ArrayIndexOutOfBoundsException

public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); }

sizeà10 get(9) boom sizeà10 remove(9)

A B

slide-58
SLIDE 58

58

17-214

Solution: Compound actions on Vector using client-side locking

  • Synchronized collections guard methods with the lock on the

collection object itself

public static Object getLast(Vector list) { synchronized synchronized (list list) { ) { int lastIndex = list.size() - 1; return list.get(lastIndex); } } public static void deleteLast(Vector list) { synchronized synchronized (list list) { ) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }

slide-59
SLIDE 59

59

17-214

Another Example

  • The size of the list might change between a call to size

and a corresponding call to get

– Will throw ArrayIndexOutOfBoundsException

  • Note: Vector is still thread safe:

– State is valid – Exception conforms with specification

for (int i = 0; i < vector.size(); i++) doSomething(vector.get(i));

slide-60
SLIDE 60

60

17-214

Solution: Client-side locking

  • Hold the Vector lock for the duration of iteration:

– No other threads can modify (+) – No other threads can access (-) synchronized (vector) { for (int i = 0; i < vector.size(); i++) doSomething(vector.get(i)); }

slide-61
SLIDE 61

61

17-214

Iterators and

ConcurrentModificationException

  • Iterators returned by the synchronized collections are not

designed to deal with concurrent modification à fail-fast

  • Implementation:

– Each collection has a modification count – If it changes, hasNext or next throws ConcurrentModificationException

  • Prevent by locking the collection:

– Other threads that need to access the collection will block until iteration is complete à starvation – Risk factor for deadlock – Hurts scalability (remember lock contention in reading)

slide-62
SLIDE 62

62

17-214

Alternative to locking the collection during iteration?

slide-63
SLIDE 63

63

17-214

Yet Another Example: Is this safe?

public class HiddenIterator { @GuardedBy("this") private final Set<Integer> set = new HashSet<Integer>(); public synchronized void add(Integer i) { set.add(i); } public synchronized void remove(Integer i) { set.remove(i); } public void addTenThings() { Random r = new Random(); for (int i = 0; i < 10; i++) add(r.nextInt()); System.out.println("DEBUG: added ten elements to " + set); } }

slide-64
SLIDE 64

64

17-214

Hidden Iterator

public class HiddenIterator { @GuardedBy("this") private final Set<Integer> set = new HashSet<Integer>(); public synchronized void add(Integer i) { set.add(i); } public synchronized void remove(Integer i) { set.remove(i); } public void addTenThings() { Random r = new Random(); for (int i = 0; i < 10; i++) add(r.nextInt()); System. System.out

  • ut.println(

.println("DEBUG: added ten elements to " "DEBUG: added ten elements to " + + set set); ); } }

  • Locking can prevent ConcurrentModificationException
  • But must remember to lock everywhere a shared collection

might be iterated

slide-65
SLIDE 65

65

17-214

Hidden Iterator

  • String concatenation

à StringBuilder.append(Object) à Set.toString() à Iterates the collection; calls toString() on each element à addTenThings() may throw ConcurrentModificationException

  • Lesson: Just as encapsulating an object’s state makes

it easier to preserve its invariants, encapsulating its synchronization makes it easier to enforce its synchronization policy

System. System.out

  • ut.println(

.println("DEBUG: added ten elements to " "DEBUG: added ten elements to " + + set set); );

slide-66
SLIDE 66

66

17-214

Concurrent Collections

  • Synchronized collections: thread safety by serializing

all access to state

– Cost: poor concurrency

  • Concurrent collections are designed for concurrent

access from multiple threads

– Dramatic scalability improvements Unsynchronized Concurrent

HashMap ConcurrentHashMap HashSet ConcurrentHashSet TreeMap ConcurrentSkipListMap TreeSet ConcurrentSkipListSet

slide-67
SLIDE 67

67

17-214

ConcurrentHashMap

  • HashMap.get: traversing a hash bucket to find a specific
  • bject à calling equals on a number of candidate objects

– Can take a long time if hash function is poor and elements are unevenly distributed

  • ConcurrentHashMap uses lock striping (recall reading)

– Arbitrarily many reading threads can access concurrently – Readers can access map concurrently with writers – Limited number of writers can modify concurrently

  • Tradeoffs:

– size only an estimate – Can’t lock for exclusive access

slide-68
SLIDE 68

68

17-214

You can’t exclude concurrent activity from a concurrent collection

  • This works for synchronized collections…

Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); synchronized(syncMap) { if (!syncMap.containsKey("foo")) syncMap.put("foo", "bar"); }

  • But not for concurrent collections

– They do their own internal synchronization – Never synchronize on a concurrent collection!

slide-69
SLIDE 69

69

17-214

Concurrent collections have prepackaged read-modify-write methods

  • V putIfAbsent(K key, V value)
  • boolean remove,(Object key, Object value)
  • V replace(K key, V value)
  • boolean replace(K key, V oldValue, V newValue)
  • V compute(K key, BiFunction<...> remappingFn);
  • V computeIfAbsent(K key, Function<...> mappingFn)
  • V computeIfPresent(K key, BiFunction<...> remapFn)
  • V merge(K key, V value, BiFunction<...> remapFn)
slide-70
SLIDE 70

70

17-214

Summary

  • Design patterns for concurrency
  • The Executor framework
  • Concurrency libraries