os_cp:threads
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| os_cp:threads [2023/05/23 21:21] – jkonczak | os_cp:threads [2025/04/25 11:55] (current) – [POSIX condition variables] jkonczak | ||
|---|---|---|---|
| Line 52: | Line 52: | ||
| pointer to arbitrary data((To specify a pointer without specifying the type of | pointer to arbitrary data((To specify a pointer without specifying the type of | ||
| underlying data one has to use '' | underlying data one has to use '' | ||
| + | < | ||
| <code c> | <code c> | ||
| void * start_routine (void * argument){ | void * start_routine (void * argument){ | ||
| Line 58: | Line 59: | ||
| } | } | ||
| </ | </ | ||
| + | < | ||
| < | < | ||
| '' | '' | ||
| Line 67: | Line 69: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| '' | '' | ||
| Line 77: | Line 79: | ||
| argument is not NULL, thread attributes will be set before starting the thread. | argument is not NULL, thread attributes will be set before starting the thread. | ||
| - | < | + | < |
| ++++An example of creating a thread and passing it some data| | ++++An example of creating a thread and passing it some data| | ||
| <code c> | <code c> | ||
| Line 130: | Line 132: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 138: | Line 140: | ||
| The ''// | The ''// | ||
| - | < | + | < |
| ++++An example code that creates 3 threads and joins them, collecting their result.| | ++++An example code that creates 3 threads and joins them, collecting their result.| | ||
| <code c> | <code c> | ||
| Line 207: | Line 209: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 217: | Line 219: | ||
| typically the thread that spawned it calls the detach. | typically the thread that spawned it calls the detach. | ||
| - | < | + | < |
| ++++An example code that creates a thread and detaches it.| | ++++An example code that creates a thread and detaches it.| | ||
| <code c> | <code c> | ||
| Line 273: | Line 275: | ||
| POSIX offers a mechanism called // | POSIX offers a mechanism called // | ||
| thread a cancellation request using the | thread a cancellation request using the | ||
| - | '' | + | '' |
| function. Each thread can select is behaviour on receiving such request using the | function. Each thread can select is behaviour on receiving such request using the | ||
| - | '' | + | '' |
| and | and | ||
| - | '' | + | '' |
| functions. One may either allow or disallow cancalling a thread, and one may | functions. One may either allow or disallow cancalling a thread, and one may | ||
| choose whether the cancallation should ouccur immediatly after a request, or | choose whether the cancallation should ouccur immediatly after a request, or | ||
| at the next cancallation point. Cancellation points are the execution points | at the next cancallation point. Cancellation points are the execution points | ||
| of several pthread functions, among which notably | of several pthread functions, among which notably | ||
| - | '' | + | '' |
| is there just to this end. | is there just to this end. | ||
| Once the thread complies to the cancellation request, it roughly does the same | Once the thread complies to the cancellation request, it roughly does the same | ||
| Line 297: | Line 299: | ||
| ~~Exercise.# | ~~Exercise.# | ||
| - | Remove the code you wrote for the previous exercise the '' | + | Remove |
| - | re-run the code. What has changed in the behaviour of the program? | + | and re-run the code. What has changed in the behaviour of the program? |
| ~~Exercise.# | ~~Exercise.# | ||
| Line 312: | Line 314: | ||
| In the main thread, collect the returned numbers and display them. | In the main thread, collect the returned numbers and display them. | ||
| - | ===== Thread-specific data, thread local storage [extra] ===== | ||
| - | In C, data items can have different storage duration and different linkage. | + | ===== Accessing the same data from multiple threads ===== |
| - | Until C11, there existed only static, automatic or allocated storage durations. | + | |
| - | Roughly: | + | |
| - | \\ | + | |
| - | • static | + | |
| - | \\ | + | |
| - | • automatic | + | |
| - | \\ | + | |
| - | • allocated | + | |
| - | \\ | + | |
| - | None of these cover a case when one wants a static (" | + | |
| - | a separate value for each thread. | + | |
| - | Such storage duration is called [[https:// | + | |
| - | and a coupe of ways to create thread-local items are presented in this section. | + | |
| - | ==== POSIX Thread-specific | + | The following examples show what might happen upon accessing the same data from |
| + | multiple threads with no synchronisation. | ||
| - | One can create //keys// (that may be understood as identifiers of variables) and | + | First, let's read/write "linearly" |
| - | then associate, for each thread separately, a value for a key. | + | < |
| - | To this end the following functions can be used: | + | |
| - | \\ | + | |
| - | < | + | |
| - | <a href="https:// | + | |
| - | '' | + | |
| - | < | + | |
| - | \\ | + | |
| - | < | + | |
| - | '' | + | |
| - | \\ | + | |
| - | '' | + | |
| - | < | + | |
| - | \\ | + | |
| - | Each key is initially associated with a '' | + | |
| - | + | ||
| - | < | + | |
| - | ++++An example code that creates a key, stores and retrieves the value from two threads.| | + | |
| <code c> | <code c> | ||
| #include < | #include < | ||
| #include < | #include < | ||
| - | #include < | ||
| #include < | #include < | ||
| - | pthread_key_t mySpecificVal; | + | unsigned long long version; |
| + | char text[1020]; | ||
| - | void printMine() { | + | void *updater(void * arg) { |
| - | | + | |
| + | for (char letter = ' | ||
| + | version++; | ||
| + | for (int i = 0; i < 1020 - 1; ++i) | ||
| + | text[i] = letter; | ||
| + | // memset(text, | ||
| + | } | ||
| } | } | ||
| - | void *routine(void *arg) { | + | int main() { |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | return | + | return |
| } | } | ||
| + | </ | ||
| + | < | ||
| - | int main() { | + | Next, let's read-modify-write the same variable from multiple threads: |
| - | char *text = malloc(4); | + | < |
| - | strcpy(text, | + | <code c> |
| + | #include < | ||
| + | #include < | ||
| - | pthread_key_create(& | + | unsigned long long counter; |
| - | pthread_setspecific(mySpecificVal, | + | |
| - | pthread_t ti; | + | void *incrementer(void * arg) { |
| - | | + | |
| - | | + | counter++; |
| + | | ||
| + | } | ||
| - | printMine(); | + | int main() { |
| + | pthread_t tid[16]; | ||
| + | for (int i = 0; i < 16; ++i) | ||
| + | pthread_create(tid + i, NULL, incrementer, | ||
| + | for (int i = 0; i < 16; ++i) | ||
| + | pthread_join(tid[i], | ||
| + | printf(" | ||
| return 0; | return 0; | ||
| } | } | ||
| </ | </ | ||
| - | ++++ | + | < |
| + | < | ||
| + | < | ||
| + | '' | ||
| + | </ | ||
| < | < | ||
| - | ==== C thread-local storage ==== | + | ~~Exercise.#~~ |
| - | + | Read, understand | |
| - | Starting with C11, a new storage duration '' | + | What is wrong with the results? What problem do these examples show? |
| - | '' | + | |
| - | C23 '' | + | |
| - | is a keyword with same meaning as the '' | + | |
| - | Variables defined | + | |
| - | Notice that the '' | + | |
| - | POSIX), but there is no way to provide a custom destructor (unlike in POSIX). | + | |
| ===== POSIX mutexes and readers-writers locks ===== | ===== POSIX mutexes and readers-writers locks ===== | ||
| - | |||
| - | ==== Mutex vs binary semaphore ==== | ||
| - | |||
| - | There are two vital differences between mutexes and semaphores: | ||
| - | - A mutex locked by some thread //belongs// to it and only the thread is allowed to unlock it.\\ A semaphore can be decremented/ | ||
| - | - A mutex can be configured recursive, and then it can be locked multiple times by the same thread and will be unlocked only after corresponding number of unlocks. | ||
| ==== Mutexes ==== | ==== Mutexes ==== | ||
| Line 420: | Line 401: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 427: | Line 408: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| '' | '' | ||
| Line 439: | Line 420: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| \\ | \\ | ||
| and finally set the attributes using corresponding functions: | and finally set the attributes using corresponding functions: | ||
| - | * < | + | * < |
| - | * < | + | * < |
| - | * < | + | * < |
| - | * < | + | * < |
| The mutex type impacts what happens when a mutex that is already locked is | The mutex type impacts what happens when a mutex that is already locked is | ||
| Line 457: | Line 438: | ||
| Some mutex types also guarantee failing((returning '' | Some mutex types also guarantee failing((returning '' | ||
| - | Example | + | An example |
| + | < | ||
| <code c> | <code c> | ||
| pthread_mutexattr_t recursiveAttrs; | pthread_mutexattr_t recursiveAttrs; | ||
| Line 465: | Line 447: | ||
| pthread_mutex_init(& | pthread_mutex_init(& | ||
| </ | </ | ||
| + | < | ||
| To lock a mutex one can use either of: | To lock a mutex one can use either of: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| '' | '' | ||
| < | < | ||
| \\ | \\ | ||
| - | < | + | < |
| '' | '' | ||
| < | < | ||
| Line 489: | Line 472: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 496: | Line 479: | ||
| ~~Exercise.# | ~~Exercise.# | ||
| - | Use a mutex to fix the program below so that every '' | + | Use a mutex to fix the programs from the two examples in the section |
| - | identical letters. | + | [[# |
| - | < | + | |
| - | <code c letters.c> | + | |
| - | #include < | + | |
| - | #include < | + | |
| - | + | ||
| - | char buffer[1000]; | + | |
| - | + | ||
| - | void *writer(void *arg) { | + | |
| - | while (1) | + | |
| - | for (char letter = ' | + | |
| - | for (int i = 0; i < 999; ++i) | + | |
| - | buffer[i] = letter; | + | |
| - | } | + | |
| - | + | ||
| - | int main() { | + | |
| - | pthread_t thread; | + | |
| - | pthread_create(& | + | |
| - | pthread_detach(thread); | + | |
| - | + | ||
| - | while (getchar() != EOF) | + | |
| - | printf(" | + | |
| - | + | ||
| - | return 0; | + | |
| - | } | + | |
| - | </ | + | |
| - | < | + | |
| < | < | ||
| + | < | ||
| ~~Exercise.# | ~~Exercise.# | ||
| The program below accesses a linked list from multiple threads. | The program below accesses a linked list from multiple threads. | ||
| Add mutexes to functions which names start with '' | Add mutexes to functions which names start with '' | ||
| no longer crashes. | no longer crashes. | ||
| - | < | + | < |
| + | ++++ Source code for this exercise: | | ||
| + | < | ||
| <code c linkedlist.c> | <code c linkedlist.c> | ||
| #include < | #include < | ||
| Line 627: | Line 587: | ||
| </ | </ | ||
| < | < | ||
| + | ++++ | ||
| ~~Exercise.# | ~~Exercise.# | ||
| Add to the program from the previous exercise a thread executing the following | Add to the program from the previous exercise a thread executing the following | ||
| function, and amend the code so that it neither crashes nor deadlocks: | function, and amend the code so that it neither crashes nor deadlocks: | ||
| - | < | + | < |
| <code c linkedlist.c> | <code c linkedlist.c> | ||
| void *beheader(void *arg) { | void *beheader(void *arg) { | ||
| Line 657: | Line 618: | ||
| \\ | \\ | ||
| To create a rwlock one has to create a '' | To create a rwlock one has to create a '' | ||
| - | it with either '' | + | it with either '' |
| \\ | \\ | ||
| A read-write lock can be locked with: | A read-write lock can be locked with: | ||
| - | * '' | + | * '' |
| - | * '' | + | * '' |
| < | < | ||
| \\ | \\ | ||
| - | To unlock a rwlock (locked in any mode), '' | + | To unlock a rwlock (locked in any mode), '' |
| + | |||
| + | |||
| + | ===== [extra] Concurrency, | ||
| + | |||
| + | The aim of __[[https:// | ||
| + | is to take advantage of the fact that a computer can execute two or more sequences | ||
| + | of instructions at the same time (literally). | ||
| + | \\ | ||
| + | One needs at least two physical threads (for instance, two CPU cores) for this. | ||
| + | |||
| + | __[[https:// | ||
| + | when two processes / threads want to use the same resource (say, the same variable) | ||
| + | at the same time. | ||
| + | |||
| + | __Concurrent programming__ deals with processes / threads that contend on some | ||
| + | resources (and run either interleaved or in parallel). | ||
| + | \\ | ||
| + | Concurrency also concerns computers that have one physical thread (a single CPU | ||
| + | core) whenever they can switch context from one process / thread to another. | ||
| + | |||
| + | When two processes / threads access the same data and at least one of the | ||
| + | accesses modifies the data, then the __[[https:// | ||
| + | occurs – the result of the computation depends upon a chance. | ||
| + | \\ | ||
| + | Race conditions often lead to incorrect state of the data and thus introduce | ||
| + | bugs (that are usually hard to pinpoint). | ||
| + | |||
| + | Regions of code that must be executed without being interleaved among each other | ||
| + | are called __[[https:// | ||
| + | At most one process may be at a time inside inside a critical section. | ||
| + | |||
| + | When a process P wants to enter a critical section, and another process Q is | ||
| + | executing a critical section, then P must __wait__ (aka __block__) until Q exits | ||
| + | the critical section. | ||
| + | |||
| + | When a number of processes / threads are waiting on synchronisation primitives | ||
| + | (say, blocked by '' | ||
| + | could wake the ones waiting (say, execute '' | ||
| + | the waiting ones, then a __[[https:// | ||
| + | occurred. | ||
| + | \\ | ||
| + | A deadlock usually means that the processing stops. | ||
| + | |||
| + | < | ||
| + | It is also possible to run into a state when processes don't stop, but can' | ||
| + | progress either. This is called a __livelock__. Imagine the following code: | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | <code c> | ||
| + | /* x, y and mtx are shared */ | ||
| + | /* the programmer believed that eventually x equals y, but now x==1, y==0 and only those two processes run: */ | ||
| + | /* Thread 1 */ │ /* Thread 2 */ | ||
| + | char loop = 1; │ char loop = 1; | ||
| + | while(loop) { | ||
| + | pthread_mutex_lock(& | ||
| + | if (*x==0 && *y==0) { | ||
| + | /* do business logic */ | ||
| + | loop = 0; | ||
| + | } | ||
| + | pthread_mutex_unlock(& | ||
| + | } | ||
| + | </ | ||
| + | </ | ||
| + | < | ||
| + | |||
| + | __Fairness__ describes whether all processes / threads have equal chance to enter | ||
| + | the critical section. | ||
| + | Sometimes priorities are intentionally given to selected processes / threads. | ||
| + | Incorrect implementation of priorities may lead to | ||
| + | __[[https:// | ||
| + | \\ | ||
| + | If the algorithm is completely unfair for some process / thread, it can | ||
| + | __[[https:// | ||
| + | never entering the critical section on contention. | ||
| + | |||
| + | ===== Critical sections, deadlocks ===== | ||
| + | |||
| + | ~~Exercise.# | ||
| + | |||
| + | ~~Exercise.# | ||
| + | |||
| + | ~~Exercise.# | ||
| + | |||
| + | < | ||
| + | <code c> | ||
| + | … | ||
| + | 10│ struct myIO {FILE *in; FILE *out;}; | ||
| + | 11│ void* threadFunc(void *num); | ||
| + | … | ||
| + | 17│ struct { | ||
| + | 18│ | ||
| + | 19│ char text[256]; | ||
| + | 20│ } item[5]; | ||
| + | 21│ | ||
| + | 22│ int main(int argc, char **argv) { | ||
| + | … | ||
| + | 28│ for (intptr_t i = 1; i < 5; ++i) { | ||
| + | 29│ | ||
| + | 30│ | ||
| + | 31│ | ||
| + | 32│ } | ||
| + | 33│ | ||
| + | 34│ } | ||
| + | 35│ | ||
| + | 36│ void *threadFunc(void* numRaw) { | ||
| + | 37│ | ||
| + | 38│ | ||
| + | … | ||
| + | 48│ while (1) { | ||
| + | 49│ | ||
| + | 50│ | ||
| + | 51│ char line[1024]; | ||
| + | 52│ | ||
| + | 53│ | ||
| + | 54│ | ||
| + | 55│ int argOne = atoi(line); | ||
| + | … | ||
| + | 61│ char *argTwoTxt = strchr(line, | ||
| + | 62│ | ||
| + | 63│ if (!argTwoTxt) { | ||
| + | 65│ | ||
| + | 66│ | ||
| + | 67│ | ||
| + | 68│ | ||
| + | 69│ } | ||
| + | 70│ | ||
| + | 71│ | ||
| + | 72│ char *e; | ||
| + | 73│ int argTwo = strtol(argTwoTxt, | ||
| + | 74│ | ||
| + | 75│ if (!*e && argTwo < 5 && argTwo >= 0 && argOne != argTwo) { | ||
| + | 77│ | ||
| + | 78│ | ||
| + | 79│ | ||
| + | 80│ | ||
| + | 81│ | ||
| + | 82│ | ||
| + | 83│ } else { | ||
| + | 85│ | ||
| + | 86│ | ||
| + | 87│ | ||
| + | 88│ | ||
| + | 89│ | ||
| + | 90│ } | ||
| + | 91│ } | ||
| + | 92│ } | ||
| + | … | ||
| + | </ | ||
| + | < | ||
| + | |||
| + | ===== Thread-specific data, thread local storage [extra] ===== | ||
| + | |||
| + | In C, data items can have different storage duration and different linkage. | ||
| + | Until C11, there existed only static, automatic or allocated storage durations. | ||
| + | Roughly: | ||
| + | \\ | ||
| + | • static = global variables and static variables defined inside functions, | ||
| + | \\ | ||
| + | • automatic = items on stack, automatically allocated inside functions (' | ||
| + | \\ | ||
| + | • allocated = items on heap, explicitly allocated and freed. | ||
| + | \\ | ||
| + | None of these cover a case when one wants a static (" | ||
| + | a separate value for each thread. | ||
| + | Such storage duration is called [[https:// | ||
| + | and a coupe of ways to create thread-local items are presented in this section. | ||
| + | |||
| + | ==== POSIX Thread-specific data ==== | ||
| + | |||
| + | One can create //keys// (that may be understood as identifiers of variables) and | ||
| + | then associate, for each thread separately, a value for a key. | ||
| + | To this end the following functions can be used: | ||
| + | \\ | ||
| + | < | ||
| + | <a href=" | ||
| + | '' | ||
| + | < | ||
| + | \\ | ||
| + | < | ||
| + | '' | ||
| + | \\ | ||
| + | '' | ||
| + | < | ||
| + | \\ | ||
| + | Each key is initially associated with a '' | ||
| + | |||
| + | < | ||
| + | ++++An example code that creates a key, stores and retrieves the value from two threads.| | ||
| + | <code c> | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | |||
| + | pthread_key_t mySpecificVal; | ||
| + | |||
| + | void printMine() { | ||
| + | printf(" | ||
| + | } | ||
| + | |||
| + | void *routine(void *arg) { | ||
| + | char *text = malloc(4); | ||
| + | strcpy(text, | ||
| + | pthread_setspecific(mySpecificVal, | ||
| + | printMine(); | ||
| + | return NULL; | ||
| + | } | ||
| + | |||
| + | int main() { | ||
| + | char *text = malloc(4); | ||
| + | strcpy(text, | ||
| + | |||
| + | pthread_key_create(& | ||
| + | pthread_setspecific(mySpecificVal, | ||
| + | |||
| + | pthread_t ti; | ||
| + | pthread_create(& | ||
| + | pthread_join(ti, | ||
| + | |||
| + | printMine(); | ||
| + | return 0; | ||
| + | } | ||
| + | </ | ||
| + | ++++ | ||
| + | < | ||
| + | |||
| + | ==== C thread-local storage ==== | ||
| + | |||
| + | Starting with C11, a new storage duration '' | ||
| + | '' | ||
| + | C23 '' | ||
| + | is a keyword with same meaning as the '' | ||
| + | Variables defined with this storage duration have unique value for each thread. | ||
| + | Notice that the '' | ||
| + | POSIX), but there is no way to provide a custom destructor (unlike in POSIX). | ||
| ===== POSIX condition variables ===== | ===== POSIX condition variables ===== | ||
| Line 679: | Line 877: | ||
| When one thread needs to execute some logic once a specific condition is fulfilled, | When one thread needs to execute some logic once a specific condition is fulfilled, | ||
| it should: | it should: | ||
| + | < | ||
| + | <WRAP group> | ||
| + | <WRAP half column> | ||
| - lock a mutex, | - lock a mutex, | ||
| - check the condition, | - check the condition, | ||
| - | - if the condition is false: | + | - while the condition is false: |
| - wait on a condition variable, | - wait on a condition variable, | ||
| - go back to step 2, | - go back to step 2, | ||
| - do the logic, | - do the logic, | ||
| - unlock the mutex. | - unlock the mutex. | ||
| + | </ | ||
| + | <WRAP half column> | ||
| + | < | ||
| + | <pre class=" | ||
| + | <span style=" | ||
| + | <span class=" | ||
| + | <span style=" | ||
| + | < | ||
| + | <span style=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| A thread that may change the state and thus affect the condition should: | A thread that may change the state and thus affect the condition should: | ||
| + | < | ||
| + | <WRAP group> | ||
| + | <WRAP half column> | ||
| - lock the mutex, | - lock the mutex, | ||
| - do its logic, | - do its logic, | ||
| - signal the condition variable, | - signal the condition variable, | ||
| - unlock the mutex((One can also signal the condition variable after unlocking the mutex.)). | - unlock the mutex((One can also signal the condition variable after unlocking the mutex.)). | ||
| + | </ | ||
| + | <WRAP half column> | ||
| + | < | ||
| + | <pre class=" | ||
| + | <span style=" | ||
| + | < | ||
| + | <span style=" | ||
| + | <span style=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| < | < | ||
| The example conditions include: | The example conditions include: | ||
| + | < | ||
| * a boolean flag is set; the flag indicates that another thread finished a part of the computation and the results can used, | * a boolean flag is set; the flag indicates that another thread finished a part of the computation and the results can used, | ||
| * a list of items is not empty; the list contains tasks to be done by this thread, | * a list of items is not empty; the list contains tasks to be done by this thread, | ||
| Line 703: | Line 933: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 711: | Line 941: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 719: | Line 949: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| < | < | ||
| Line 728: | Line 958: | ||
| \\ | \\ | ||
| < | < | ||
| - | <a href=" | + | <a href=" |
| '' | '' | ||
| '' | '' | ||
| Line 746: | Line 976: | ||
| \\ • The thread collecting items wastes CPU on busy waiting. Point the line that wastes CPU. | \\ • The thread collecting items wastes CPU on busy waiting. Point the line that wastes CPU. | ||
| \\ • Add condition variable(s) to amend the program. | \\ • Add condition variable(s) to amend the program. | ||
| - | < | + | < |
| <code c linkedlist.c> | <code c linkedlist.c> | ||
| #include < | #include < | ||
| Line 831: | Line 1061: | ||
| \\ | \\ | ||
| The API for these synchronisation constructs looks like a simplified POSIX API: | The API for these synchronisation constructs looks like a simplified POSIX API: | ||
| - | < | + | < |
| <code c> | <code c> | ||
| #include < | #include < | ||
os_cp/threads.1684876917.txt.gz · Last modified: (external edit)