23 KiB
23 KiB
file:: ostep_1681115599584_0.pdf file-path:: ../assets/ostep_1681115599584_0.pdf
-
Part II
- thread
ls-type:: annotation
hl-page:: 311
hl-color:: yellow
id:: 6433ca28-1bdf-433d-8ed9-0d54bf5ba940
collapsed:: true
- share the same address space and thus can access the same data
- context switch: the address space remains the same hl-page:: 311 ls-type:: annotation id:: 6433cb70-d168-4863-8268-1e969df6ce06 hl-color:: yellow
- thread control blocks ls-type:: annotation hl-page:: 311 hl-color:: yellow id:: 6433cb56-fbef-46da-83c2-13fa2dba2967
- thread-local storage: one stack per thread in the address space hl-page:: 312 ls-type:: annotation id:: 6433cba2-61bd-4549-a29f-2ad85b3e30cd hl-color:: yellow
- Why thread?
- possible speedup through parallelization
- enable overlap of IO in a single program
- Though these could be done through multi-processing, threading makes share data easier
- KEY CONCURRENCY TERMS
ls-type:: annotation
hl-page:: 323
hl-color:: yellow
id:: 6433eabf-48d6-4776-b66f-a5f7804d1ddc
collapsed:: true
- indeterminate: the results depend on the timing execution of the code.
- race condition ls-type:: annotation hl-page:: 320 hl-color:: yellow id:: 6433e4cc-69e4-4057-8cc6-1766240d82f4
- A critical section is a piece of code that accesses a shared variable (or resource) and must not be concurrently executed by more than one thread. hl-page:: 320 ls-type:: annotation id:: 6433e52b-1f38-4f7c-b168-0aed624f9bdf hl-color:: yellow
- mutual exclusion: This property guarantees that if one thread is executing within the critical section, the others will be prevented from doing so. hl-page:: 320 ls-type:: annotation id:: 6433e566-e6ef-45b3-84b1-eba981be914a hl-color:: yellow
- Atomicity: as a unit, or, all or none hl-page:: 321 ls-type:: annotation id:: 6433e6a1-407c-4936-b184-dee868ef4107 hl-color:: yellow
- synchronization primitives ls-type:: annotation hl-page:: 322 hl-color:: yellow id:: 6433e729-7043-453b-8d60-6e6c41560543
- sane 精神健全的;神志正常的;明智的;理智的 ls-type:: annotation hl-page:: 322 hl-color:: green id:: 6433e6e7-d995-4b69-96b3-261b79f94c1d
- Thread API
hl-page:: 327
ls-type:: annotation
id:: 6433f35b-403b-4b25-b9f9-076e9e34777e
hl-color:: yellow
collapsed:: true
pthread_createpthread_joinpthread_mutex_lockpthread_cond_*
- Locks
ls-type:: annotation
hl-page:: 339
hl-color:: yellow
id:: 6433f45b-0345-4790-8379-3d1a94e57ef5
- A lock is just a variable
hl-page:: 339
ls-type:: annotation
id:: 6433f4ba-f2e4-4743-a536-e2b7747433b7
hl-color:: yellow
collapsed:: true
- lock variable: some type of variable, which holds the state of the lock(and maybe additional data such as its holder or a queue for acquisition)
- lock state: available (or unlocked or free); acquired (or locked or held)
- lock routines:
lock()tries to acquire the lock. If no other thread holds the lock, the thread will acquire the lock and enter the critical section(become the owner of the lock). Otherwise, it will not return while the lock is held by another thread.unlock(): The owner of the lock callsunlock(), then it is available again. If there are waiting threads, one of them will (eventually) notice (or be informed of) this change of the lock's state, acquire the lock, and enter the critical section.
- Locks help transform the chaos that is traditional OS scheduling into a more controlled activity hl-page:: 340 ls-type:: annotation id:: 6433f5e6-bc06-42a9-866e-e9a3053f528f hl-color:: yellow
- Controlling Interrupts
ls-type:: annotation
hl-page:: 342
hl-color:: yellow
id:: 6433fbfd-a1bf-4fd9-a54d-e15189c77b15
collapsed:: true
- For single-processor systems, disable interrupts for critical sections.
- Problems
- disable interrupts is privileged. In the worst case, the OS may never regain control when the interrupt isn't going to be enabled.
- does NOT work on multi-processor systems, each CPU has its own interrupt state
- importance interrupts may get lost
- inefficient
- Just Using Loads/Stores(Fail)
hl-page:: 343
ls-type:: annotation
id:: 6433fe7e-2221-41ee-ad6b-7deaa4459aa5
hl-color:: yellow
collapsed:: true
- use a simple variable (flag) to indicate whether some thread has possession of a lock
hl-page:: 343
ls-type:: annotation
id:: 6433ff4a-856d-4e4b-af30-6cb600aefeb5
hl-color:: yellow
- On acquisition, load, test the flag. If free, set the flag; If not free, spin-wait(loop load and test).
- On releasing, clear the flag.
- Problem
- When interrupted between load and test, mutual exclusion is broken.
- Low efficiency because of spin-waiting.
- use a simple variable (flag) to indicate whether some thread has possession of a lock
hl-page:: 343
ls-type:: annotation
id:: 6433ff4a-856d-4e4b-af30-6cb600aefeb5
hl-color:: yellow
- spin lock
-
void lock(lock_t *lock) { while (TestAndSet(&lock->status, 1) == 1); } void unlock(lock_t *lock) { lock->status = 0; } - Requires a preemptive scheduler(or it may spin forever)
- No fairness guarantee
- For single processor systems, terrible performance, because the thread holding the lock cannot make any progress to release the lock until it is scheduled again and thus all other threads waiting for the lock can do nothing but spinning even they are scheduled.
- For multi-processor systems, spin lock may work well when thread B on CPU1 waits for thread A on CPU0, and the critical section is short. Because lock owner keeps making progress, spinning doesn't waste many cycles.
- Priority Inversion: Threads with high priority wait for locks held by threads with low priority. hl-page:: 355 ls-type:: annotation id:: 6435099b-0834-483e-9ef2-98a0b795cf00 hl-color:: yellow Solution: priority inheritance or give up the priority?
-
- Test-And-Set (Atomic Exchange)
hl-page:: 344
ls-type:: annotation
id:: 643401e0-fcec-41d3-9898-d5c4175ac464
hl-color:: yellow
collapsed:: true
- Returns the old value pointed to by the
old_ptr, and simultaneously updates said value tonew. - "test" the old value (which is what is returned) while simultaneously "set" the memory location to a new value
-
int TestAndSet(int *old_ptr, int new) { int old = *old_ptr; *old_ptr = new; return old; }
- Returns the old value pointed to by the
- Compare-And-Swap
hl-page:: 348
ls-type:: annotation
id:: 6434f8ac-d762-40a4-abb0-2955c2c8b396
hl-color:: yellow
collapsed:: true
- Test whether the value at the address specified by
ptris equal toexpected. hl-page:: 348 ls-type:: annotation id:: 6434fab0-08de-4f28-8d8e-f48f7e04aaaa hl-color:: yellow If so, update the memory location with thenewvalue. If not, do nothing. Return the old value at the memory location. -
int CompareAndSwap(int *ptr, int expected, int new) { int original = *ptr; if (original == expected) *ptr = new; return orginial } - Compare-and-swap flavor spin lock
void lock(lock_t *lock) { while (CompareAndSwap(&lock->status, 0, 1) == 1) ; }
- Test whether the value at the address specified by
- load-linked and store-conditional
hl-page:: 349
ls-type:: annotation
id:: 6434fde1-9d19-4381-805e-f2a972875dc2
hl-color:: yellow
collapsed:: true
- The load-linked operates much like a typical load instruction, and simply fetches a value from memory and places it in a register. ls-type:: annotation hl-page:: 349 hl-color:: yellow id:: 6434fe1c-47f3-422c-a317-be72f08d6aef
- store-conditional only succeeds if no intervening store to the address has taken place.
hl-page:: 349
ls-type:: annotation
id:: 6434fe62-0e92-4414-86cc-b0c37fcf51ec
hl-color:: yellow
On success, return 1 and update the value at
ptrto value. On failure, return 0 and the value atptris not updated. -
int LL(int *ptr) { return *ptr; } int SC(int *ptr, int value) { if (/*no update to *ptr since LoadLinked to this address*/) { *ptr = value; return 1; // success! } else { return 0; // failed to update } } - LL/SC flavor spin lock: very similar to the errant Load/Store lock, but the special instructions here can detect intervening
void lock(lock_t *lock) { while (true) { while (LL(&lock->status) == 1) ; // test if (SC(&lock->status, 1) == 1) // set break; // else retry, in case lock->status is changed } }
- Fetch-And-Add
ls-type:: annotation
hl-page:: 350
hl-color:: yellow
id:: 64350170-c853-4080-9ed1-2777ea3a18c8
collapsed:: true
- Atomically increments a value while returning the old value at a particular address
-
int FetchAndAdd(int *ptr) { int old = *ptr; *ptr = old + 1; return old; } - ticket lock
hl-page:: 351
ls-type:: annotation
id:: 64350331-8fbb-4c41-9ac1-1a4ba852f772
hl-color:: yellow
-
struct lock_t{ int ticket; int turn; }; void lock(lock_t *lock) { int myturn = FetchAndAdd(&lock->ticket); // atomically allocate a ticket as the thread's turn while (lock->turn != myturn) ; // wait for its turn } void unlock(lock_t *lock) { lock->turn += 1; } - Ensure progress for all threads. Once a thread is assigned its ticket value, it will be scheduled at some point in the future (i.e. it will definitely get its turn as
unlock()operations increase globalturnvalue). hl-page:: 351 ls-type:: annotation id:: 64350420-ca8a-4cac-af2f-f4e7deb5d1be hl-color:: yellow In contrast, test-and-set spin lock may starve, if it is very unlucky.(never succeeds in contention)
-
- Simple Yield Lock
hl-page:: 353
ls-type:: annotation
id:: 64350781-6995-41db-8b8e-2de0eb84136a
hl-color:: yellow
collapsed:: true
yield: a system call that moves the caller from the running state to the ready state, and thus promotes another thread to running. hl-page:: 353 ls-type:: annotation id:: 643507af-1153-46c1-b232-31a9a203e5df hl-color:: yellow-
void lock(lock_t *lock) { while (TestAndSet(&lock->status, 1) == 1) yield(); } - Problem: Starvation is still possible; Context switch overhead, though better than spinning
- Lock With Queues, Test-and-set, Yield, And Wakeup
ls-type:: annotation
hl-page:: 354
hl-color:: yellow
id:: 64350b44-dfae-4544-93f9-ff2b343fefd4
- The real problem is: We have not much control over which thread to run next and thus causes potential waste. hl-page:: 353 ls-type:: annotation id:: 64350b4e-9559-49d9-aa37-eda9fe425b7f hl-color:: yellow
park(): put a calling thread to sleep hl-page:: 354 ls-type:: annotation id:: 64350bfb-64f7-4d41-8cc2-260dbec3372d hl-color:: yellowunpark(threadID): wake a particular thread hl-page:: 354 ls-type:: annotation id:: 64350c01-39bb-4d15-b554-0287b13806ee hl-color:: yellow- implement
struct lock_t{ int lk; int guard; // spin lock for the whole lock queue_t *q; // control who gets the lock next }; void lock(lock_t *lock) { while (TestAndSet(&m->guard, 1) == 1) ; if (m->lk == 0) { m->lk = 1; m->guard = 0; } else { m->q->add(get_tid()); setpark(); // newly added m->guard = 0; // ---- wakeup/waiting race ---- park(); } } void unlock(lock_t *lock) { while (TestAndSet(&m->guard, 1) == 1) ; if (m->q->empty()) m->flag = 0; else unpark(m->q->pop()); // should not clear flag here // Wake up Only one waiting thread m->guard = 0; } - When a thread is woken up, it will be as if it is returning from
park(). Thus whenunparka thread, pass the lock directly from the thread releasing the lock to the next thread acquiring it; flag is not set to 0 in-between. - wakeup/waiting race: If the thread is scheduled out just before it calls
park, and then the lock owner callsunparkon that thread, it would sleep forever. hl-page:: 356 ls-type:: annotation id:: 64351ba3-d4b5-4999-bc61-7733d5e0a061 hl-color:: yellow- One solution is to use
setpark(): indicate the thread is about topark. If it happens to be interrupted and another thread callsunparkbeforeparkis actually called, the subsequent park returns immediately instead of sleeping.
- One solution is to use
- Peterson's algorithm: mutual exclusion lock for 2 threads without hardware atomic instruction. Use 2 intention flags and a turn flag. hl-page:: 345 ls-type:: annotation id:: 6434edd3-2a7b-4e11-af18-29854e628bc6 hl-color:: yellow
- two-phase lock
hl-page:: 358
ls-type:: annotation
id:: 643522a7-4b16-4998-9b2f-47a852681a16
hl-color:: yellow
- A combination of spin lock and sleep lock
- In the first phase, the lock spins for a while, hoping that it can acquire the lock. hl-page:: 358 ls-type:: annotation id:: 6435230e-d84a-4c91-8329-b7608b0d543a hl-color:: yellow
- A second phase is entered if the lock is not acquired, where the caller is put to sleep, and only woken up when the lock becomes free later. ls-type:: annotation hl-page:: 358 hl-color:: yellow id:: 64352344-d140-468c-987c-e8afa05c2171
- Linux System Call futex
hl-page:: 356
ls-type:: annotation
id:: 64351e9a-6505-4176-a6fb-ddf63f3245a8
hl-color:: yellow
- each
futexis associated with ==a specific physical memory location==, and ==an in-kernel queue== futex_wake(address)wakes one thread that is waiting on the queue.futex_wait(address, expected)puts the calling thread to sleep, assuming the value ataddressis equal toexpected. If it is not equal, the call returns immediately.- Figure 28.10: Linux-based Futex Locks ls-type:: annotation hl-page:: 357 hl-color:: yellow id:: 64352221-d590-4371-a5f0-29e9cfa75ccb
- each
- A lock is just a variable
hl-page:: 339
ls-type:: annotation
id:: 6433f4ba-f2e4-4743-a536-e2b7747433b7
hl-color:: yellow
collapsed:: true
- efficacy 功效,效力 ls-type:: annotation hl-page:: 341 hl-color:: green id:: 6433fb69-1425-46b4-996f-f91da5d3e8d0
- foil ls-type:: annotation hl-page:: 347 hl-color:: green id:: 6434f523-44b7-40ab-8fea-528969c5acfd
- delve 钻研;探究;挖 ls-type:: annotation hl-page:: 349 hl-color:: green id:: 6434fb8c-2b3b-4d80-83fb-3b34da4dcd28
- brag 吹嘘;自吹自擂 ls-type:: annotation hl-page:: 351 hl-color:: green id:: 643501c1-f11b-4e85-8125-d2a5a31f69b0
- scourge
- 鞭打;鞭笞;折磨;使受苦难
- Lock-based Concurrent Data Structures
ls-type:: annotation
hl-page:: 361
hl-color:: yellow
id:: 643525b0-e245-489b-877d-a2a1d63e7ea6
- Concurrent Counters
hl-page:: 361
ls-type:: annotation
id:: 643525e5-fb85-48d4-905a-2a88b9ac0b0d
hl-color:: yellow
collapsed:: true
- Counter with lock
- Wrap the all the operations with a single lock.
- Performance is bad due to lock contention and it gets worse when the number of threads increases.
- perfect scaling: the increase in thread number doesn't harm the performance hl-page:: 363 ls-type:: annotation id:: 64352751-d9bd-4d5e-a8ba-cd18f86b1a15 hl-color:: yellow
- approximate counter
hl-page:: 363
ls-type:: annotation
id:: 64352794-d7c8-42f9-8321-f874967cebf2
hl-color:: yellow
- represent a single logical counter via ==numerous local physical counters==(one per CPU core), as well as ==a single global counter==. Each actual counter has a ==lock==.
- To add the counter, acquire the ==local lock== and increase it, thus avoiding contention.
- To read the counter, acquire the ==global lock== and read.
- To keep the global counter up to date, the local values are periodically transferred to the global counter and reset, which requires ==global lock and local lock==. A threshold
Sdetermines how often this transfer happens, tuning the trade-off between scalability and precision.
- Counter with lock
- Concurrent Linked Lists
ls-type:: annotation
hl-page:: 367
hl-color:: yellow
id:: 643530d8-9d09-4c8a-9e92-47dfe814ef50
collapsed:: true
- Again, the simplest way to implement this is to wrap all operations on the list with a single lock.
- Assuming the
mallocis ==thread-safe==, we can improve the code a little by narrowing critical section: only operations on global structure need to be locked. - hand-over-hand locking: a lock per node.
hl-page:: 369
ls-type:: annotation
id:: 64353237-4b74-4148-b7c1-5854d83a18c7
hl-color:: yellow
- When traversing the list, the code first grabs the next node's lock and then releases the current node's lock.
- In practice, it ==doesn't work== due to prohibitive overhead
- Concurrent Queues
ls-type:: annotation
hl-page:: 370
hl-color:: yellow
id:: 64353353-9de2-421b-967d-dc80a597eecd
- Two locks, head and tail, for
enqueueanddequeueoperation. - Add a dummy node to separate head and tail operation. Without this,
dequeueoperation needs to acquire both locks in case the queue is empty.
- Two locks, head and tail, for
- Concurrent Hash Table
hl-page:: 372
ls-type:: annotation
id:: 6435360d-c176-494a-9d61-b1fd0107a9bd
hl-color:: yellow
- instead of having a single lock for the entire structure, it uses a lock per hash bucket ls-type:: annotation hl-page:: 372 hl-color:: yellow id:: 6435363d-c697-42a6-bfd0-8a2332cef394
- Concurrent Counters
hl-page:: 361
ls-type:: annotation
id:: 643525e5-fb85-48d4-905a-2a88b9ac0b0d
hl-color:: yellow
collapsed:: true
- ubiquitous 似乎无所不在的;十分普遍的 ls-type:: annotation hl-page:: 372 hl-color:: green id:: 6435365a-b5d6-46fc-a9a1-25b0d23aa529
- humble 谦逊;低声下气;虚心;贬低 ls-type:: annotation hl-page:: 373 hl-color:: green id:: 6435367f-dd9e-449d-b0e4-3d8c9e14f6c2
- sloppy 马虎的,草率的;(衣服)宽松肥大的;太稀的,不够稠的; hl-page:: 376 ls-type:: annotation id:: 643536c8-fc05-4bbe-8d1d-0f4f6d1c4fee hl-color:: green
- gross 总的,毛的;严重的,极端的;粗鲁的;臃肿的;粗略的; hl-page:: 378 ls-type:: annotation id:: 643537d3-7d01-442b-b47e-59433c2aa6db hl-color:: green
- condition variable
hl-page:: 378
ls-type:: annotation
id:: 643537ff-1028-4725-8d7a-c0338cc946d3
hl-color:: yellow
- A ==condition variable== is an explicit queue that threads can put themselves on when some state of execution(condition) is not as desired (by waiting on the condition); some other thread, when it changes said state, can then wake one (or more) of those waiting threads and thus allow them to continue (by signaling). hl-page:: 378 ls-type:: annotation id:: 64353882-7697-4c16-8e53-c8f59ea256c1 hl-color:: yellow
- Operations
wait()put the caller to sleep.pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m)hl-page:: 378 ls-type:: annotation id:: 643538d5-9ea3-4399-9fa2-d75fdf0e1dd4 hl-color:: yellowsignal()wake up a sleeping thread waiting on this condition.pthread_cond_signal(pthread_cond_t *c);hl-page:: 379 ls-type:: annotation id:: 643538de-cc40-4dd2-8f03-9492004f209b hl-color:: yellow- The
wait()also takes a mutex as a parameter; it assumes that this mutex is locked whenwait()is called. The responsibility ofwait()is to ==release the lock and put the calling thread to sleep== (atomically); when the thread wakes up, it must ==re-acquire the lock before returning== to the caller. The design is helpful to avoid some race conditions when trying to sleep. - use a while loop instead of just an if statement when deciding whether to wait on the condition. ls-type:: annotation hl-page:: 380 hl-color:: yellow id:: 643547c5-1613-49e9-899e-0e86f59a1462
- stem ls-type:: annotation hl-page:: 379 hl-color:: green id:: 64353eb8-8ed8-4680-a3c0-91608b429408
- producer/consumer problem
hl-page:: 382
ls-type:: annotation
id:: 64354974-adea-4b20-90f4-a12ebe1e4d5b
hl-color:: yellow
- Mesa semantics: Signaling a thread only wakes them up; it is thus a hint that the state of the world has ==changed==, but there is ==no guarantee== that when the woken thread runs, the state will ==still be as desired==. (Another guy may run before the thread and change the state again)
hl-page:: 385
ls-type:: annotation
id:: 64354cc4-14c5-408d-b879-7d4d011b2b5c
hl-color:: yellow
- So, always use while loops. While loops make sure the thread wake up in the desired state of world, which tackles the ((64355502-f41f-40dd-b71f-e0abdbc76716)) and provides support for ((64355441-5a1b-4015-baa1-65917526079c)) hl-page:: 386 ls-type:: annotation id:: 64354db0-8c74-4c14-b063-d26378a10555 hl-color:: yellow
- Hoare semantics: provides a stronger guarantee that the woken thread will run immediately upon being woken hl-page:: 386 ls-type:: annotation id:: 64354d46-4286-44fd-9e82-2ba562a50f25 hl-color:: yellow
- Incorrect Solution: single condition variable. The problem arises from the ==undirected wakeup operation==: God knows which thread is to be woken up.
- Envision multiple consumers and one producer:
- producer
P1increases count to 1, signals the CV and sleeps - consumer
C1is awaken, reduces count to 0, signals the CV and sleeps - another consumer
C2is woken up ==by accident==, finds out count is 0, sleeps - In this case, they all sleep and thus nobody will signal any of them
- producer
- If in step 3, the producer
P1is woken up, everything is fine. Obviously, one solution is to ==exert control over which thread is to be woken up==. Well, wake up all threads may also solve this problem, see ((64355441-5a1b-4015-baa1-65917526079c)).
- Envision multiple consumers and one producer:
- Correct solution: 2 condition variable.
- Producer threads wait on the condition
empty, and signalsfill. Conversely, consumer threads wait onfilland signalempty. - Code
cond_t empty, fill; mutex_t mutex; void *producer(void *arg) { int i; for (i = 0; i < loops; i++) { Pthread_mutex_lock(&mutex); while (count == MAX) Pthread_cond_wait(&empty, &mutex); put(i); Pthread_cond_signal(&fill); Pthread_mutex_unlock(&mutex); } } void *consumer(void *arg) { int i; for (i = 0; i < loops; i++) { Pthread_mutex_lock(&mutex); while (count == 0) Pthread_cond_wait(&fill, &mutex); int tmp = get(); Pthread_cond_signal(&empty); Pthread_mutex_unlock(&mutex); printf("%d\n", tmp); } }
- Producer threads wait on the condition
- spurious wakeups
hl-page:: 390
ls-type:: annotation
id:: 64355502-f41f-40dd-b71f-e0abdbc76716
hl-color:: yellow
- In some thread packages, due to details of the implementation, it is possible that two threads get woken up though just a single signal has taken place.
- covering condition
hl-page:: 391
ls-type:: annotation
id:: 64355441-5a1b-4015-baa1-65917526079c
hl-color:: yellow
- covers all the cases where a thread needs to wake up, those unneeded simply wake up, re-check condition and go back to sleep
pthread_cond_broadcast()wakes up all waiting threads
- Mesa semantics: Signaling a thread only wakes them up; it is thus a hint that the state of the world has ==changed==, but there is ==no guarantee== that when the woken thread runs, the state will ==still be as desired==. (Another guy may run before the thread and change the state again)
hl-page:: 385
ls-type:: annotation
id:: 64354cc4-14c5-408d-b879-7d4d011b2b5c
hl-color:: yellow
- albeit 尽管;虽然 ls-type:: annotation hl-page:: 390 hl-color:: green id:: 64354f54-b26c-48dc-a328-4ae355b680f3
- spurious 虚假的;伪造的;建立在错误的观念(或思想方法)之上的;谬误的 hl-page:: 390 ls-type:: annotation id:: 643554f4-75a7-48fa-9366-87058ee723fb hl-color:: green