Concurrent Programming in Harmony: Critical Sections and Locks CS - - PowerPoint PPT Presentation

concurrent programming in harmony critical sections and
SMART_READER_LITE
LIVE PREVIEW

Concurrent Programming in Harmony: Critical Sections and Locks CS - - PowerPoint PPT Presentation

Concurrent Programming in Harmony: Critical Sections and Locks CS 4410 Operating Systems [Robbert van Renesse] An Operating System is a Concurrent Program The kernel contexts of each of the processes share many data structures


slide-1
SLIDE 1

Concurrent Programming in Harmony: Critical Sections and Locks

CS 4410 Operating Systems

[Robbert van Renesse]

slide-2
SLIDE 2
  • The ”kernel contexts” of each of the

processes share many data structures

  • Further complicated by interrupt

handlers that also access those data structures

An Operating System is a Concurrent Program

2

slide-3
SLIDE 3
  • Not making this up…

So I talked with a recruiter last week…

3

slide-4
SLIDE 4
  • What is the problem?
  • no determinism, no atomicity
  • What is the solution?
  • some form of locks
  • How to implement locks?
  • there are multiple ways
  • How to reason about concurrent

programs?

  • How to construct correct concurrent

programs?

Synchronization Lectures Outline

4

slide-5
SLIDE 5

Why?

  • Concurrent programs are non-deterministic
  • run them twice with same input, get two different answers
  • or worse, one time it works and the second time it fails
  • Program statements are executed non-atomically
  • x += 1 compiles to something like
  • LOAD x
  • ADD 1
  • STORE x

Concurrent Programming is Hard

5

slide-6
SLIDE 6

2 threads updating a shared variable amount

  • One thread (you) wants to decrement amount by $10K
  • Other thread (IRS) wants to decrement amount by 50%

What happens when both threads are running?

Two Theads, One Variable

6

. . .

amount -= 10,000;

. . . . . .

amount /= 2;

. . .

Memory

100,000 amount

T1 T2

slide-7
SLIDE 7

Might execute like this:

Two Theads, One Variable

7

Memory

. . . r1 = load from amount r1 = r1 – 10,000 store r1 to amount . . . . . . r2 = load from amount r2 = r2 / 2 store r2 to amount . . .

40,000 amount

Or vice versa (T1 then T2 à 45,000)… either way is fine…

T1 T2

slide-8
SLIDE 8

Or it might execute like this:

Two Theads, One Variable

8

Memory

. . . r1 = load from amount r1 = r1 – 10,000 store r1 to amount . . . . . . r2 = load from amount . . . r2 = r2 / 2 store r2 to amount . . .

50,000 amount

Lost Update! Wrong ..and very difficult to debug

T1 T2

slide-9
SLIDE 9
  • 2 concurrent enqueue() operations?
  • 2 concurrent dequeue() operations?

What could possibly go wrong?

Example: Races with Shared Queue

9

tail head

slide-10
SLIDE 10

= timing dependent error involving shared state

  • Once thread A starts, it needs to “race” to finish
  • Whether race condition happens depends on

thread schedule

  • Different “schedules” or “interleavings” exist

(total order on machine instructions)

All possible interleavings should be safe!

Race Conditions

10

slide-11
SLIDE 11
  • Number of possible interleavings is huge
  • Some interleavings are good
  • Some interleavings are bad
  • But bad interleavings may rarely happen!
  • Works 100x ≠ no race condition
  • Timing dependent: small changes hide bugs

Race Conditions are Hard to Debug

11

slide-12
SLIDE 12

1. Students develop their concurrent code in Python or C 2. They test by running code many times 3. They submit their code, confident that it is correct 4. RVR tests the code with his secret and evil methods

  • uses homebrew library that randomly

samples from possible interleavings

5. Finds most submissions are broken 6. RVR unhappy, students unhappy

My experience until now

12

slide-13
SLIDE 13
  • Several studies show that heavily used

code implemented, reviewed, and tested by expert programmers have lots of concurrency bugs

  • Even professors who teach concurrency
  • r write books about concurrency get it

wrong sometimes

It’s not stupidity

13

slide-14
SLIDE 14
  • Handwritten correctness proofs just as likely to

have bugs as programs

  • or even more likely as you can’t test handwritten

proofs

  • Lack of mainstream tools to check concurrent

algorithms

  • Tools that do exist have a steep learning curve

My take on the problem

14

slide-15
SLIDE 15
  • A new concurrent programming language
  • heavily based on Python syntax to reduce

learning curve for many

  • careful: important differences with Python
  • A new underlying virtual machine
  • very different from any other:

it tries all possible executions of a program until it finds a problem

(this is called “model checking”)

Enter Harmony

15

slide-16
SLIDE 16

Example (same as before)

16 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ;

slide-17
SLIDE 17

Example (same as before)

17 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main();

