Slide 1: CS110 Lecture 08: Concurrency and Race Conditions

Principles of Computer Systems

Winter 2020

Stanford University

Computer Science Department

Instructors: Chris Gregg and

                            Nick Troccoli

https://comic.browserling.com/53

Slide 2: CS110 Topic 2: How can our programs create and interact with other programs?

Slide 3: Learning About Processes

Creating processes and running other programs

Inter-process communication

Signals

Concurrency and Race Conditions

1/15

1/22

1/27

Today

Slide 4: Today's Learning Goals

Slide 5: Plan For Today

Slide 6: Plan For Today

Slide 7: Signals

A signal is a way to notify a process that an event has occurred

Slide 8: Sending Signals

The operating system sends many signals, but we can also send signals manually.

 

 

 

int kill(pid_t pid, int signum);

// same as kill(getpid(), signum)
int raise(int signum);

Slide 9:
waitpid()

Waitpid can be used to wait on children to terminate or change state:

 

The default behavior is to wait for the specified child process to exit.  options lets us customize this further (can combine these flags using | ):

Slide 10: Signal Handlers

We can have a function of our choice execute when a certain signal is received.

 

 

 

typedef void (*sighandler_t)(int);
...
sighandler_t signal(int signum, sighandler_t handler);

Slide 11: Signal Handlers

A signal can be received at any time, and a signal handler can execute at any time.

Slide 12: Signal Handlers

Key Idea: a signal handler is called if one or more signals of a type are sent.

Solution: signal handler should clean up as many children as possible, using WNOHANG, which means don't block.  If there are children we would have waited on but aren't, returns 0.  -1 typically means no children left.

static void reapChild(int sig) {
  while (true) {
    pid_t pid = waitpid(-1, NULL, WNOHANG);
    if (pid <= 0) break;
    numDone++;
  } 
}

Slide 13: Do Not Disturb

The sigprocmask function lets us temporarily block signals of the specified types.  Instead, they will be delivered when the block is removed.

 

 

Slide 14: Do Not Disturb

sigset_t is a special type (usually a 32-bit int) used as a bit vector.  It must be created and initialized using special functions (we generally ignore the return values).

 

 

 

 

 

 

// Initialize to the empty set of signals
int sigemptyset(sigset_t *set);

// Set to contain all signals
int sigfillset(sigset_t *set);

// Add the specified signal
int sigaddset(sigset_t *set, int signum);

// Remove the specified signal
int sigdelset(sigset_t *set, int signum);
static void imposeSIGCHLDBlock() { 
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD); 
    sigprocmask(SIG_BLOCK, &set, NULL);
}

Slide 15: Plan For Today

Slide 16: Concurrency

Concurrency means performing multiple actions at the same time.

Slide 17: Off To The Races

// job-list-broken.c
static void reapProcesses(int sig) {
  while (true) {
    pid_t pid = waitpid(-1, NULL, WNOHANG);
    if (pid <= 0) break;
    printf("Job %d removed from job list.\n", pid);
  }
}

char * const kArguments[] = {"date", NULL};
int main(int argc, char *argv[]) {
  signal(SIGCHLD, reapProcesses);
  for (size_t i = 0; i < 3; i++) {
    pid_t pid = fork();
    if (pid == 0) execvp(kArguments[0], kArguments);
    sleep(1); // force parent off CPU
    printf("Job %d added to job list.\n", pid);
  }
  return 0;
}
myth60$ ./job-list-broken
Sun Jan 27 03:57:30 PDT 2019
Job 27981 removed from job list.
Job 27981 added to job list.
Sun Jan 27 03:57:31 PDT 2019
Job 27982 removed from job list.
Job 27982 added to job list.
Sun Jan 27 03:57:32 PDT 2019
Job 27985 removed from job list.
Job 27985 added to job list.
myth60$ ./job-list-broken
Sun Jan 27 03:59:33 PDT 2019
Job 28380 removed from job list.
Job 28380 added to job list.
Sun Jan 27 03:59:34 PDT 2019
Job 28381 removed from job list.
Job 28381 added to job list.
Sun Jan 27 03:59:35 PDT 2019
Job 28382 removed from job list.
Job 28382 added to job list.
myth60$

