1. Thread A thread is usually called a lightweight process. Although this method is somewhat simplified, it is helpful to understand the concept of thread. Because threads and processes are relatively small, threads consume less CPU resources. Processes often need their own resources, However, resources can be shared between threads, which saves more memory. The Mach thread allows programmers to write programs that run concurrently, These programs can run either on a single processor or on a multi-processor machine. In addition, in a single-processor environment, When the application executes operations that may cause congestion and delay, the thread can improve the efficiency. One of the reasons for using multithreading is that compared with processes, it is a very "frugal" multi-task operation method. We know that in Linux, To start a new process, you must allocate it an independent address space and create a large number of data tables to maintain its code segment, stack segment, and data segment, This is an "expensive" way of multitasking. Multiple threads running in a process use the same address space for each other, To share most of the data, it takes much less space to start a thread than to start a process, The time required for switching between threads is much less than the time required for switching between processes. According to statistics, in general, the overhead of a process is about one The thread overhead is about 30 times. Of course, in a specific system, this data may be quite different. The second reason for using multithreading is the convenient communication mechanism between threads. For different processes, they have independent data space, Data transmission can only be performed through communication, which is time-consuming and inconvenient. The thread is not, Because the threads in the same process share data space, the data of one thread can be directly used by other threads, which is fast and convenient. Of course, data sharing also brings about other problems. Some Variables cannot be modified by two threads at the same time, and some subroutines declare static. The data is more likely to bring catastrophic impact to multi-threaded programs. These are the most important points of attention when writing multi-threaded programs. In addition to the advantages mentioned above, multi-threaded programs, as a multi-task and concurrent working method, certainly have the following advantages: 1) Improve application response. This is especially meaningful for graphic interfaces. When an operation takes a long time, the entire system will wait for this operation, At this time, the program does not respond to keyboard, mouse, and menu operations. Instead, it uses multithreading technology to place time-consuming operations (time consuming) in a new thread, This can be avoided. 2) Make the multi-CPU system more effective. The operating system ensures that different threads run on different CPUs when the number of threads is not greater than the number of CPUs. 3) Improve the program structure. A long and complex process can be divided into multiple threads to become several independent or semi-independent running parts, Such a program is easy to understand and modify. 2. Introduction to thread programming Use the subfunction pthread_create to create a new thread. It has four parameters: a thread variable used to save the thread, a thread attribute, The function to be called and the parameter of this function during thread execution. For example: Pthread_ta_thread; Pthread_attr_ta_thread_attribute; Void thread_function (void * argument ); Char * some_argument; Pthread_create (& a_thread, a_thread_attribute, (void *) & thread_function, (Void *) & some_argument ); The thread attribute only specifies the minimum size of the stack to be used. In future programs, other values can be specified for thread attributes, But now most programs can use the default value. Unlike the processes created using the fork system in Unix systems, they use the same execution point as their parent processes, The parameter used by the thread in pthread_create indicates the function to be executed. Now we can compile the first program. We compile a multi-threaded application and print "Hello wo r l d" in the standard output ". First, we need two thread variables, which can be called when a new thread starts execution. We also need to specify the information that each thread should print. One way is to separate the strings to be printed and give each thread a string as the starting parameter. See the following code: Void print_message_function (void * PTR ); Main () { Pthread_t thread1, thread2; Char * message1 = "hello "; Char * message2 = "wo r l d "; Pthread_create (& thread1, pthread_attr_default, (Void *) & print_message_function, (void *) message1 ); Pthread_create (& thread2, pthread_attr_default, (Void *) & print_message_function, (void *) message2 ); Exit (0 ); } Void print_message_function (void * PTR) { Char * message; Message = (char *) PTR; Printf ("% s", message ); } The program creates the first thread by calling pthread_create and uses "hello" as its startup parameter. The parameter of the second thread is "world ". When the first thread starts execution, it uses the "hello" parameter to execute the function print_message_function. It prints "hello" in the standard output ", Then, call the function. The thread terminates when it leaves its initialization function. Therefore, the first thread terminates after "hello" is printed. When the second thread executes, it prints "world" and then terminates. But this program has two major defects. The first and most important thing is that the thread is executed simultaneously. In this way, the first thread cannot execute the print statement first. So you may see "World Hello" on the screen ", Instead of "Hello World ". Note that the exit call is used by the parent thread in the main program. In this way, if the parent thread calls the print statement in two subthreads If exit is called before, no output is printed. This is because the exit function will exit the process and release the task at the same time, so all threads are ended. All other threads will be terminated when exit is called by any thread (no matter the parent thread or sub-thread. If you want the threads to terminate separately, you can use the pthread_exit function. We can use one way to make up for this defect. We can insert a latency program in the parent thread to give the child thread enough time to complete the printing call. Similarly, Before the second thread is called, a latency program is inserted to ensure that the first thread completes the task before the second thread is executed. Void print_message_function (void * PTR ); Main () { Pthread_t thread1, thread2; Char * message1 = "hello "; Char * message2 = "wo r l d "; Pthread_create (& thread1, pthread_attr_default, (Void *) & print_message_function, (void *) message1 ); Sleep (10 ); Pthread_create (& thread2, pthread_attr_default, (Void *) & print_message_function, (void *) message2 ); Sleep (10 ); Exit (0 ); } Void print_message_function (void * PTR) { Char * message; Message = (char *) PTR; Printf ("% s", message ); Pthread_exit (0 ); } Does this meet our requirements? This is not the case because delayed execution of synchronization by time is unreliable. The situation and a distributed program and Resources are shared in the same way. The shared resources are standard output devices, and the distributed computing program has three threads. In fact, there is another error. The sleep function and e x I t function are also related to processes. When the thread calls sleep, The whole process is in sleep state, that is, all three threads are in sleep state. In this way, we have not actually solved any problems. The function to sleep a thread is pthread_delay_np. For example, to sleep a thread for 2 seconds, run the following program: struct timespec delay; Delay. TV _sec = 2; Delay. TV _nsec = 0; Pthread_delay_np (& delay ); } 3. Thread Synchronization POSIX provides two methods for thread synchronization: mutex and conditional variable. Mutex is a simple locking method to control access to shared resources. We can create a read/write program that shares a shared buffer and uses mutex to control access to the buffer. Void reader_function (void ); Void writer_function (void ); Char Buf e r; Int buffer_has_item = 0; Pthread_mutex_t mutex; Struct timespec delay; Main () { Pthread_t reader; Delay. TV _sec = 2; Delay. TV _nsec = 0; Pthread_mutex_init (& mutex, pthread_mutexattr_default ); Pthread_create (& reader, pthread_attr_default, (void *) & reader_function, N u L ); Writer_function () Void writer_function (void) { While (1) { Pthread_mutex_lock (& mutex ); If (buffer_has_item = 0) { Buffer = make_new_item (); Buffer_has_item = 1; } Pthread_mutex_unlock (& mutex ); Pthread_delay_np (& delay ); } } Void reader_function (void) { While (1) { Pthread_mutex_lock (& mutex ); If (buffer_has_item = 1) { Consume_item (buffer ); Buffer_has_item = 0; } Pthread_mutex_unlock (& mutex ); Pthread_delay_np (& delay ); } } In the above program, we assume that the buffer can only save one message, so that the buffer has only two States, one information or no information. Latency is used to prevent a thread from occupying mutex forever. However, the disadvantage of mutex is that it has only two States: Locked and unlocked. POSIX conditional variables are implemented by allowing threads to block and wait for signals from another thread, This makes up for the lack of mutex. When a signal is received, the blocking thread will be aroused and attempt to obtain the related mutex lock. 4. semaphores A semaphore is a counter that can be used to control multiple processes to access shared resources. It is often used as a locking mechanism to prevent a process from accessing shared resources, Another process also accesses the same resource. I will explain it in detail here, but I have spent a lot of time looking for this information !! The following describes the data structure involved in semaphores. 1. semid_ds, the data structure in the kernel Like message queue, the system kernel stores an internal data structure for each semaphore set in the kernel address space. The prototype of the data structure is semid_ds. It is defined as follows in Linux/SEM. h:
/* One Semid data structure for each set of semaphores in the system .*/ Structsemid_ds { Structipc_permsem_perm;/* permissions .. seeipc. H */ Time_tsem_otime;/* Last semop time */ Time_tsem_ctime;/* last change time */ Structsem * sem_base;/* PTR to first semaphore in array */ Structwait_queue * eventn; Structwait_queue * eventz; Structsem_undo * undo;/* undo requestson this array */ Ushortsem_nsems;/* No. of semaphores in array */ };
Sem_perm is an instance of the data structure ipc_perm defined in Linux/IPC. h. It stores information with the access permission of the semaphore set, And information about the semaphore set creator. Sem_otime the time when the last semop () operation was performed. The time when sem_ctime last modified the data structure. The sem_base pointer to the first semaphore in the array. Number of incomplete requests in the sem_undo array. The number of semaphores in the sem_nsems semaphore set (array.
2. Data Structure SEM in the kernel The data structure semid_ds contains a pointer to the semaphore array. Each element in this array is Data Structure SEM. It is also defined in Linux/SEM. h: /* One semaphore structure for each semaphore in the system .*/ Structsem { Eclipsempid;/* PID of LAS toperation */ Ushortsemval;/* Current Value */ Ushortsemncnt;/* num procs awaiting increase in semval */ Ushortsemzcnt;/* num procs awaiting semval = 0 */ }; The PID (process ID) of the last operation of sem_pid ). The current value of sem_semval. The number of processes that sem_semncnt waits for resources. The number of processes that sem_semzcnt waits for resources to be completely idle. The above is the (P/V) operation that we often use in the program. P operation (representing Dutch proberen means to try ): Wait (wait) a traffic signal. This operation tests the value of this traffic signal. If it is less than 0 | equal to 0, it will wait (blocking ), Once the value increases, it will be reduced by 1.
V Operation (representing Dutch verhogen means adding ): Post a signal lamp. This operation adds the value of the signal lamp to 1.
This section describes the P/V Operations. The following describes some methods for semaphores: Semget ()
We can use the system to call semget () to create a new semaphore set or access an existing semaphore set: System Call: semget (); Prototype: intsemget (key_t key, int nsems, int semflg ); Return Value: If successful, the IPC identifier of the semaphore set is returned. -1: errno = eaccess (no permission) is returned if the request fails) Eexist (the semaphore set already exists and cannot be created) Eidrm (semaphore set deleted) Enoent (the semaphore set does not exist and ipc_creat is not used) Enomem (not enough memory to create a new semaphore set) The first parameter of the enospc system calling semget () is the keyword value (generally returned by the system calling ftok ). The system kernel compares this value with the keyword value of other semaphore sets in the system. The open and access operations are related to the content in the semflg parameter. Ipc_creat: If the semaphore set does not exist in the system kernel, a semaphore set is created. Ipc_excl when used together with ipc_creat, If the semaphore set already exists, the call fails. If ipc_creat is used separately, semget () either returns the identifier of the newly created semaphore set, Either return the semaphore identifier that already exists with the same keyword value in the system. If ipc_excl and ipc_creat are used together, Then either the identifier of the newly created semaphore set is returned or-1 is returned. It is meaningless to use ipc_excl independently. The nsems parameter indicates the number of semaphores that should be created in a new semaphores set. The maximum number of semaphores in a semaphores set is defined in Linux/SEM. h:
# Definesemmsl32/* <= 512maxnumofsemaphoresperid */ The following is a program for opening and creating a semaphore set: Export pen_semaphore_set (key_t keyval, int numsems) { Intsid; If (! Numsems) Return (-1 ); If (SID = semget (mykey, numsems, ipc_creat | 0660) =-1) { Return (-1 ); } Return (SID ); } }; Semop () System Call: semop (); Call prototype: int semop (INT Semid, struct sembuf * SOPs, unsign ednsops ); Return Value: 0. If yes. -1. If it fails: errno = e2big (nsops is greater than the maximum number of OPS) Eaccess (insufficient permissions) Eagain (ipc_nowait is used, but the operation cannot continue) Efault (the address to which SOPs points is invalid) Eidrm (semaphore set deleted) Eintr (receives other signals during sleep) Einval (the semaphore set does not exist or the Semid is invalid) Enomem (sem_undo is used, but there is not enough memory to create the required data structure) Erange (signal value out of range) The first parameter is the keyword value. The second parameter is the pointer to the array to be operated. The third parameter is the number of operations in the array. The SOPs parameter points to an array composed of sembuf. This array is defined in Linux/SEM. h:
/* Semop systemcall takes an array of these */ Structsembuf { Ushortsem_num;/* semaphore index in array */ Eclipsem_op;/* semaphore operation */ Eclipsem_flg;/* operation flags */ The number of semaphores to be processed by sem_num. The operation to be performed by sem_op. Sem_flg operation flag. If sem_op is a negative number, the semaphore will subtract its value. This is related to the resources controlled by semaphores. If ipc_nowait is not used, The called process goes to sleep until the resources controlled by the semaphore can be used. If sem_op is a positive number, the semaphore is added with its value. This means that the process releases resources controlled by semaphores. Finally, if sem_op is 0, the calling process calls sleep () until the semaphore value is 0. This is used when a process is waiting for completely idle resources. Semctl () System Call: semctl (); Prototype: int semctl (INT Semid, int semnum, int cmd, Union semunarg ); Return Value: If successful, it is a positive number. -1: errno = eaccess (insufficient permissions) Efault (the address to which Arg points is invalid) Eidrm (semaphore set deleted) Einval (the semaphore set does not exist or the Semid is invalid) Eperm (EUID does not have the CMD permission) Erange (signal value out of range)
The system calls semctl to perform control operations on the semaphore set. This is very similar to the system call of msgctl in the message queue. However, the two System Call parameters are slightly different. Because semaphores are generally used as a semaphores set, rather than a separate semaphores. Therefore, you must know not only the IPC keyword value, but also the specific semaphores in the semaphores set. The cmd parameter is used for both system calls, It is used to indicate the specific commands to be operated. The last parameter in the two system calls is different. In msgctl, The last parameter is a pointer to the data structure used in the kernel. We use this data structure to obtain information about message queues, And set or change the access permissions and users of the queue. However, the semaphore supports additional optional commands, which requires a more complex data structure. The first parameter that the system calls semctl () is the keyword value. The second parameter is the number of semaphores. The command that can be used in the CMD parameter is as follows: · IPC _ stat reads the data structure semid_ds of a semaphore set and stores it in the Buf parameter of semun. · Ipc_perm, an element in semid_ds, sets the data structure of the semaphore set. Its value is taken from the Buf parameter in semun. · IPC _ rmid: removes the semaphore set from the memory. · Getall is used to read the values of all semaphores in the semaphores set. · Getncnt returns the number of processes waiting for resources. · Getpid returns the PID of the last process that executes the semop operation. · Getval returns the value of a single semaphore In the semaphore set. · Getzcnt returns the number of processes that are waiting for completely idle resources. · Setall: Set the values of all semaphores in the semaphores set. · Setval: set the value of a separate semaphore In the semaphore set. The ARG parameter represents a semun instance. Semun is defined in Linux/SEM. h: /* Arg for semctl systemcils .*/ Unionsemun { Intval;/* value for setval */ Structsemid_ds * Buf;/* buffer for ipc_stat & ipc_set */ Ushort * array;/* array for getall & setall */ Structseminfo * _ Buf;/* buffer for ipc_info */ Void * _ pad;
Val is used when the setval command is executed. Buf is used in the ipc_stat/ipc_set command. Represents the data structure of semaphores used in the kernel. The pointer used by array when the getall/setall command is used. The following program returns the semaphore value. When the getval command is used, the last parameter in the call is ignored:
Intget_sem_val (intsid, intsemnum) { Return (semctl (SID, semnum, getval, 0 )); }
The following is an example of a practical application:
# Definemax_printers5 Printer_usage () { Int X; For (x = 0; x <max_printers; X ++) Printf ("Printer % d: % d/n/R", X, get_sem_val (SID, x )); }
The following program can be used to initialize a new signal value:
Void init_semaphore (INT Sid, int semnum, int initval) { Union semunsemopts; Semopts. Val = initval; Semctl (SID, semnum, setval, semopts ); } Note that the last parameter in the System Call semctl is a replica of the Union type, rather than a pointer to the Union type. |