CS202 Review Session 3 [Xiangyu Gao](https://xiangyug.github.io/), TA from Fall 2021, Edited by Khanh Nguyen, TA Spring 2022 Edited by Jinli Xiao, TA Spring 2023 Edited by Sophia Watts, TA Spring 2024 Edited by Saeed Bafarat, TA Fall 2024 Edited by Andrew Hua, TA Spring 2025 1. Introduction 2. Lab 2 continued 3. Aside: Process vs Thread 4. What is concurrency? 5. Lab 3 6. Resources --------------------------------------------------------------------- 1. Introduction 2. Lab 2 continued 2.1. Recursive Calls - One of the lab's requirements includes the flag `-R`, so what is it, why is it necessary, how to approach it? - Birdseye view - The `-R` flag tells ls to recursively explore the relevant directory/directories, meaning that any subdirectories will also be listed, and any inside that subdirectory will be printed, etc... - Why? If searching or exploring complex file structure, need to be able to scan all contents of a directory - Manually try to ls without -R by ls -> cd -> ... - Compare to going through using ls -R lab2/example - If you've been doing this somewhat ad-hoc(if statements with -a -> F, -l -> G, -al -> H, etc.), recursion will require you to change the structure of things order to accommodate recursion - Treat it like a recursive problem - Start from base case - Figure out how it branches out - Implement 2.2. Error Handling - Explain how the bitwise math of errors works - Give dummy example with a 4 bit error code and 4 errors - Let's say we have 4 errors A, B, C, D - Why would we want to use a number from 0-15 to represent an error code - For example, if we got error A, C, then would be 10 - Explain why 0 is "all-good" return value - Point to portion in code that handles error handling - PRINT ERROR macro and handle_error function - Do `./ls ./missing_dir` to get a directory not found error - explain `echo $?` displays error code, then execute to show that it is 0 - then do `ls ./missing_dir` and `echo $?` to show non-zero echo code 2.3. Testing and Debugging - This info is mostly drawn from the lab page itself, so you can use that as a second reference - Simplest: run `make test` to run the tests automatically, with their output being printed in the terminal. - Execute `make test` to show example results - Test discrepancies will be in the form of a diff, which describes the differences between two files. See rs02 for references and explanation - Next you can dissect the individual tests to see what is going on under the hood - `./mktest.sh [DIR_NAME]` creates the directory used for testing in the specified location - recommend `/tmp/test` for consistency - If need to remove, use rm -r [DIR_NAME] (be careful with lab files) - Then go to test.bats, each line describes a test in the set - First 2 lines are important, result executes your ls, compare executes correct ls - Unpack the commands pipe by pipe to understand process(do for check ls -a test, both result and compare) - Note you can always redirect w/ > to get the input in a text file (show with final compare result) - If you want to use gdb with this, need to use set an option to make it debug-friendly - ASAN_OPTIONS=detect_leaks=0 gdb ls - Do an example of gdb, `b` a line, then `r test` - note that you can run with flags and arguments, etc... 3. Process vs thread Process: - an instance of a program. - each process has its own memory space and system resources (like file handles and network connections). - processes are isolated, so the crash of one process won't affect others. Thread: - a unit of execution within a process. - a single process can be referred to as a single-threaded process. A process can have many threads, referred to as multi-thread. - different threads in a process share address spaces and resources. - if a thread crashes, it can cause the entire process to crash. Process Visualization: ---------------- | Stack Thread 1 | | | ---------------- | Stack Thread 2 | | | ---------------- | | | Heap | ---------------- | | | Program Code | ---------------- Note that this entire stack frame is one process, so it shares the same PCB(process control block): heap and code, but separated stacks. This is what allows concurrency to occur, as threads can interact with both shared data and thread data. Note: the stacks above have an invalid page(think slice of memory) between them which stops them from clobbering each other. 4. What is concurrency? - "It's when there are multiple threads?" - "It's when things execute at the same time" It's true. That's what concurrency is in essence: multiple things happening at the same time, competing for shared resources. multi-threading, multi-processing and asynchronous programming are all forms of concurrency. a. Why concurrency? - Allows for one process to work on I/O and computation at once, more efficient use of shared resources, for example while one task waits on network, another can run - In a concurrent system, multiple tasks can be executed at the same time. For example, a computer system can run Zoom, Spotify, and a game simultaneously. - However, programs sometimes need to share resources. For example, multiple threads may need to read or write the same file. This can easily cause problems if they access the file concurrently, resulting in one thread reading inconsistent data from the file. - An example of shared resources is GitHub: Users take the state, then locally change it before pushing / pull requesting changes to change the shared state. - In the programming world, various concurrency models and primitives have been developed to control access to shared resources. These include monitors, message passing, mutexes, conditional variables, and semaphores. In CS202, we will be using a monitor, which is a combination of mutexes and conditional variables, to handle concurrency. b. Sequential Consistency - "Informally, sequential consistency implies that operations appear to take place in some total order, and that that order is consistent with the order of operations on each individual process” (https://jepsen.io/consistency/models/sequential) Mental Model: T1: ---------A1------------A2------ T2: ------B1-------------B2------- In sequential consistency model, we can think of this as a game where you have to provide an ordering of all the events across all the threads. But the catch is that you can't re-order event within single thread. However, order across different thread can be re-ordered For instance, here are some valid order: - B1-A1-A2-B2 - B1-A1-B2-A2 - A1-B1-A2-B2 And here is some invalid order: - B1-A2-A1-B2 // A2 happens before A1 but both in T1 - B2-B1-A1-A2 // B2 happens before B1 but both in T2 c. Concurrency Commandments - Although you've already heard them and had them explained, I want to reiterate with examples because adhering these commandments means your code will almost always be correct. - Rule 1: always acquire/release locks at beginning/end of methods Better object-orientedness, so the objects can interact concurrently - Rule 2: always hold lock when doing condition variable operations - Rule 3: a thread in wait() must be prepared to be restarted at any time, not just when another thread calls signal In other words, if you have: if(!safe_to_proceed()) { wait(mtx); } MUST replace with a while loop, because the wait could spontaneously be woken and proceed without it being guaranteed to be safe. - Rule 4: Cannot replace broadcast() with signal(). But can replace signal() with broadcast() in cases where we don't care about extra wake-ups. - Implied rule: always code your concurrency the same way Being consistent will allow you to look back a day later, a week later, and still be able to understand instead of having to decode each ad-hoc implementation. It may be more efficient code, it's less efficient coding. - Hierarchy of concurrency - safe and correct >> - fast and efficient - Coarse grain locking locks the entire state, or a large section of it, when any thread is acting - why is this safer, but less concurrent? - Fine grain locking means more of the state is broken up, so need to keep track of more concurrency structures to cover the same info. - This is why fine-grain locking is harder than coarse-grain d. Concurrency Procedure 1. Planning(the most important part): 1a. Identify units of concurrency. Write down threads and the actions they'd take at a high level. 1b. Identify shared chunks of state. Plan to have these objects be monitors. 1c. Write down the high-level main loop of each thread. Advice: stay high level here. Don't worry about synchronization yet. Let the objects do the work for you. 2. Write down the synchronization constraints on the solution. Identify the type of each constraint: mutual exclusion or scheduling. For scheduling constraints, ask, "when does a thread wait" and "what does it wait for"? 3. Create a lock or condition variable corresponding to each constraint 4. Write the methods, using locks and condition variables for coordination Separate threads from objects. Shared state and synchronization should be encapsulated in shared objects. 5. Lab 3 5.1 Producer-Consumer architecture - Examples? Who would be corresponding consumer + producer? - Online marketplace - Task distributor - Creating a store where producers can add items or change their attributes while consumers buy it if they have the budget - Step 1: - What could the shared states be? - What types would users fall into, and how would they interact with the state? - Why this lab: to give students practice in using threads and monitors 5.2 Notable files - sthread.cpp: our thin wrapper around pthread. Use this as your threading library - TaskQueue.cpp: the task queue that suppliers and consumers dequeue to operate on - estoresim.cpp: main entry point for the code, creating threads - EStore.cpp: the shared object that suppliers and consumers are operating on - RequestHandlers.cpp: handler so that worker thread knows what to do 5.3 Common pitfalls: - Forgot to use destructor to free/destroy resources (to avoid do grep -i smutex_init and check that it has a corresponding smutex_destroy) - Forgot to release the mutex (to avoid do grep -i smutex_lock and check that it has a corresponding smutex_unlock) - Has code of the form: int condition = some_condition while(condition) { .... } -> This is wrong because condition might rely on shared variables so need to be recomputed -> how would we fix this? Example: Want to buy an item but you do not have the budget If you do price=item.id then do while(budget and just use the C String you are used to. - [cppreference](cppreference.com) is a great resource for both C and C++ libraries that you use 6. Resources: - Jepsen: https://jepsen.io/consistency/models/sequential - Concurrency practice(a bit simple, but it helps to visualize the "interlacing" of threads): http://deadlockempire.github.io/#menu - cppreference: https://en.cppreference.com/w/