Symptom: it looks like jobs are being removed from the list before being added!  How is this possible?

Slide 18: Off To The Races

// job-list-broken.c
static void reapProcesses(int sig) {
  while (true) {
    pid_t pid = waitpid(-1, NULL, WNOHANG);
    if (pid <= 0) break;
    printf("Job %d removed from job list.\n", pid);
  }
}

char * const kArguments[] = {"date", NULL};
int main(int argc, char *argv[]) {
  signal(SIGCHLD, reapProcesses);
  for (size_t i = 0; i < 3; i++) {
    pid_t pid = fork();
    if (pid == 0) execvp(kArguments[0], kArguments);
    sleep(1); // force parent off CPU
    printf("Job %d added to job list.\n", pid);
  }
  return 0;
}

Issue: the signal handler is being called before the parent adds to the job list.

Solution: block SIGCHLD from lines 14-17 to force the parent to always add to the job list first.

 

This is called a critical section - a piece of code that is indivisible.  It cannot be interrupted midway by our other code.

block

unblock

Slide 19: Off To The Races

// job-list-fixed.c
char * const kArguments[] = {"date", NULL};
int main(int argc, char *argv[]) {
  signal(SIGCHLD, reapProcesses);
  
  // Create set with just SIGCHLD
  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, SIGCHLD);
  
  for (size_t i = 0; i < 3; i++) {
    sigprocmask(SIG_BLOCK, &set, NULL);
    pid_t pid = fork();
    if (pid == 0) {
      sigprocmask(SIG_UNBLOCK, &set, NULL);
      execvp(kArguments[0], kArguments);
    }
    sleep(1); // force parent off CPU
    printf("Job %d added to job list.\n", pid);
    sigprocmask(SIG_UNBLOCK, &set, NULL);
  }
  return 0;
}

Slide 20: Race Conditions and Concurrency

Race conditions are a fundamental problem in concurrent code.

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

Slide 21: Plan For Today

static void executeCommand(char *command, bool inBackground) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        exit(1);
    }

    // If we are the parent, either wait or return immediately
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitpid(pidOrZero, NULL, 0);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
    }
}

int main(int argc, char *argv[]) {
    signal(SIGCHLD, reapProcesses);
    ...
}

Slide 22: Revisiting Our Shell

Last time, we added a handler that cleans up terminated children, to clean up background commands.

 

Issue: this handler will be called to clean up all children, even foreground commands.  Why?Signal handlers are first when waking up processes.

static void executeCommand(char *command, bool inBackground) {
    pid_t pidOrZero = fork();
    if (pidOrZero == 0) {
        char *arguments[] = {"/bin/sh", "-c", command, NULL};
        execvp(arguments[0], arguments);
        exit(1);
    }

    // If we are the parent, either wait or return immediately
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitpid(pidOrZero, NULL, 0);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
    }
}

int main(int argc, char *argv[]) {
    signal(SIGCHLD, reapProcesses);
    ...
}

Slide 23: Revisiting Our Shell

Issue: this handler will be called to clean up all children, even foreground commands.

 

Therefore, the waitpid on line 13 may block, but it always returns -1.  Can we get rid of it?

 

Goal: only call waitpid in signal handler.

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    while (foregroundPID == pid) {;}
}

static void executeCommand(char *command, bool inBackground) {
    // ...(omitted for brevity)...
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitForForegroundCommand(pidOrZero);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
        if (result == foregroundPID) foregroundPID = 0;
    }
}

Slide 24: Revisiting Our Shell

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    while (foregroundPID == pid) {;}
}

static void executeCommand(char *command, bool inBackground) {
    // ...(omitted for brevity)...
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitForForegroundCommand(pidOrZero);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
        if (result == foregroundPID) foregroundPID = 0;
    }
}

Slide 25: Step 1: Identify At-Risk Shared Data

Thought: foregroundPID is used in the main code and signal handler.  Maybe there's a race condition because of that.

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    while (foregroundPID == pid) {;}
}