slide-18
SLIDE 18

Example (same as before)

18 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main();

Equivalent to: while not (done1 and done2): pass; ;

slide-19
SLIDE 19

Example (same as before)

19 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main();

Assertion: useful to check properties

slide-20
SLIDE 20

Example (same as before)

20 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main();

Output amount if assertion fails

slide-21
SLIDE 21

Example (same as before)

21 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main();

Initialize shared variables

slide-22
SLIDE 22

Example (same as before)

22 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main();

Spawn three processes (threads)

slide-23
SLIDE 23

Example (same as before)

23 def T1(): amount −= 10000; done1 = True; ; def T2(): amount /= 2; done2 = True; ; def main(): await done1 and done2; assert (amount == 40000) or (amount == 45000), amount; ; done1 = done2 = False; amount = 100000; spawn T1(); spawn T2(); spawn main(); #states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17]. 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

slide-24
SLIDE 24

Simplified model (ignoring main)

24

T1a: LOAD amount T1b: SUB 10000 T1c: STORE amount T2a: LOAD amount T2b: DIV 2 T2c: STORE amount

slide-25
SLIDE 25

Simplified model (ignoring main)

25

_init_

amount = 100000

init T1a: LOAD amount T1b: SUB 10000 T1c: STORE amount T2a: LOAD amount T2b: DIV 2 T2c: STORE amount

slide-26
SLIDE 26

Simplified model (ignoring main)

26

_init_

amount = 100000

init T1a: LOAD amount T1b: SUB 10000 T1c: STORE amount T2a: LOAD amount T2b: DIV 2 T2c: STORE amount

T1 loaded 100000 T2 loaded 100000

T1a T2a

slide-27
SLIDE 27

Simplified model (ignoring main)

27

_init_

amount = 100000

init T1a: LOAD amount T1b: SUB 10000 T1c: STORE amount T2a: LOAD amount T2b: DIV 2 T2c: STORE amount

T1 loaded 100000 T2 loaded 100000

T1a T2a

T1 loaded 100000 T2 loaded 100000

T2a T1a

slide-28
SLIDE 28

Simplified model (ignoring main)

28

_init_

amount = 100000

init T1a: LOAD amount T1b: SUB 10000 T1c: STORE amount T2a: LOAD amount T2b: DIV 2 T2c: STORE amount

T1 loaded 100000 T2 loaded 100000

T1a T2a

T1 got 90000 T1 loaded 100000 T2 loaded 100000 T2 got 50000

T2a T1a T1b T2b

T1 stored 90000

T1c T2a T2c T1a T1b T2b T2a

slide-29
SLIDE 29

Harmony Output

29

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17]. 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

slide-30
SLIDE 30

Output

30

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17]. 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

#states in the state graph

slide-31
SLIDE 31

Output

31

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17]. 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

diameter of the state graph

slide-32
SLIDE 32

Output

32

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

something went wrong in (at least) one path in the graph (assertion failure)

slide-33
SLIDE 33

Output

33

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

path shortest path to assertion failure

slide-34
SLIDE 34

Output

34

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

_init_

slide-35
SLIDE 35

Output

35

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

T1ab _init_

slide-36
SLIDE 36

Output

36

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

T2abc T1ab _init_

slide-37
SLIDE 37

Output

37

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

T1c T2abc T1ab _init_

slide-38
SLIDE 38

Output

38

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

main T1c T2abc T1ab _init_

slide-39
SLIDE 39

Output

39

#states = 100 diameter = 5 ==== Safety violation ==== __init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True } >>> Harmony Assertion (file=test.hny, line=11) failed: 90000

main T1c T2abc T1ab _init_

slide-40
SLIDE 40

Output

40

__init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17]. 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True }

“name tag” of a process

slide-41
SLIDE 41

Output

41

__init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True }

“microsteps” = list of program counters

  • f machine instructions

executed

slide-42
SLIDE 42

0 Jump 40 1 Frame T1 () 2 Load amount 3 Push 10000 4 2-ary − 5 Store amount 6 Push True 7 Store done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount 12 Push 2 13 2-ary / 14 Store amount 15 Push True 16 Store done2 17 Return 18 …

Harmony Machine Code

42

T1a: LOAD amount T1b: SUB 10000 T1c: STORE amount T1d: done1 = True T2a: LOAD amount T2b: DIV 2 T2c: STORE amount T2d: done2 = True

slide-43
SLIDE 43

0 Jump 40 PC := 40 1 Frame T1 () 2 Load amount 3 Push 10000 4 2-ary − 5 Store amount 6 Push True 7 Store done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount 12 Push 2 13 2-ary / 14 Store amount 15 Push True 16 Store done2 17 Return 18 …

Harmony Machine Code

43

slide-44
SLIDE 44

