pete > courses > CS 315 Fall 26 > lecture 14: threads and mutexes
Lecture 14: threads and mutexes
Goals
- use threads to perform concurrent execution
- describe tradeoffs between threads and processes (ie, using fork) for this purpose
first, a lot of history
the view of the computer processor presented in Computer Architecture is that we’ve got an ALU, a register file, and memory
every time we tick the clock, a new instruction is executed, which causes things like data copied from memory to the register file or values in the register file passing through the ALU with the result stored back in the register file or data copied from the register file to memory
this is accurate, but it is (unsurprisingly) simplified
TODO: clockspeed increases, multi-processor systems, cores, multi-core systems, multithreading
all of that is not directly germaine to what you need to know in this course, it just sets the stage for why breaking up our tasks into multiple processes (or, shortly, multiple threads) helps us make more efficiency use of the hardware being produced today
we have seen how fork and exec can be used to implement concurrent execution: that is, multiple processes running at once
and, while those processes inherit certain characteristics of their parent, they do not share them
that is, a child process inherits all the variables and their values from the parent process, but changes to the variables in the child will not affect the variables in the parent
can we quantify this? can we figure out exactly what is shared between parent and child?
would I ask this question if the answer was "no" ?
result: only memory shared past fork is that which was mmap’d with MAP_SHARED
so only memory explicitly mapped as shared will be… you know, shared between parent and child; all other memory is completely segregated
but what if we want the two processes to work together? it would help if they could share data/state more conveniently
enter: threads
sometimes called lightweight processes (LWP)
they’re a lot like processes, except they can be made to "share" more state (eg, pid, specific parts of memory)
the kernel is responsible for scheduling threads, however, just as it is responsible for scheduling processes
(interestingly, this wasn’t always the case: the library we’ll be using for this stuff used to be entirely userspace, and there still remain threading libraries that exist entirely in userspace)
the pthread ("POSIX threads") library provides both threading and synchronization functionality
need to #include <pthread.h> and link with -lpthread
pthread_create is the threading analog to fork: it’s how we create a new thread
the third parameter is the important one: it’s the function that should execute within the new thread; when this function returns, the thread is finished
wildly unpredictable, in a bad way
sometimes the child prints, sometimes it doesn’t
it we run it twice, sometimes the child of the first process prints when the second process runs
I honestly have no idea what’s going on here, but it’s not good
we can fix this by introducing the threading equivalent of wait, which is pthread_join
as demonstrated here
now the child always prints before the parent exits
note that the pids are a) the same and b) increase by 2
I’m going to introduce a delay before the child (and therefore, by extension, the parent) exits
so we can examine the list of running processes while both threads are active
ps auxww | grep thread3 shows a single process
(in previous semesters, /proc/<pid>/maps showed a separate stack for the child thread)
recall the fork-based program I showed a few weeks ago that demonstrated child processes interleaving their output (see lecture 11)
I wrote the equivalent program using threads
it has some interesting peculiarities
note how we pass data to a thread (must be a void *)
note that we must have the pthread_t identifying the thread to join (ie, wait for) it
note that the threads are round-robin scheduled!
now to see how the same test works with threads
all memory is shared past pthread_create
good to know
now, finally, to demonstrate critical sections and mutual exclusion
I’ve slightly modified the interleaving code from before with the idea that each thread’s critical section is where it causes output
so where we previously saw all that interleaving, we would now like to see a solid chunk of zeroes, a solid chunk of ones, and a solid chunk of twos (though we may see all the twos first and then all the ones, etc)
clearly this does not happen just by wishing
here’s the same program with mutexes
really simple: acquire the mutex before the critical section, release it afterwards