static void executeCommand(char *command, bool inBackground) {
    // ...(omitted for brevity)...
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitForForegroundCommand(pidOrZero);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
        if (result == foregroundPID) foregroundPID = 0;
    }
}

Slide 26: Step 2: Identify Operation Ordering

waitForForegroundCommand waits for the handler to change foregroundPID to 0.  It assumes this ordering:

  1. it will set foregroundPID to the pid of interest
  2. the handler will execute
  3. meanwhile, waitForForegroundCommand waits for foregroundPID to be 0.

How can races break this

assumption?

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    while (foregroundPID == pid) {;}
}

static void executeCommand(char *command, bool inBackground) {
    // ...(omitted for brevity)...
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitForForegroundCommand(pidOrZero);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
        if (result == foregroundPID) foregroundPID = 0;
    }
}

Slide 27: Step 2: Identify Operation Ordering

  1. spawn child process
  2. execute line 14 (function call)
  3. child finishes, SIGCHLD received
  4. reapProcess completes, not modifying foregroundPID.
  5. main code resumes, calling waitForForegroundCommand
  6. line 5 executed
  7. enter loop on line 6
  8. loop never terminates because foregroundPID will never again change!
// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    while (foregroundPID == pid) {;}
}

static void executeCommand(char *command, bool inBackground) {
    // ...(omitted for brevity)...
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitForForegroundCommand(pidOrZero);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
        if (result == foregroundPID) foregroundPID = 0;
    }
}

Slide 28: Step 2: Identify Operation Ordering

waitForForegroundCommand waits for the handler to change foregroundPID to 0.  It assumes this ordering:

  1. it will set foregroundPID to the pid of interest
  2. the handler will execute
  3. meanwhile, waitForForegroundCommand waits for foregroundPID to be 0.

Problem: steps 1 & 2 could be flipped, causing deadlock.

Slide 29: Deadlock

Deadlock is a program state in which no progress can be made - it is caused by code waiting for something that will never happen.

 

E.g. waitForForegroundProcess loops until foregroundPID is set to 0.  But it never will be set to 0 in this case!

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    while (foregroundPID == pid) {;}
}

static void executeCommand(char *command, bool inBackground) {
    // ...(omitted for brevity)...
    if (inBackground) {
        printf("%d %s\n", pidOrZero, command);
    } else {
        waitForForegroundCommand(pidOrZero);
    }
}

static void reapProcesses(int signum) {
    while (true) {
        pid_t result = waitpid(-1, NULL, WNOHANG);
        if (result <= 0) break;
        if (result == foregroundPID) foregroundPID = 0;
    }
}

Slide 30: Step 3: Force Expected Orderings

We want to force this ordering:

  1. it will set foregroundPID to the pid of interest
  2. the handler will execute
  3. meanwhile, waitForForegroundCommand waits for foregroundPID to be 0.

 

How can we force the handler to only execute after step 1? 

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    setSIGCHLDBlock(false);
    while (foregroundPID == pid) {;}
}

// ...omitted for brevity...

Slide 31: Waiting For SIGCHLD

Slide 32: Plan For Today

Slide 33: Announcements

Slide 34: Mid-Lecture Checkin:

We can now answer the following questions:

Slide 35: Plan For Today

// The currently-running foreground command PID
static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    setSIGCHLDBlock(false);
    while (foregroundPID == pid) {
    	pause();
    }
}

// ...omitted for brevity...

Slide 36: Waiting For SIGCHLD

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    setSIGCHLDBlock(false);
    while (foregroundPID == pid) {
    	pause();
    }
}

Identify shared data that may be modified concurrently.  What global variables are used in both the main code and signal handlers?   Whether we've received SIGCHLD.

Document and confirm an ordering of events that causes unexpected behavior.  What assumptions are made in the code that can be broken by certain orderings?

Use concurrency directives to force expected orderings.  How can we use signal blocking and atomic operations to force the correct ordering(s)?

static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    setSIGCHLDBlock(false);
    while (foregroundPID == pid) {
    	pause();
    }
}