0 Jump 40 PC := 40 1 Frame T1 () 2 Load amount push amount onto the stack of process T1 3 Push 10000 4 2-ary − 5 Store amount 6 Push True 7 Store done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount 12 Push 2 13 2-ary / 14 Store amount 15 Push True 16 Store done2 17 Return 18 …

Harmony Machine Code

44

slide-45
SLIDE 45

0 Jump 40 PC := 40 1 Frame T1 () 2 Load amount push amount onto the stack of process T1 3 Push 10000 push 10000 onto the stack of process T1 4 2-ary − replace top two elements of stack with difference 5 Store amount 6 Push True 7 Store done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount 12 Push 2 13 2-ary / 14 Store amount 15 Push True 16 Store done2 17 Return 18 …

Harmony Machine Code

45

slide-46
SLIDE 46

0 Jump 40 PC := 40 1 Frame T1 () 2 Load amount push amount onto the stack of process T1 3 Push 10000 push 10000 onto the stack of process T1 4 2-ary − replace top two elements of stack with difference 5 Store amount store top of the stack of T1 into amount 6 Push True 7 Store done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount 12 Push 2 13 2-ary / 14 Store amount 15 Push True 16 Store done2 17 Return 18 …

Harmony Machine Code

46

slide-47
SLIDE 47

0 Jump 40 PC := 40 1 Frame T1 () 2 Load amount push amount onto the stack of process T1 3 Push 10000 push 10000 onto the stack of process T1 4 2-ary − replace top two elements of stack with difference 5 Store amount store top of the stack of T1 into amount 6 Push True push True onto the stack of process T1 7 Store done1 store top of the stack of T1 into done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount 12 Push 2 13 2-ary / 14 Store amount 15 Push True 16 Store done2 17 Return 18 …

Harmony Machine Code

47

slide-48
SLIDE 48

0 Jump 40 PC := 40 1 Frame T1 () 2 Load amount push amount onto the stack of process T1 3 Push 10000 push 10000 onto the stack of process T1 4 2-ary − replace top two elements of stack with difference 5 Store amount store top of the stack of T1 into amount 6 Push True push True onto the stack of process T1 7 Store done1 store top of the stack of T1 into done1 8 Return 9 Jump 40 10 Frame T2 () 11 Load amount push amount onto the stack of process T2 12 Push 2 push 2 onto the stack of process T2 13 2-ary / replace top two elements of stack with division 14 Store amount store top of the stack of T2 into amount 15 Push True push True onto the stack of process T2 16 Store done2 store top of the stack of T2 into done2 17 Return 18 …

Harmony Machine Code

48

slide-49
SLIDE 49

Output

49

__init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True }

current program counter (after microsteps)

slide-50
SLIDE 50

Output

50

__init__/() [0,40-58] 58 { amount: 100000, done1: False, done2: False } T1/() [1-4] 5 { amount: 100000, done1: False, done2: False } T2/() [10-17] 17 { amount: 50000, done1: False, done2: True } T1/() [5-8] 8 { amount: 90000, done1: True, done2: True } main/() [19-23,25-34,36-37] 37 { amount: 90000, done1: True, done2: True }

current state (after microsteps)

slide-51
SLIDE 51

Three parts:

  • 1. code (which never changes)
  • 2. values of the shared variables
  • 3. states of each of the running processes
  • “contexts”

State represents one vertex in the graph model

Harmony Virtual Machine State

51

slide-52
SLIDE 52
  • Name tag
  • PC (program counter)
  • stack (+ implicit stack pointer)
  • local variables
  • parameters (aka arguments)
  • “result”
  • there is no return statement
  • local variables
  • declared in let and for statements

Context (state of a process)

52

slide-53
SLIDE 53

Harmony != Python

53

Harmony Python

tries all possible executions executes just one every statement ends in ; ; at end of statement optional indentation recommended indentation required ( … ) == [ … ] == … 1 != [1] != (1) 1, == [1,] == (1,) != (1) == [1] == 1 [1,] == [1] != (1) == 1 != (1,) f(1) == f 1 == f[1] f 1 and f[1] are illegal { } is empty set set() != { } few operator precedence rules --- use brackets often many operator precedence rules variables global unless declared

  • therwise

depends... Sometimes must be explicitly declared global no return, break, continue various flow control escapes no classes

  • bject-oriented

… …

slide-54
SLIDE 54
  • Input:
  • choose expression
  • x = choose({ 1, 2, 3 })
  • allows Harmony to know all possible inputs
  • const expression
  • const x = 3
  • can be overridden with “-c x=4” flag to harmony
  • Output:
  • assert x + y < 10
  • assert x + y < 10, (x, y)

I/O in Harmony?

54

slide-55
SLIDE 55
  • Input:
  • choose expression
  • x = choose({ 1, 2, 3 })
  • allows Harmony to know all possible inputs
  • const expression
  • const x = 3
  • can be overridden with “-c x=4” flag to Harmony
  • Output:
  • assert x + y < 10
  • assert x + y < 10, (x, y)

