Review session 3 Outline: 1. CPP 2. Mutexes and Condition variables 3. Talk through lab3 skeleton code 1. CPP a) OOP, classes and objects * encapsulates 1)a set of variables - shared memory / state 2)synchronized methods to access the state * relatively clean synchronization code compared to equivalent in C ** initiate the mutexes/conds in the constructor, destroy them in destructor ** lock at the beginning and unlock and the end of every public methods, i.e. the entry points to the shared state * method syntax foo *p; foo a; p->b() a.b(); b) Manual memory management, analogies to C: C - CPP * malloc - new * free - delete, cf. lab3 c) Namespace: using namespace std; class methods should be defined in its namespace d) Useful STL: * queue - FIFO: #include queue q; // polymorhpic / generic q.push(a); q.front(); q.pop(); q.size(); q.empty(); You are welcome to use other STL library. 2. Mutexes and Condition variables - Mutex - a C construct, ugly init and destroy: * API: void smutex_init(smutex_t *mutex); void smutex_destroy(smutex_t *mutex); void smutex_lock(smutex_t *mutex); void smutex_unlock(smutex_t *mutex); * Usage: straight-forward Before interaction with shared state, lock corresponding mutex After interaction, unlock the mutex - Condition variable * Motivation: Threads need to wait for certain condition of the shared state in order to execute its ``business logic''. * What it is not: NOT the actual state * What it is: a queue of threads that are waiting FOR some condition to become true. put differently, they should not be run until the condition is true Like a callback scheme. [MW: or, more generally, any notion of blocking.] i.e. when "something" happens, call me back / wake me up! Some problems: - when waked up the state could be changed again, so always check state again - spurious wakeup Operations: void scond_init(scond_t *cond); void scond_destroy(scond_t *cond); /* * Condition variables are always associated with state * variables that you access before signalling, broadcasting, * or waiting. To access the state variable, you must hold * the associated mutex. To help enforce this, you * are required to hold the mutex and pass it in as an * argument to these functions. */ void scond_signal(scond_t *cond, smutex_t *mutex); void scond_broadcast(scond_t *cond, smutex_t *mutex); void scond_wait(scond_t *cond, smutex_t *mutex); - Tobacco example in CPP Solving strategy: 1. Decompose problems into objects: - Units of concurrency: agent, smokers, table - shared state: stuff on the table - Main loop: - agent: put stuff on the table - smoker: consume paper, tobcacco, match 2. Sync constraints: only one thread can access table at once Agent should modify table only when table is empty Smoker can only modify the table when table has the things he/she needs 3. Lock - locks the whole table Two conditions: when table is full, wake up smokers when table is empty, wake up agent class Table { public: void matchSmokerUseTable(); void paperSmokerUseTable(); void tobaccoSmokerUseTable(); void agentUseTable(); Table(); int getPaper(); int getTobacco(); int getMatch(); ~Table(); private: scond_t tableEmptyCond; // contract: when table is empty, signal this scond_t tableFullCond; // when table is full, signal this smutex_t tableMutex; int paper, tobacco, match; // shared state, encapsulated // table is small, so can at most have two items at once bool tableIsFull(); bool tableIsEmpty(); }; Table:: Table() { paper = 0; tobacco = 0; match = 0; scond_init(&tableFullCond); scond_init(&tableEmptyCond); smutex_init(&tableMutex); } Table:: ~Table() { scond_destroy(&tableEmptyCond); scond_destroy(&tableFullCond); smutex_destroy(&tableMutex); } // warm up example int Table:: getPaper() { smutex_lock(&tableMutex); int r = paper; smutex_unlock(&tableMutex); return r; } // get tobacco and get match is similar // private method, why we don't need mutex here? bool Table:: tableIsFull() { return paper + tobacco + match >= 2; } bool Table:: tableIsEmpty() { return !tableIsFull(); } void Table:: agentUseTable() { smutex_lock(&tableMutex); while (tableIsFull()) { scond_wait(&tableEmptyCond, &tableMutex); } chooseIngredients(&paper, &tobacco, &match); // why broadcast intead of signal? scond_broadcast(&tableFullCond, &tableMutex); smutex_unlock(&tableMutex); } void Table:: paperSmokerUseTable() { smutex_lock(&tableMutex); while(tableIsEmpty() || (match < 1 || tobacco < 1)) { scond_wait(&tableFullCond, &tableMutex); } match--; tobacco--; scond_signal(&tableEmptyCond, &tableMutex); smutex_unlock(&tableMutex); } * Coding standards excerpts: - Always hold the lock when interacting the condition variable: Example: mutex_lock(&m); // Inspect shared state while (condition_not_met()) { mutex_unlock(&m); // preempted, another thread: change the state, call cond_signal but signaled an "empty" cond cond_wait(&c, &m); //sleeping forever } - Always use "while" when checking a condition, not "if": Not every signal will guarrantee the condition is met, because: 1. the wakeup is not immediate, so a third thread might grab the lock faster and modified the state before the waiting thread start to execute 2. spurious wakeup 3. the contract of the condition variable might not match the waiting thread's logic exactly 3. lab 3 walkthrough - Simulate a electronic store, with buyers and suppliers Store is a shared state: items, shipping price, discount. Buyers.... buy item(s) Supplier add items, change shipping price, discount, etc.. - Task generator, task queue Buyer task queue and supplier task queue Task on buyer queue: item to buy, budget Task on supplier: item to add, how many, etc.. Task generator: generate random tasks to add to either queue Buyer/supplier: dequeue task and execute against the shared state: estore - Threads involved: Task generator threads: two, one for buyer, one for supplier Buyer threads: dequeue buyer queue, execute Supplier threads: dequeue supplier queue, execute Special task: stop, when executed should exit the thread - So two blobs of shared states. - Two task queues, synchronized enqueue and deque between generators, buyers, suppliers - EStore, synchronization between buyers and suppliers