waitForForegroundCommand waits for SIGCHLD and then checks the global state.  It assumes this ordering:

  1. it will set foregroundPID to the pid of interest
  2. it will allow SIGCHLD signals
  3. it will loop until a SIGCHLD is received
  4. a SIGCHLD signal will come in that causes the loop to break

Problem: steps 3 & 4 could be flipped, causing deadlock (pause never returns)

static pid_t foregroundPID = 0;

static void waitForForegroundCommand(pid_t pid) {
    foregroundPID = pid;
    setSIGCHLDBlock(false);
    while (foregroundPID == pid) {
    	pause();
    }
}

sigsuspend, atomically both adjusts the blocked signal set and goes to sleep until a signal is received.  When some unblocked signal arrives, the process gets the CPU, the signal is handled, the original blocked set is restored, and sigsuspend returns.
 

 

 

 

 

 

// simplesh-all-better.c                                                                                                                                                           
static void waitForForegroundProcess(pid_t pid) {
  fgpid = pid;
  sigset_t empty;
  sigemptyset(&empty);
  while (fgpid == pid) {
    sigsuspend(&empty);
  }
  updateSIGCHLDBlock(false);
}

Slide 37: Waiting For Signals

static void waitForForegroundProcess(pid_t pid) {
  fgpid = pid;
  sigset_t empty;
  sigemptyset(&empty);
  while (fgpid == pid) {
    /* sigsuspend does (in one atomic operation)
     * 1) update blocked set to this mask
     * 2) go to sleep until signal
     * 3) when woken up, restore original mask
     */
    sigsuspend(&empty);
  }
  updateSIGCHLDBlock(false);
}

Slide 38: Waiting For Signals

Slide 39: Plan For Today

Consider this program and its execution. Assume that all processes run to completion, all system and printf calls succeed, and that all calls to printf are atomic. Assume nothing about scheduling or time slice durations.

static void bat(int unused) { 
  printf("pirate\n"); 
  exit(0);
}

int main(int argc, char *argv[]) {
  signal(SIGUSR1, bat);
  pid_t pid = fork();
  if (pid == 0) { 
    printf("ghost\n"); 
    return 0;
  }
  kill(pid, SIGUSR1); 
  printf("ninja\n"); return 0;
}

Slide 40: Practice Problem 1

Consider this program and its execution. Assume that all processes run to completion, all system and printf calls succeed, and that all calls to printf are atomic. Assume nothing about scheduling or time slice durations.

static void bat(int unused) { 
  printf("pirate\n"); 
  exit(0);
}

int main(int argc, char *argv[]) {
  signal(SIGUSR1, bat);
  pid_t pid = fork();
  if (pid == 0) { 
    printf("ghost\n"); 
    return 0;
  }
  kill(pid, SIGUSR1); 
  printf("ninja\n"); return 0;
}

Slide 41: Practice Problem 1

Consider this program and its execution. Assume that all processes run to completion, all system and printf calls succeed, and that all calls to printf are atomic. Assume nothing about scheduling or time slice durations.

int main(int argc, char *argv[]) {
    pid_t pid;
    int counter = 0;
    while (counter < 2) {
        pid = fork();
        if (pid > 0) break;
        counter++;
        printf("%d", counter);
    }
    if (counter > 0) printf("%d", counter);
    if (pid > 0) {
        waitpid(pid, NULL, 0);
        counter += 5;
        printf("%d", counter);
    }
    return 0;
}

Slide 42: Practice Problem 2

Slide 43: Example midterm question #2

int main(int argc, char *argv[]) {
    pid_t pid;
    int counter = 0;
    while (counter < 2) {
        pid = fork();
        if (pid > 0) break;
        counter++;
        printf("%d", counter);
    }
    if (counter > 0) printf("%d", counter);
    if (pid > 0) {
        waitpid(pid, NULL, 0);
        counter += 5;
        printf("%d", counter);
    }
    return 0;
}
int main(int argc, char *argv[]) {
    pid_t pid;
    int counter = 0;
    while (counter < 2) {
        pid = fork();
        if (pid > 0) break;
        counter++;
        printf("%d", counter);
    }
    if (counter > 0) printf("%d", counter);
    if (pid > 0) {
        waitpid(pid, NULL, 0);
        counter += 5;
        printf("%d", counter);
    }
    return 0;
}