I/O in Harmony?

55

No open(), read(), input(),

  • r print() statements
slide-56
SLIDE 56

Two sources:

  • 1. choose expressions
  • 2. process interleavings

Non-determinism in Harmony

56

slide-57
SLIDE 57

Limitation: models must be finite!

57

_init_

amount = 100000

init

T1 loaded 100000 T2 loaded 100000

T1a T2a

T1 got 90000 T1 loaded 100000 T2 loaded 100000 T2 got 50000

T2a T1a T1b T2b

T1 stored 90000

T1c T2a T2c T1a T1b T2b T2a

  • But models are allowed to have cycles.
  • Executions are allowed to be unbounded!
  • Harmony does check for possibility of termination
slide-58
SLIDE 58

2 threads updating a shared variable amount

  • One thread wants to decrement amount by $10K
  • Other thread wants to decrement amount by 50%

How to “serialize” these executions?

Back to our problem…

58

. . .

amount -= 10,000;

. . . . . .

amount /= 2;

. . .

Memory

100,000 amount

T1 T2

slide-59
SLIDE 59

Must be serialized due to shared memory access

Goals

Mutual Exclusion: 1 thread in a critical section at time Progress: all threads make it into the CS if desired Fairness: equal chances of getting into CS … in practice, fairness rarely guaranteed

Critical Section

59

. . . CSEnter(); amount -= 10000; CSExit(); . . . . . . CSEnter(); amount /= 2; CSExit(); . . . T1 T2

slide-60
SLIDE 60

Must be serialized due to shared memory access

Goals

Mutual Exclusion: 1 thread in a critical section at time Progress: all threads make it into the CS if desired Fairness: equal chances of getting into CS … in practice, fairness rarely guaranteed

Critical Section

60

. . . CSEnter(); Critical section CSExit(); . . . . . . CSEnter(); Critical section CSExit(); . . . T1 T2

slide-61
SLIDE 61

Critical Sections in Harmony

61

def process(self): while True: … # code outside critical section … # code to enter the critical section … # critical section itself … # code to exit the critical section ; ; spawn process(1); spawn process(2); …

  • How do we check mutual exclusion?
  • How do we check termination?
slide-62
SLIDE 62

Critical Sections in Harmony

62

def process(self): while True: … # code outside critical section … # code to enter the critical section @cs: assert atLabel.cs == dict{ nametag(): 1 }; … # code to exit the critical section ; ; spawn process(1); spawn process(2); …

  • How do we check mutual exclusion?
  • How do we check progress?
slide-63
SLIDE 63

Critical Sections in Harmony

63

def process(self): while choose( { False, True } ): … # code outside critical section … # code to enter the critical section @cs: assert atLabel.cs == dict{ nametag(): 1 }; … # code to exit the critical section ; ; spawn process(1); spawn process(2); …

  • How do we check mutual exclusion?
  • How do we check progress?
  • if code to enter/exit the critical section

does not terminate, Harmony with balk

slide-64
SLIDE 64
  • True, but this is an O.S. class!
  • The question is:

How does one build a lock?

  • Harmony is a concurrent

programming language. Really, doesn’t Harmony have locks?

You have to program them!

Sounds like you need a lock…

64

slide-65
SLIDE 65

First attempt: a naïve lock

65

slide-66
SLIDE 66

First attempt: a naïve lock

66 wait till lock is free, then take it

slide-67
SLIDE 67

First attempt: a naïve lock

67 wait till lock is free, then take it release the lock

slide-68
SLIDE 68

First attempt: a naïve lock

68

==== Safety violation ==== __init__/() [0,26-36] 36 { lockTaken: False } process/0 [1-2,3(choose True),4-7] 8 { lockTaken: False } process/1 [1-2,3(choose True),4-8] 9 { lockTaken: True } process/0 [8-19] 19 { lockTaken: True } >>> Harmony Assertion (file=code/naiveLock.hny, line=8) failed

slide-69
SLIDE 69

Second attempt: flags

69

slide-70
SLIDE 70

Second attempt: flags

70 enter, then wait for other

slide-71
SLIDE 71

Second attempt: flags

71 enter, then wait for other leave

slide-72
SLIDE 72

Second attempt: flags

72

==== Non-terminating State === __init__/() [0,36-46] 46 { flags: [False, False] } process/0 [1-2,3(choose True),4-12] 13 { flags: [True, False] } process/1 [1-2,3(choose True),4-12] 13 { flags: [True, True] } blocked process: process/1 pc = 13 blocked process: process/0 pc = 13

slide-73
SLIDE 73

Third attempt: turn variable

73

slide-74
SLIDE 74

Third attempt: turn variable

74 wait for your turn

