Presentation is loading. Please wait.

Presentation is loading. Please wait.

Practical Session 3 Threads

Similar presentations


Presentation on theme: "Practical Session 3 Threads"— Presentation transcript:

1 Practical Session 3 Threads
Operating Systems Practical Session 3 Threads

2 Threads Executed within a process.
Allow multiple independent executions under the same process (container). Possible states: running, ready, blocked, terminated. In most of today’s operating systems, a process is created with at least one thread but may have more than one thread (multithreading). Once the process finishes execution, or is terminated by the operating system, it is no longer needed. The process is removed instantly or is moved to the "terminated" state.

3 Threads - Advantages Share open files, data structures, global variables, child processes, etc. Peer threads can communicate without using System calls. Threads are faster to create/terminate/switch than processes (have no resources attached). Parallelism which improves overall performance: A single core CPU and a substantial amount of computing and I/O Multiple cores Threads in the same process share: Process instructions Most data open files (descriptors) signals and signal handlers current working directory User and group id Each thread has a unique: Thread ID set of registers, stack pointer stack for local variables, return addresses signal mask priority Return value: errno

4 Threads - Disadvantages
Share open files, data structures, global variables, child processes, etc. No protection between threads – one can read/write/wipe out/corrupt the other’s data. Sending some signals (such as SIGSTOP) to a process affects all threads running within it.

5 Threads vs. Processes (“classic” approach – Linux’s clone results in some ambiguity)
shared data unique data shared code unique code shared open I/O unique open I/O shared signal table unique signal table unique stack unique PC unique registers unique state light context switch heavy context switch UNIX processes contain a signal mask that defines which signals can be delivered and which are blocked from delivery at any given time Signal handlers must be shared among all threads of a multithreaded application; however, each thread must have its own mask of pending and blocked signals (POSIX ).

6 Threads - motivation Dispatcher thread: while (TRUE) {
get_next_request(&buf); handoff_work(&buf); } Dispatcher Worker thread: while (TRUE) { wait_for_work(&buf); look_for_page_in_cache(&buf, &page); if (page_not_in_cache(&page)) read_page_from_disk(&buf, &page); return page(&page); } Workers Web page cache Dispatcher thread delivers the request to one of the worker threads. This way of communication wouldn’t be available if each worker is a different process. Also in case cache refers to a shared memory, threads are able to share the cache to speed up the application. Webpage request Why are threads better in this case? Example from “Modern Operating Systems”, 2nd Edition, pg. 88

7 Threads – some known Issues
Does the fork() command duplicate just the calling thread or all threads of the process? POSIX defines that only the calling thread is replicated in the child process. Solaris 10 defines fork1() and forkall() which attempt to better define the relation between fork and threads, however, this is not POSIX compliant. Many issues arise from using fork in a multithreaded code. Unless calling exec immediately after fork, try to avoid it! Does the exec() command replace the entire process? The entire process is replaced including all its threads.

8 User-level and Kernel-level Threads
Library P User space Kernel space P User space Kernel space (a) Pure user-level (b) Pure kernel-level

9 User-level and Kernel-level Threads
P User space Kernel space Threads Library P (c) Combined Source: Stallings, Operating Systems: Internals and design principles 7th ed.

10 User-level threads The kernel sees just the main thread of the process (all other threads that run within the process’ context are “invisible” to the OS). The user application – not the kernel – is responsible for scheduling CPU time for its internal threads within the running time scheduled for it by the kernel.

11 User-level threads (cont’d)
The kernel’s inability to distinguish between user level threads makes it difficult to design preemptive scheduling. When context switching is made directly towards the entire process, clock interrupts are usually used for this purpose. User level thread will usually have to voluntarily give up the CPU. If a thread makes a blocking system call, the entire process is blocked. Will only utilize a single CPU.

12 Kernel-level threads All threads are visible to the kernel.
The kernel manages the threads. The kernel schedules each thread within the time-slice of each process. The user cannot define the scheduling policy. Context switching is slower for kernel threads than for user-level threads. Because the kernel is aware of the threads, in multiple CPU machines, each CPU can run a different thread of the same process, at the same time.

13 User-level vs. kernel-level threads
User-level threads Visible to the kernel Invisible to the kernel Threads Kernel defined User defined Scheduling policy Preemptive Non-preemptive* Thread switching Slower, done by the kernel Faster, done by the runtime Context switch Block the single thread Block the whole process Blocking calls Held by the kernel Held by the process Thread table Switching between kernel-level threads takes longer: Switching take place in the kernel. Switching to kernel mode also takes time. In case of switching between threads of different processes. This requires storing the entire data and memory mapping of a process.