Slide 44: Practice Midterm Problem 2

int main(int argc, char *argv[]) {
    pid_t pid;
    int counter = 0;
    while (counter < 2) {
        pid = fork();
        if (pid > 0) break;
        counter++;
        printf("%d", counter);
    }
    if (counter > 0) printf("%d", counter);
    if (pid > 0) {
        waitpid(pid, NULL, 0);
        counter += 5;
        printf("%d", counter);
    }
    return 0;
}

Slide 45: Practice Midterm Problem 2

static pid_t pid; // necessarily global so handler1 has
                  // access to it 
static int counter = 0;
static void handler1(int unused) { 
	counter++;
	printf("counter = %d\n", counter); 
	kill(pid, SIGUSR1);
}
static void handler2(int unused) { 
	counter += 10;
	printf("counter = %d\n", counter); 
	exit(0);
}
int main(int argc, char *argv[]) { 
	signal(SIGUSR1, handler1);
	if ((pid = fork()) == 0) {
		signal(SIGUSR1, handler2); 
		kill(getppid(), SIGUSR1); 
		while (true) {}
	}
	if (waitpid(-1, NULL, 0) > 0) { 
		counter += 1000;
		printf("counter = %d\n", counter);
	}
	return 0; 
}

Slide 46: Practice Midterm Problem 3

Slide 47: Practice Midterm Problem 3

    counter = 1
    counter = 10
    counter = 1001
static pid_t pid; // necessarily global so handler1 has
                  // access to it 
static int counter = 0;
static void handler1(int unused) { 
	counter++;
	printf("counter = %d\n", counter); 
	kill(pid, SIGUSR1);
}
static void handler2(int unused) { 
	counter += 10;
	printf("counter = %d\n", counter); 
	exit(0);
}
int main(int argc, char *argv[]) { 
	signal(SIGUSR1, handler1);
	if ((pid = fork()) == 0) {
		signal(SIGUSR1, handler2); 
		kill(getppid(), SIGUSR1); 
		while (true) {}
	}
	if (waitpid(-1, NULL, 0) > 0) { 
		counter += 1000;
		printf("counter = %d\n", counter);
	}
	return 0; 
}
static pid_t pid; // necessarily global so handler1 has
                  // access to it 
static int counter = 0;
static void handler1(int unused) { 
	counter++;
	printf("counter = %d\n", counter); 
	kill(pid, SIGUSR1);
}
static void handler2(int unused) { 
	counter += 10;
	printf("counter = %d\n", counter); 
	exit(0);
}
int main(int argc, char *argv[]) { 
	signal(SIGUSR1, handler1);
	if ((pid = fork()) == 0) {
		signal(SIGUSR1, handler2); 
		kill(getppid(), SIGUSR1); 
		while (true) {}
	}
	if (waitpid(-1, NULL, 0) > 0) { 
		counter += 1000;
		printf("counter = %d\n", counter);
	}
	return 0; 
}

Slide 48: Practice Midterm Problem 3

Slide 49: Practice Midterm Problem 3

static pid_t pid; // necessarily global so handler1 has
                  // access to it 
static int counter = 0;
static void handler1(int unused) { 
	counter++;
	printf("counter = %d\n", counter); 
	kill(pid, SIGUSR1);
}
static void handler2(int unused) { 
	counter += 10;
	printf("counter = %d\n", counter); 
	exit(0);
}
int main(int argc, char *argv[]) { 
	signal(SIGUSR1, handler1);
	if ((pid = fork()) == 0) {
		signal(SIGUSR1, handler2); 
		kill(getppid(), SIGUSR1); 
		while (true) {}
	}
	if (waitpid(-1, NULL, 0) > 0) { 
		counter += 1000;
		printf("counter = %d\n", counter);
	}
	return 0; 
}

Slide 50: Overview: Signals and Concurrency

Slide 51: Recap

 

Next Time: Introduction to Threads