slide-75
SLIDE 75

Third attempt: turn variable

75 wait for your turn let the other process take a turn

slide-76
SLIDE 76

Third attempt: turn variable

76

==== Non-terminating State === __init__/() [0,28-38] 38 { turn: 0 } process/0 [1-2,3(choose True),4-26,2,3(choose True),4] 5 { turn: 1 } process/1 [1-2,3(choose False),4,27] 27 { turn: 1 } blocked process: process/0 pc = 5

slide-77
SLIDE 77

Peterson’s Algorithm: flags & turn

77

slide-78
SLIDE 78

Peterson’s Algorithm: flags & turn

78 “you go first”

slide-79
SLIDE 79

Peterson’s Algorithm: flags & turn

79 “you go first” wait until alone or it’s my turn

slide-80
SLIDE 80

Peterson’s Algorithm: flags & turn

80 “you go first” wait until alone or it’s my turn leave

slide-81
SLIDE 81

Peterson’s Algorithm: flags & turn

81

#states = 104 diameter = 5 #components: 37 no issues found

slide-82
SLIDE 82

So, we proved Peterson’s Algorithm correct by brute force, enumerating all possible executions. But how does one prove it by deduction? so one might understand why it works…

82

slide-83
SLIDE 83
  • Need to show that, for any execution, all

states reached satisfy mutual exclusion

  • in other words, mutual exclusion is invariant
  • Sounds similar to sorting:
  • Need to show that, for any list of numbers, the

resulting list is ordered

  • Let’s try proof by induction on the length of

an execution

What and how?

83

slide-84
SLIDE 84

You want to prove that some Induction Hypothesis IH(n) holds for any n:

  • Base Case:
  • show that IH(0) holds
  • Induction Step:
  • show that if IH(i) holds, then so does IH(i+1)

Proof by induction

84

slide-85
SLIDE 85

To show that some IH holds for an execution E of any number of steps:

  • Base Case:
  • show that IH holds in the initial state(s)
  • Induction Step:
  • show that if IH holds in a state produced by E,

then for any possible next step s, IH also holds in the state produced by E + [s]

Proof by induction in our case

85

slide-86
SLIDE 86
  • Obvious answer: mutual exclusion itself
  • if 𝑄0 is in the critical section, then 𝑄1 is not
  • without loss of generality…
  • Formally: 𝑄0@𝑑𝑡 ⟹ ¬𝑄1@𝑑𝑡
  • Unfortunately, this won’t work…

First question: what should IH be?

86

slide-87
SLIDE 87

State before P1 takes a step:

87 P0 P1

flags == [ True, True ] turn == 1

slide-88
SLIDE 88

State after P1 takes a step:

88 P0 P1

flags == [ True, True ] turn == 1

slide-89
SLIDE 89

So, is Peterson’s Algorithm broken?

89

slide-90
SLIDE 90

No, it’ll turn out this prior state cannot be reached from the initial state

90 P0 P1

flags == [ True, True ] turn == 1

slide-91
SLIDE 91
  • Based on the aw

await condition:

𝑄0@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0

  • Promising because if 𝑄0@𝑑𝑡 ∧ 𝑄1@𝑑𝑡 then

! 𝑄0@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 ∧ 𝑄1@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 0 ∨ 𝑢𝑣𝑠𝑜 == 1 ⇒ 6𝑢𝑣𝑠𝑜 == 0 ∧ 𝑢𝑣𝑠𝑜 == 1

⟹ False (therefore mutual exclusion)

  • Unfortunately, this is not an invariant…

Let’s try another obvious one

91

slide-92
SLIDE 92

State before P1 takes a step:

92 P0 P1

flags == [ True, False ] turn == 1

𝑄0@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 holds

note: this is a reachable state

slide-93
SLIDE 93

State after P1 takes a step:

93 P0 P1

flags == [ True, True ] turn == 1

𝑄0@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 𝑤𝑗𝑝𝑚𝑏𝑢𝑓𝑒

note: this is also a reachable state

slide-94
SLIDE 94

But suggests an improved hypothesis

94

𝑄0@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 ∨ 𝑄1@𝑕𝑏𝑢𝑓

P0 P1

slide-95
SLIDE 95

To prove: 𝑄0@𝑑𝑡 ⟹ ¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 ∨ 𝑄1@𝑕𝑏𝑢𝑓 By induction: Base case:

  • In initial state ¬𝑄0@𝑑𝑡
  • false implies anything

Induction Step: assume 𝑄0@𝑑𝑡 and 𝑄1 takes a step when Case 1: ¬𝑔𝑚𝑏𝑕𝑡 1 then after step either ¬𝑔𝑚𝑏𝑕𝑡 1 or 𝑄1@𝑕𝑏𝑢𝑓 Case 2: 𝑢𝑣𝑠𝑜 == 0 then after step still 𝑢𝑣𝑠𝑜 == 0 (𝑄1 never sets turn to 1) Case 3: 𝑄1@𝑕𝑏𝑢𝑓 then after step 𝑢𝑣𝑠𝑜 == 0