14 A tech. note on POSIX threads
When the first Unix and POSIX functions were designed, it was assumed that there will be a single thread of execution. Consider a naïve implementation of errno in a multi threaded environment for example. Hence, the need for reentrant functions. While this is supported by many standard functions, the compiler must be aware of the need for re-entrant functions: gcc –D_REENTRANT –lpthread … Reentrant functions are safe to call before a previous call has finished. The kernel is reentrant. Reentrancy is achieved by either using only local vars, or using locking mechanisms.

15 Threads in POSIX (pthreads)
int pthread_create( pthread_t* thread, pthread_attr_t* attr, void* (*start_func)(void*) , void* arg) Creates a new thread of control that executes concurrently with the calling thread. On success, the identifier of the newly created thread is stored in the location pointed by the thread argument, and a 0 is returned. On error, a non-zero error code is returned. attr specifies thread attributes that will be applied to the new thread (e.g. detached, scheduling-policy). Can be NULL (default attributes). start_func is pointer to the function the thread will start executing; start_func receives one argument of type void* and returns a void*. arg is the parameter to be given to func. attr - Set to NULL if default thread attributes are used. (else define members of the struct pthread_attr_t defined in bits/pthreadtypes.h) Attributes include: detached state (joinable? Default: PTHREAD_CREATE_JOINABLE. Other option: PTHREAD_CREATE_DETACHED) scheduling policy (real-time? PTHREAD_INHERIT_SCHED,PTHREAD_EXPLICIT_SCHED,SCHED_OTHER) scheduling parameter inheritsched attribute (Default: PTHREAD_EXPLICIT_SCHED Inherit from parent thread: PTHREAD_INHERIT_SCHED) scope (Kernel threads: PTHREAD_SCOPE_SYSTEM User threads: PTHREAD_SCOPE_PROCESS Pick one or the other not both.) guard size stack address (See unistd.h and bits/posix_opt.h _POSIX_THREAD_ATTR_STACKADDR) stack size (default minimum PTHREAD_STACK_SIZE set in pthread.h), pthread_t pthread_self() return this thread’s identifier.

16 Threads in POSIX (pthreads) – cont’d
int pthread_join( pthread_t th, void** thread_return ) Suspends the execution of the calling thread until the thread identified by th terminates. On success, the return value of th is stored in the location pointed by thread_return, and a 0 is returned. On error, a non-zero error code is returned. At most one thread can wait for the termination of a given thread. Calling pthread_join on a thread th on which another thread is already waiting for termination returns an error. th is the identifier of the thread that needs to be waited for. thread_return is pointer to the returned value of the th thread (can be NULL). Pthread_exit - Doesn’t terminate the whole process if called from the main function. Main function regards the thread’s main (run) function and not to “MAIN”. To allow other threads to continue execution, the main thread should terminate by calling pthread_exit() rather than exit(3). Performing a return from the start function of any thread other than the main thread results in an implicit call to pthread_exit(), using the function's return value as the thread's exit status. void pthread_exit( void* ret_val ) Terminates the execution of the calling thread. Doesn’t terminate the whole process if called from the main function. If ret_val is not null, then ret_val is saved, and its value is given to the thread who performed join on this thread; that is, it will be written to the thread_return parameter in the pthread_join call.

17 What can happen if we remove the join part?
Hello World! #include <pthread.h> #include <stdio.h> void *printme() { printf("Hello World!\n"); return NULL; } void main() { pthread_t tcb; void *status; if (pthread_create(&tcb, NULL, printme, NULL) != 0) { perror("pthread_create"); exit(1); if (pthread_join(tcb, &status) != 0) { perror("pthread_join"); When compiling a multi-threaded app: gcc –D_REENTRANT –o myprog myprog.c –lpthread What can happen if we remove the join part?