Invariance proof

95

slide-96
SLIDE 96

𝑄0@𝑑𝑡 ∧ 𝑄1@𝑑𝑡 ⟹ )¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 ∨ 𝑄1@𝑕𝑏𝑢𝑓 ¬𝑔𝑚𝑏𝑕𝑡 0 ∨ 𝑢𝑣𝑠𝑜 == 1 ∨ 𝑄0@𝑕𝑏𝑢𝑓 ∧ ⟹ 𝑢𝑣𝑠𝑜 == 0 ∧ turn == 1 ⟹ 𝐺𝑏𝑚𝑡𝑓

Finally, prove mutual exclusion

96

slide-97
SLIDE 97

𝑄0@𝑑𝑡 ∧ 𝑄1@𝑑𝑡 ⟹ )¬𝑔𝑚𝑏𝑕𝑡 1 ∨ 𝑢𝑣𝑠𝑜 == 0 ∨ 𝑄1@𝑕𝑏𝑢𝑓 ¬𝑔𝑚𝑏𝑕𝑡 0 ∨ 𝑢𝑣𝑠𝑜 == 1 ∨ 𝑄0@𝑕𝑏𝑢𝑓 ∧ ⟹ 𝑢𝑣𝑠𝑜 == 0 ∧ turn == 1 ⟹ 𝐺𝑏𝑚𝑡𝑓

Finally, prove mutual exclusion

97

QED

slide-98
SLIDE 98

Review in Pictures: State Space

98

Mutual Exclusion Holds Mutual Exclusion Violated

slide-99
SLIDE 99

Review in Pictures: State Space

99

Reachable States Mutual Exclusion Holds Mutual Exclusion Violated

slide-100
SLIDE 100

Review in Pictures: State Space

100

Initial States Reachable States Mutual Exclusion Holds Mutual Exclusion Violated Final States

slide-101
SLIDE 101

Review in Pictures: State Space

101

Initial States Reachable States Mutual Exclusion Holds Mutual Exclusion Violated Final States Inductive Invariant Holds

slide-102
SLIDE 102

II is an inductive invariant if for any state S (including unreachable ones!):

  • Base case: II holds if S is an initial state
  • Induction step: if II holds in S, then II also holds

in any states reachable from S in one step

Note, an ordinary invariant only needs to hold in all reachable states

II is useful if it implies an invariant that we are interested in (mutual exclusion in this case)

Inductive Invariant

102

slide-103
SLIDE 103
  • Mutual Exclusion can be implemented

with LOAD and STORE instructions to access shared memory

  • 3 STOREs and 1 or more LOADs
  • Peterson’s can be generalized to >2

processes

  • even more STOREs and LOADs
  • Too inefficient in practice

Peterson’s Reconsidered

103

slide-104
SLIDE 104
  • Machine instructions that do multiple

shared memory accesses atomically

  • E.g., TestAndSet s, p
  • sets p to the (old) value of s
  • sets s to True
  • i.e., LOAD s, STORE p, STORE s
  • Entire operation is atomic
  • other machine instructions cannot interleave

Enter Interlock Instructions

104

slide-105
SLIDE 105
  • Machine instructions that do multiple

shared memory accesses atomically

  • E.g., TestAndSet s, p
  • sets p to the (old) value of s
  • sets s to True
  • i.e., LOAD s, STORE p, STORE s
  • Entire operation is atomic
  • other machine instructions cannot interleave

Enter Interlock Instructions

105

slide-106
SLIDE 106
  • If x is a shared variable, ?x is the address of x
  • If p is a shared variable and p == ?x, then we say

that p is a pointer to x

  • Finally, !p refers to the value of x

Harmony interlude: pointers

106

s and p are pointers, thus tas(s, p) can be used with any two shared variables: tas(?x, ?y) or tas(?q, ?r)

slide-107
SLIDE 107

Critical Sections with TAS

107

“spinlock”

private[ i ] belongs to process( i ) process(self)@𝑑𝑡 ⟹ ¬𝑞𝑠𝑗𝑤𝑏𝑢𝑓 𝑡𝑓𝑚𝑔 number of processes

slide-108
SLIDE 108

1. ∀𝑗: 𝑞𝑠𝑝𝑑𝑓𝑡𝑡 𝑗 @𝑑𝑡 ⇒ ¬𝑞𝑠𝑗𝑤𝑏𝑢𝑓 𝑗 2. at most 1 of 𝑡ℎ𝑏𝑠𝑓𝑒 and 𝑞𝑠𝑗𝑤𝑏𝑢𝑓[𝑗] is False

  • 1. Obvious
  • 2. Easy proof by induction