18 Example A – Version 1 void *printme(void *id) { int *i; i = (int *)id; printf("Hi. I'm thread %d\n", *i); return NULL; } void main() { int i, vals[4]; pthread_t tids[4]; void *retval; for (i = 0; i < 4; i++) { vals[i] = i; pthread_create(tids+i, NULL, printme, vals+i); printf("Trying to join with tid%d\n", i); pthread_join(tids[i], &retval); printf("Joined with tid%d\n", i);

19 Example A – Version 1 possible output
Trying to join with tid0 Hi. I'm thread 0 Hi. I'm thread 1 Hi. I'm thread 2 Hi. I'm thread 3 Joined with tid0 Trying to join with tid1 Joined with tid1 Trying to join with tid2 Joined with tid2 Trying to join with tid3 Joined with tid3

20 Example A – Version 2 void *printme(void *id) { int *i; i = (int *)id; printf("Hi. I'm thread %d\n", *i); pthread_exit(NULL); } void main() { int i, vals[4]; pthread_t tids[4]; void *retval; for (i = 0; i < 4; i++) { vals[i] = i; pthread_create(tids+i, NULL, printme, vals+i); printf("Trying to join with tid%d\n", i); pthread_join(tids[i], &retval); printf("Joined with tid%d\n", i); Performing a return from the start function of any thread other than the main thread results in an implicit call to pthread_exit(), using the function's return value as the thread's exit status.

21 Example A – Version 2 possible output
Trying to join with tid0 Hi. I'm thread 0 Hi. I'm thread 1 Hi. I'm thread 2 Hi. I'm thread 3 Joined with tid0 Trying to join with tid1 Joined with tid1 Trying to join with tid2 Joined with tid2 Trying to join with tid3 Joined with tid3

22 Example A – Version 3 void *printme(void *id) { int *i; i = (int *)id; printf("Hi. I'm thread %d\n", *i); pthread_exit(NULL); } void main() { int i, vals[4]; pthread_t tids[4]; void *retval; for (i = 0; i < 4; i++) { vals[i] = i; pthread_create(tids+i, NULL, printme, vals+i); printf("Trying to join with tid%d\n", i); pthread_join(tids[i], &retval); printf("Joined with tid%d\n", i);

23 Example A – Version 3 output
Hi. I'm thread 0 Hi. I'm thread 1 Hi. I'm thread 2 Hi. I'm thread 3 To allow other threads to continue execution, the main thread should terminate by calling pthread_exit() rather than exit(3). If the main thread calls pthread_exit(), the process will continue executing until the last thread terminates or the whole process is terminated

24 Example A – Version 4 void *printme(void *id) { int *i = (int *)id; sleep(5); printf("Hi. I'm thread %d\n", *i); pthread_exit(NULL); } int main() { int i, vals[4]; pthread_t tids[4]; void *retval; for (i = 0; i < 4; i++) { vals[i] = i; pthread_create(tids+i, NULL, printme, vals+i); return 0;

25 Example A – Version 4 possible output
No Output!

26 Example A – Version 5 void *printme(void *id) { int *i; i = (int *)id; printf("Hi. I'm thread %d\n", *i); exit(0); } main() { int i, vals[4]; pthread_t tids[4]; void *retval; for (i = 0; i < 4; i++) { vals[i] = i; pthread_create(tids+i, NULL, printme, vals+i); printf("Trying to join with tid%d\n", i); pthread_join(tids[i], &retval); printf("Joined with tid%d\n", i); pthread_exit(NULL);

27 Example A – Version 5 possible output
Trying to join with tid0 Hi. I'm thread 0

28 Midterm – 2006 בעץ תהליכים כל קודקוד מייצג תהליך. קודקוד g מצביע על קודקוד q אם"ם g הוא אבא של q, כלומר אם g יצר את q. g q (א) שרטטו את עץ התהליכים הנוצר ע"י הרצת הקוד הבא בשפת C. (תנו שמות שרירותיים לתהליכים הנוצרים.) 1. int x; 2. fork(); 3. x = fork(); 4. if(x != 0) fork(); 7. printf(“pid= %d”,getpid());

29 Midterm – 2006 (cont’d) פתרון (א): 2 3 6 4 5 1

30 Midterm – 2006 (cont’d) ב. מהו הפלט של הרצת התוכנית מסעיף א'? האם זהו הפלט היחיד האפשרי? הסבירו. (עד 3 שורות). פתרון (ב): שישה מספרים גדולים מ 0. הפלט אינו יחיד, כל שישה מספרים נכונים. ג. אם בין שורות 4 ו 6 נוסיף את השורה: kill(x, SIGINT); מה ישתנה בעץ התהליכים ובפלט?   פתרון (ג): התהליכים 3 ו 4 ימותו. הפלט עשוי להישאר זהה או שיודפסו רק 5 מספרים או רק 4 מספרים.

31 Midterm – 2006 (cont’d) ד. האם ייתכן תסריט שבו לאחר השינוי נקבל פלט זהה לפלט אותו קיבלנו לפני השינוי? אם כן, מהו תסריט זה? אם לא, נמקו מדוע לא יתכן כי נקבל פלט זהה. פתרון (ד): כן, יתכן כזה תסריט. נניח שהמתזמן נותן לכל בן שנוצר ב- fork לרוץ עד אשר הוא מסיים, הרי שכל אחד יספיק להגיע לשורת ההדפסה.

32 Midterm – 2006 (cont’d) ה. נניח כי תידרשו לכתוב תוכנית מרובת threads, שתרוץ על מערכת הפעלה התומכת גם ב-user threads וגם ב-kernel threads. באיזו אפשרות תבחרו אם ה-threads מבצעים פעולות I/O רבות? הסבירו (עד 3 שורות). הסבירו באילו נסיבות (כלומר, עבור איזה סוג תוכנית) הייתם בוחרים באפשרות השנייה. פתרון (ה): פעולת I/O גורמת ל user threads כולם לעבור ל blocking שכן מערכת ההפעלה לא מודעת לקיומם ולכן לא סביר לבחור באופציה זו במקרה של ריבוי פעולות I/O. לעומת זאת, כדאי לבחור ב user threads במקרים בהם רוצים למשל שליטה מלאה על התזמון. בנוסף, אם מדובר במערכת עם יחסית מעט מעבדים נעדיף user threads שכן החלפה ביניהם היא מהירה יותר.

33 Thread-specific data Programs often need global or static variables that have different values in different threads: Thread-specific data (TSD). Each thread possesses a private memory block, the TSD area. This area is indexed by TSD keys (Map). TSD keys are common to all threads, but the value associated with a given TSD key can be different in each thread. Defined in POSIX.

34 Thread-specific data (cont’d)
Question: Why can’t we achieve this by using regular variables? Because threads share one memory space. Usage examples: Separate log for each thread. errno variable. C99 standard defines __thread storage specifier, which specifies that a variable should be distinct per thread. Supported in GCC since version 3.3.

35 Thread-specific data (cont’d)
int pthread_key_create(pthread_key_t* key, void (*destr_func)(void*)) Allocates a new TSD key. Return 0 on success and a non-zero error code on failure. key the key is stored in the location pointed to by key. destr_func if not NULL, specifies a destructor function associated with the key. When a thread terminates via pthread_exit, destr_func is called with arguments – the value associated with the key in that thread. The order in which destructor functions are called at thread termination time is unspecified. int pthread_key_delete(pthread_key_t key) Deallocates a new TSD key. Return 0 on success and a non-zero error code on failure. It does not check whether non-NULL values are associated with that key in the currently executing threads, nor call the destructor function associated with the key. key the key of the value to delete. The pthread_key_delete() function deletes a thread-specific data key previously returned by pthread_key_create(). The thread-specific data values associated with key need not be NULL at the time pthread_key_delete() is called. It is the responsibility of the application to free any application storage or perform any cleanup actions for data structures related to the deleted key or associated thread-specific data in any threads; this cleanup can be done either before or after pthread_key_delete() is called. Any attempt to use key following the call to pthread_key_delete() results in undefined behaviour. The pthread_key_delete() function is callable from within destructor functions. No destructor functions will be invoked by pthread_key_delete(). Any destructor function that may have been associated with key will no longer be called upon thread exit. For an application to know that it is safe to delete a key, it has to know that all the threads that might potentially ever use the key do not attempt to use it again. For example, it could know this if all the client threads have called a cleanup procedure declaring that they are through with the module that is being shut down, perhaps by setting a reference count to zero.

36 Thread-specific data (cont’d)
int pthread_setspecific(pthread_key_t key, const void* pointer) Changes the value associated with key in the calling thread, storing the given pointer instead. void* pthread_getspecific(pthread_key_t key) Returns the value currently associated with key in the calling thread, or NULL on error.

37 TSD Usage example Suppose, for instance, that your application divides a task among multiple threads. For audit purposes, each thread is to have a separate log file, in which progress messages for that thread's tasks are recorded. The thread-specific data area is a convenient place to store the file pointer for the log file for each individual thread.

38 #include <malloc.h>
#include <pthread.h> #include <stdio.h> // The key used to associate a log file pointer with each thread. static pthread_key_t thread_log_key; // Write MESSAGE to the log file for the current thread. void write_to_thread_log(const char* message) { FILE* thread_log = (FILE*)pthread_getspecific (thread_log_key); fprintf(thread_log, "%s\n", message); } // Close the log file pointer THREAD_LOG. void close_thread_log (void* thread_log) fclose((FILE*) thread_log);

39 void* thread_function (void* args) { char thread_log_filename[20];
FILE* thread_log; sprintf(thread_log_filename,"thread%d.log",(int) pthread_self()); thread_log = fopen (thread_log_filename, "w"); pthread_setspecific (thread_log_key, thread_log); write_to_thread_log ("Thread starting."); /* Do work here... */ return NULL; } int main () int i; pthread_t threads[5]; pthread_key_create (&thread_log_key, close_thread_log); for (i = 0; i < 5; ++i) pthread_create (&(threads[i]), NULL, thread_function, NULL); pthread_join (threads[i], NULL); return 0; sprintf writes formatted data to string. 

40 Threads in XV6 XV6 doesn’t support threads.
Only processes can be created using the fork() system call. Thread support can be added to XV6: Implementing the clone(…) system call. Configuring clone(…) to create a process with the same memory space as its parent process. Adding some basic threads functionality (create, terminate, join…)


Download ppt "Practical Session 3 Threads"

Similar presentations


Ads by Google