both can also be checked by Harmony (see book)

If at most one 𝑞𝑠𝑗𝑤𝑏𝑢𝑓 𝑗 can be False, then at most one 𝑞𝑠𝑝𝑑𝑓𝑡𝑡(𝑗) can be @𝑑𝑡

Two essential invariants

108

slide-109
SLIDE 109

Checking the second invariant

109

Check that at most one of shared and private[i] is False

check it here, atomically

assert statements are evaluated atomically

slide-110
SLIDE 110

Checking the second invariant

110

Riddle: this code checks the invariant

  • nly once, and yet it checks the

invariant at every state. How can that be?

slide-111
SLIDE 111

Best understood as “baton passing”

  • At most one process, or 𝑡ℎ𝑏𝑠𝑓𝑒, can “hold” False

“Locks”

111

slide-112
SLIDE 112

Locks in the “synch” module

112

Observation: 𝑞𝑠𝑗𝑤𝑏𝑢𝑓[𝑗] does not need to be a shared variable. Just return the

  • ld value
slide-113
SLIDE 113
  • No longer have private[i]
  • Instead:
  • We say that a lock is held or owned by a

process

  • The invariants become:
  • 1. 𝑄@𝑑𝑡 ⇒ 𝑄 holds the lock
  • 2. at most one process can hold the lock

“Ghost” state

113

slide-114
SLIDE 114

Using locks from the sync module

114

import the sync module

slide-115
SLIDE 115

Using locks from the sync module

115

initialize lock import the sync module

slide-116
SLIDE 116

Using locks from the sync module

116

enter critical section initialize lock import the sync module

slide-117
SLIDE 117

Using locks from the sync module

117

enter critical section initialize lock import the sync module

?countlock is the address of countlock process self holds countlock

slide-118
SLIDE 118

Using locks from the sync module

118

enter critical section initialize lock exit critical section import the sync module

slide-119
SLIDE 119
  • Spinlocks work well when processes on

different cores need to synchronize

  • But how about when it involves two

processes on the same core:

  • when there is no pre-emption?
  • when there is pre-emption?

Spinlocks and Time Sharing

119

slide-120
SLIDE 120
  • Harmony allows contexts to be saved

and restored

  • r = stop list
  • stops the current process and places its context

at the end of the given list

  • go context r
  • adds a process with the given context to the bag
  • f processes. Process resumes from stop

expression, returning r

Context switching in Harmony

120

slide-121
SLIDE 121

Locks using stop and go

121

slide-122
SLIDE 122

Locks using stop and go

122

lkàlocked is short for (!lk).locked (cf. C, C++)

slide-123
SLIDE 123

Locks using stop and go

123

Similar to a Linux “futex”: if there is no contention (hopefully the common case) lock() and unlock() are cheap. If there is contention, they involve a context switch.

slide-124
SLIDE 124
  • “synch” is the (default) module that has

the TAS version of lock

  • “synchS” is the module that has the

stop/go version of lock

  • you can select which one you want:

harmony -m synch=synchS x.hny

  • “sync” tends to be faster than “syncS”
  • smaller state graph

Choosing modules in Harmony

124

slide-125
SLIDE 125

Atomic section ≠ Critical Section

125

Atomic Section Critical Section

  • nly one process can

execute multiple process can execute concurrently, just not within a critical section rare programming paradigm ubiquitous: locks available in many mainstream programming languages good for implementing interlock instructions good for building concurrent data structures

slide-126
SLIDE 126
  • q = Qnew(): allocate a new queue
  • Qenqueue(q, v): add v to the tail of queue q
  • r = Qdequeue(q): returns r = () if q is empty or

r = (v,) (a singleton tuple) if v was at the head

  • f the queue

Building a Concurrent Queue

126

slide-127
SLIDE 127

Queue Test Program Example

127

enqueue v onto q dequeue until queue q is empty

slide-128
SLIDE 128

Queue implementation, v1

128

.head .tail .lock .value .next .value .next .value .next

None

slide-129
SLIDE 129

Queue implementation, v1

129

.head .tail .lock .value .next .value .next .value .next

None

dynamic memory allocation

slide-130
SLIDE 130

Queue implementation, v1

130

.head .tail .lock .value .next .value .next .value .next

None

slide-131
SLIDE 131

Queue implementation, v1

131

.head .tail .lock .value .next .value .next .value .next

None

malloc’d memory must be explicitly released (cf. C)

slide-132
SLIDE 132
  • Answer: all important
  • any resource that needs scheduling
  • CPU run queue
  • disk, network, printer waiting queue
  • lock waiting queue
  • inter-process communication
  • Posix pipes:
  • cat file | tr a-z A-Z | grep RVR
  • actor-based concurrency

How important are concurrent queues?

132

slide-133
SLIDE 133

Better concurrent queue: 2 locks

133

.head .tail .hdlock .tllock .value .next .value .next .value .next

None

dummy

slide-134
SLIDE 134

Better concurrent queue: 2 locks

134

.head .tail .hdlock .tllock .value .next .value .next .value .next

None

dummy

No contention for concurrent enqueue and dequeue operations! è more concurrency è faster

slide-135
SLIDE 135

Idea: allow multiple read-only operations to execute concurrently

  • In many cases, reads are much more

frequent than writes

èreader/writer lock Either:

  • multiple readers, or
  • a single writer

How to get more concurrency?

135

thus not:

  • a reader and a writer, nor
  • multiple writers
slide-136
SLIDE 136

2 0 0 1 0 0 0 0 0 2 1 0 1 1 0 2 2 0 2 0 1 1 0 1 (1) process not in c.s. terminates (2) process enters r.c.s. (3) process leaves r.c.s. (4) process enters w.c.s. (5) process leaves w.c.s. (1) (1) (1) (1) (2) (2) (2) (3) (3) (3) (5) (5) (4) (4)

Reader/writer lock state diagram

slide-137
SLIDE 137
  • acquire_rlock()
  • get a read lock. Multiple processes can have the

read lock simultaneously, but no process can have a write lock simultaneously

  • release_rlock()
  • release a read lock. Other processes may still have

the read lock. When the last read lock is released, a write lock may be acquired

  • acquire_wlock()
  • acquire the write lock. Only one process can have a

write lock, and if so no process can have a read lock

  • release_wlock()
  • release the write lock. Allows other processes to

either get a read or write lock

Reader/writer lock interface:

137

slide-138
SLIDE 138
  • Uses a single ordinary lock and two

integers to count #readers and #writers

R/W lock, Implementation #1

138

Invariants:

  • if 𝑜 readers in the critical section, then 𝑜𝑠𝑓𝑏𝑒𝑓𝑠𝑡 ≥ 𝑜
  • if 𝑜 writers in the critical section, then 𝑜𝑥𝑠𝑗𝑢𝑓𝑠𝑡 ≥ 𝑜
  • 𝑜𝑠𝑓𝑏𝑒𝑓𝑠𝑡 ≥ 0 ∧ 𝑜𝑥𝑠𝑗𝑢𝑓𝑠𝑡 = 0 ∨ (𝑜𝑠𝑓𝑏𝑒𝑓𝑠𝑡 = 0 ∧ 0 ≤ 𝑜𝑥𝑠𝑗𝑢𝑓𝑠𝑡 ≤ 1)

rwlock protects the nreaders/nwriters variables, not the critical section!

  • :
slide-139
SLIDE 139

R/W lock, Implementation #1

139

slide-140
SLIDE 140

R/W lock, Implementation #1

140

“busy wait” (i.e., spin) until no writer in the critical section

slide-141
SLIDE 141

R/W lock, Implementation #1

141

“busy wait” until no other process in the critical section

slide-142
SLIDE 142

R/W Locks: test for mutual exclusion

142

no writer

1 writer and no readers

slide-143
SLIDE 143
  • ok for multi-core (true) parallelism
  • bad for time-sharing (virtual) parallelism

About busy waiting

143

slide-144
SLIDE 144
  • Uses two ordinary locks and an integer

that counts the number of readers

R/W Lock, implementation #2

144

Invariants:

  • if 𝑜 readers in the critical section, then 𝑜𝑠𝑓𝑏𝑒𝑓𝑠𝑡 ≥ 𝑜
  • if a writer in the critical section, then 𝑜𝑠𝑓𝑏𝑒𝑓𝑠𝑡 = 0
  • if writer W in the critical section, then W holds rwlock

(if some reader in the critical section, the readers collectively hold rwlock)

rlock protects the nreaders variable

slide-145
SLIDE 145

R/W Lock, implementation #2

145

rlock protects the nreaders variable

first reader acquires rwlock last reader releases rwlock writer acquires rwlock writer releases rwlock

slide-146
SLIDE 146

R/W Lock, implementation #2

146

R1 W R2

Writer W is in the critical section and holds rwlock Reader R1 holds rlock and is waiting for rwlock nreaders == 0 Reader R2 is waiting for rlock Writer W left the critical section Reader R1 holds rwlock and released rlock nreaders == 1 Reader R2 released rlock nreaders == 2 Reader R1 leaves but rwlock is still “held” nreaders == 1 nreaders == 0 Reader R2 released rwlock

slide-147
SLIDE 147

R/W Lock, implementation #2

147

no busy waiting!

both readers and writers “block” when they can’t enter the critical section

slide-148
SLIDE 148
  • Prior test only checks mutual exclusion
  • How do you test if the implementation

allows multiple readers?

  • How do you test if the implementation

uses busy waiting or not? For both, see book

More testing of reader/writer lock implementations

148