POSIX thread details, part 1

Source: Internet
Author: User

Detailed description of condition Variables

At the end of the previous article, I described a special problem: If the thread is waiting for a specific condition to happen, how should it handle this situation? It can repeatedly lock and unlock mutex objects. Each time it checks the shared data structure to find a value. However, this is a waste of time and resources, and the efficiency of such busy queries is very low. The best way to solve this problem is to use pthread_cond_wait ()
To wait for special conditions to occur.

Understanding the role of pthread_cond_wait () is very important-it is the core of the POSIX thread signal sending system and the most difficult to understand.

First, let's consider the following situation: the thread locks the mutex object to view the linked list, but the list happens to be empty. This particular thread cannot do anything-it is designed to remove nodes from the list, but now there are no nodes. Therefore, it can only:

When a mutex object is locked, the thread will call pthread_cond_wait (& mycond, & mymutex ). Pthread_cond_wait () calls are quite complex, so we only execute one operation at a time.

The first thing pthread_cond_wait () does is to unlock the mutex object at the same time (so other threads can modify the linked list) and wait for the condition mycond to occur (when pthread_cond_wait () when receiving the "signal" from another thread, it will wake up ). The mutex object has been unlocked. Other threads can access and modify the linked list, and may add items.

At this time, the call to pthread_cond_wait () has not yet returned. Unlocking A mutex will happen immediately, but the waiting condition mycond is usually a blocking operation, which means the thread will sleep and will not consume the CPU cycle until it wakes up. This is exactly what we are looking forward. The thread will sleep until a specific condition occurs. During this period, no busy queries that waste CPU time will occur. From the thread point of view, it is only waiting for the pthread_cond_wait () call to return.

Now, let's continue to explain that another thread (called thread 2) locks mymutex and adds an item to the linked list. After the mutex object is unlocked, thread 2 immediately calls the pthread_cond_broadcast (& mycond) function ). After this operation, thread 2 will immediately wake up all threads waiting for the mycond condition variable. This means that the first thread (still in the pthread_cond_wait () call) will be awake now.

Now, let's take a look at what happened to the first thread. You may think that after thread 2 calls pthread_cond_broadcast (& mymutex), The pthread_cond_wait () of thread 1 will return immediately. That's not the case! In fact, pthread_cond_wait () will execute the last operation: Re-lock mymutex. Once pthread_cond_wait () locks the mutex object, it will return and allow thread 1 to continue execution. At that time, it can immediately check the list and view the changes it is interested in.

Stop and review!

That process is very complicated, so let's review it first. The first thread first calls:

    pthread_mutex_lock(&mymutex);

Then, it checks the list. Something of interest is not found, so it calls:

    pthread_cond_wait(&mycond, &mymutex);

Then, the pthread_cond_wait () call performs many operations before the return:

           pthread_mutex_unlock(&mymutex);

It unlocks mymutex and then enters sleep state, waiting for mycond to receive the POSIX thread "signal ". Once a "signal" is received (quotes are enclosed because we are not discussing traditional UNIX signals, but the signals from pthread_cond_signal () or pthread_cond_broadcast () calls), it will wake up. But pthread_cond_wait () does not return immediately -- it also needs to do one thing: Re-lock mutex:

         pthread_mutex_lock(&mymutex);

Pthread_cond_wait () knows the changes behind mymutex, so it continues to lock the mutex for us before returning.

Back to Top

Pthread_cond_wait () quiz

Now we have reviewed the call of pthread_cond_wait (). you should understand how it works. All operations performed in sequence by pthread_cond_wait () should be described. Try it. If you understand pthread_cond_wait (), the rest is quite easy, so read the above again until you remember it. Okay. Can you tell me when calling pthread_cond_wait ()?
Before, What status must the mutex object be in? After pthread_cond_wait () is returned, what is the status of the mutex object? The answer to both questions is "locked ". Now that you fully understand the pthread_cond_wait () call, let's continue to look at the simpler things-initialization and real sending and broadcasting processes. By that time, we will be familiar with the C code that contains the multi-threaded working queue.

Back to Top

Initialization and cleanup

A condition variable is a real data structure to be initialized. The initialization method is as follows. First, define or assign a condition variable, as shown below:

    pthread_cond_t mycond;

Then, call the following function for initialization:

    pthread_cond_init(&mycond,NULL);

Look, Initialization is complete! You need to destroy a condition variable before releasing it, as shown below:

    pthread_cond_destroy(&mycond);

It's easy. Next we will discuss the call to pthread_cond_wait.

Wait

Once the mutex object and condition variable are initialized, you can wait for a condition as follows:

    pthread_cond_wait(&mycond, &mymutex);

Note that the Code should logically contain mycond and mymutex. A specific condition can only have one mutex object, and the condition variable should indicate a special condition change for the mutex data "internal. A mutex can use many condition variables (such as cond_empty, cond_full, and cond_cleanup), but each condition variable can only have one mutex object.

Send signals and broadcast

Pay attention to sending signals and broadcasts. If the thread changes some shared data and wants to wake up all the waiting threads, use pthread_cond_broadcast to call it, as shown below:

    pthread_cond_broadcast(&mycond);

In some cases, the active thread only needs to wake up the first sleeping thread. Assume that you have added only one job to the queue. It is not polite to wake up another worker thread !) :

    pthread_cond_signal(&mycond);

This function only wakes up one thread. If POSIX thread standards allow you to specify an integer, you can wake up a certain number of sleeping threads, which is more perfect. Unfortunately, I was not invited to the meeting.

Working Group

I will demonstrate how to create a multi-threaded workgroup. In this solution, we have created many worker threads. Each thread checks wq ("work queue") to check whether there is any work to be done. If there is work to be completed, the thread will remove a node from the queue, execute the specific work, and then wait for the new work to arrive.

At the same time, the main thread is responsible for creating these worker threads, adding jobs to the queue, and then collecting all worker threads when it exits. You will encounter a lot of C code. Please be prepared!

Queue

The queue is required for two reasons. First, you need a queue to save the job. It also needs to be used to track the data structure of terminated threads. Remember the previous articles (refer to
Reference), have I mentioned the need to use pthread_join with specific process identifiers? You can use "Clear queue" (called "cq") to solve the problem of waiting.AnyThread terminated (this issue will be discussed in detail later ). The following is the standard queue code. Save this code to the files queue. h and queue. c:

Queue. h

/* queue.h** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.** Author: Daniel Robbins** Date: 16 Jun 2000*/typedef struct node {  struct node *next;} node;typedef struct queue {  node *head, *tail; } queue;void queue_init(queue *myroot);void queue_put(queue *myroot, node *mynode);node *queue_get(queue *myroot);

Queue. c

/* queue.c** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.** Author: Daniel Robbins** Date: 16 Jun 2000**** This set of queue functions was originally thread-aware.  I** redesigned the code to make this set of queue routines** thread-ignorant (just a generic, boring yet very fast set of queue** routines).  Why the change?  Because it makes more sense to have** the thread support as an optional add-on.  Consider a situation** where you want to add 5 nodes to the queue.  With the** thread-enabled version, each call to queue_put() would** automatically lock and unlock the queue mutex 5 times -- that's a** lot of unnecessary overhead.  However, by moving the thread stuff** out of the queue routines, the caller can lock the mutex once at** the beginning, then insert 5 items, and then unlock at the end.** Moving the lock/unlock code out of the queue functions allows for** optimizations that aren't possible otherwise.  It also makes this** code useful for non-threaded applications.**** We can easily thread-enable this data structure by using the** data_control type defined in control.c and control.h.  */#include <stdio.h>#include "queue.h"void queue_init(queue *myroot) {  myroot->head=NULL;  myroot->tail=NULL;}void queue_put(queue *myroot,node *mynode) {  mynode->next=NULL;  if (myroot->tail!=NULL)    myroot->tail->next=mynode;  myroot->tail=mynode;  if (myroot->:head==NULL)    myroot->head=mynode;}node *queue_get(queue *myroot) {  //get from root  node *mynode;  mynode=myroot->head;  if (myroot->head!=NULL)    myroot->head=myroot->head->next;  return mynode;}

Data_control code

I have not compiled a thread-safe queue routine. In fact, I have created a "data packaging" or "control" structure, which can be a data structure supported by any thread. Take a look at control. h:

Control. h

#include typedef struct data_control {  pthread_mutex_t mutex;  pthread_cond_t cond;  int active;} data_control;

Now you can see the data_control structure definition. The following is its visual representation:

The data_control structure used

The lock in the image represents a mutex object, which allows mutex access to the data structure. A yellow star represents a condition variable, which can sleep until the data structure discussed changes. On/Off indicates the integer "active", which tells the thread whether the data is active. In the code, I use the integer active as the flag to tell the working queue when it should be closed. The following is control. C:

Control. c

/* control.c** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.** Author: Daniel Robbins** Date: 16 Jun 2000**** These routines provide an easy way to make any type of** data-structure thread-aware.  Simply associate a data_control** structure with the data structure (by creating a new struct, for** example).  Then, simply lock and unlock the mutex, or** wait/signal/broadcast on the condition variable in the data_control** structure as needed.**** data_control structs contain an int called "active".  This int is** intended to be used for a specific kind of multithreaded design,** where each thread checks the state of "active" every time it locks** the mutex.  If active is 0, the thread knows that instead of doing** its normal routine, it should stop itself.  If active is 1, it** should continue as normal.  So, by setting active to 0, a** controlling thread can easily inform a thread work crew to shut** down instead of processing new jobs.  Use the control_activate()** and control_deactivate() functions, which will also broadcast on** the data_control struct's condition variable, so that all threads** stuck in pthread_cond_wait() will wake up, have an opportunity to** notice the change, and then terminate.*/#include "control.h"int control_init(data_control *mycontrol) {  int mystatus;  if (pthread_mutex_init(&(mycontrol->mutex),NULL))    return 1;  if (pthread_cond_init(&(mycontrol->cond),NULL))    return 1;  mycontrol->active=0;  return 0;}int control_destroy(data_control *mycontrol) {  int mystatus;  if (pthread_cond_destroy(&(mycontrol->cond)))    return 1;  if (pthread_cond_destroy(&(mycontrol->cond)))    return 1;  mycontrol->active=0;  return 0;}int control_activate(data_control *mycontrol) {  int mystatus;  if (pthread_mutex_lock(&(mycontrol->mutex)))    return 0;  mycontrol->active=1;  pthread_mutex_unlock(&(mycontrol->mutex));  pthread_cond_broadcast(&(mycontrol->cond));  return 1;}int control_deactivate(data_control *mycontrol) {  int mystatus;  if (pthread_mutex_lock(&(mycontrol->mutex)))    return 0;  mycontrol->active=0;  pthread_mutex_unlock(&(mycontrol->mutex));  pthread_cond_broadcast(&(mycontrol->cond));  return 1;}

Debugging time

A file is required before Debugging starts. Dbug. h:

Dbug. h

#define dabort() \ {  printf("Aborting at line %d in source file %s\n",__LINE__,__FILE__); abort(); }

This code is used to handle unrecoverable errors in the workgroup code.

Workgroup code

When talking about the Working Group Code, the following is:

Workcrew. c

#include <stdio.h>#include <stdlib.h>#include "control.h"#include "queue.h"#include "dbug.h"/* the work_queue holds tasks for the various threads to complete. */struct work_queue {  data_control control;  queue work;} wq;/* I added a job number to the work node.  Normally, the work node   would contain additional data that needed to be processed. */typedef struct work_node {  struct node *next;  int jobnum;} wnode;/* the cleanup queue holds stopped threads.  Before a thread   terminates, it adds itself to this list.  Since the main thread is   waiting for changes in this list, it will then wake up and clean up   the newly terminated thread. */struct cleanup_queue {  data_control control;  queue cleanup;} cq;/* I added a thread number (for debugging/instructional purposes) and   a thread id to the cleanup node.  The cleanup node gets passed to   the new thread on startup, and just before the thread stops, it   attaches the cleanup node to the cleanup queue.  The main thread   monitors the cleanup queue and is the one that performs the   necessary cleanup. */typedef struct cleanup_node {  struct node *next;  int threadnum;  pthread_t tid;} cnode;void *threadfunc(void *myarg) {  wnode *mywork;  cnode *mynode;  mynode=(cnode *) myarg;  pthread_mutex_lock(&wq.control.mutex);  while (wq.control.active) {    while (wq.work.head==NULL && wq.control.active) {      pthread_cond_wait(&wq.control.cond, &wq.control.mutex);    }    if (!wq.control.active)       break;    //we got something!    mywork=(wnode *) queue_get(&wq.work);    pthread_mutex_unlock(&wq.control.mutex);    //perform processing...    printf("Thread number %d processing job %d\n",mynode->threadnum,mywork->jobnum);    free(mywork);    pthread_mutex_lock(&wq.control.mutex);  }  pthread_mutex_unlock(&wq.control.mutex);  pthread_mutex_lock(&cq.control.mutex);  queue_put(&cq.cleanup,(node *) mynode);  pthread_mutex_unlock(&cq.control.mutex);  pthread_cond_signal(&cq.control.cond);  printf("thread %d shutting down...\n",mynode->threadnum);  return NULL;  }#define NUM_WORKERS 4int numthreads;void join_threads(void) {  cnode *curnode;  printf("joining threads...\n");  while (numthreads) {    pthread_mutex_lock(&cq.control.mutex);    /* below, we sleep until there really is a new cleanup node.  This       takes care of any false wakeups... even if we break out of       pthread_cond_wait(), we don't make any assumptions that the       condition we were waiting for is true.  */    while (cq.cleanup.head==NULL) {      pthread_cond_wait(&cq.control.cond,&cq.control.mutex);    }    /* at this point, we hold the mutex and there is an item in the       list that we need to process.  First, we remove the node from       the queue.  Then, we call pthread_join() on the tid stored in       the node.  When pthread_join() returns, we have cleaned up       after a thread.  Only then do we free() the node, decrement the       number of additional threads we need to wait for and repeat the       entire process, if necessary */      curnode = (cnode *) queue_get(&cq.cleanup);      pthread_mutex_unlock(&cq.control.mutex);      pthread_join(curnode->tid,NULL);      printf("joined with thread %d\n",curnode->threadnum);      free(curnode);      numthreads--;  }}int create_threads(void) {  int x;  cnode *curnode;  for (x=0; x<NUM_WORKERS; x++) {    curnode=malloc(sizeof(cnode));    if (!curnode)      return 1;    curnode->threadnum=x;    if (pthread_create(&curnode->tid, NULL, threadfunc, (void *) curnode))      return 1;    printf("created thread %d\n",x);    numthreads++;  }  return 0;}void initialize_structs(void) {  numthreads=0;  if (control_init(&wq.control))    dabort();  queue_init(&wq.work);  if (control_init(&cq.control)) {    control_destroy(&wq.control);    dabort();  }  queue_init(&wq.work);  control_activate(&wq.control);}void cleanup_structs(void) {  control_destroy(&cq.control);  control_destroy(&wq.control);}int main(void) {  int x;  wnode *mywork;  initialize_structs();  /* CREATION */    if (create_threads()) {    printf("Error starting threads... cleaning up.\n");    join_threads();    dabort();  }  pthread_mutex_lock(&wq.control.mutex);  for (x=0; x<16000; x++) {    mywork=malloc(sizeof(wnode));    if (!mywork) {      printf("ouch! can't malloc!\n");      break;    }    mywork->jobnum=x;    queue_put(&wq.work,(node *) mywork);  }  pthread_mutex_unlock(&wq.control.mutex);  pthread_cond_broadcast(&wq.control.cond);  printf("sleeping...\n");  sleep(2);  printf("deactivating work queue...\n");  control_deactivate(&wq.control);  /* CLEANUP  */  join_threads();  cleanup_structs();}

Code sorting

Now we can quickly sort the code. The first structure defined is called "WQ", which contains the data_control and queue header. The data_control structure is used to mediate access to the entire queue, including nodes in the queue. The next step is to define the actual work node. To make the Code conform to the example in this article, the job number is included here.

Then, create a clear queue. Annotations describe how it works. Now let's skip the threadfunc (), join_threads (), create_threads (), and initialize_structs () calls and directly jump to main (). The first thing to do is to initialize the structure-including initializing data_controls and queues, and activating work queues.

Precautions for clearing

Now initialize the thread. If you look at the create_threads () call, it seems that everything is normal-except one thing. Please note that we are allocating a clearing node and initializing its thread number and TID component. We will also clear the node and pass it as the initial independent variable to every new worker thread. Why?

This is because when a worker thread exits, it connects its clearing nodes to the clearing queue and terminates it. At that time, the main thread will detect this node (using conditional variables) in the clearing queue and remove this node from the queue. Because the TID (thread ID) is stored in the clearing node, the main thread can know exactly which thread has been terminated. Then, the main thread will call pthread_join (tid) and join the appropriate worker thread. If no record is made, the main thread needs to be connected to the worker thread in any order, probably in the creation order. Because the thread may not be terminated in this order, the main thread may wait for another thread to be connected when it has been connected to ten threads. Can you understand how this design decision enables code acceleration to be disabled (especially when hundreds of worker threads are used )?

Create a job

We have started the worker threads (they have completed the execution of threadfunc () and will discuss this function later). Now the main thread starts to insert the worker nodes into the work queue. First, it locks the control mutex object of wq, then allocates 16000 work packages, and inserts them into the queue one by one. After completion, pthread_cond_broadcast () will be called, so all sleeping threads will be awakened and started to execute the work. At this time, the main thread will sleep for two seconds, then release the working queue, and notify the working program thread to terminate the activity. The main thread then calls the join_threads () function to clear all worker threads.

Threadfunc ()

Now let's discuss threadfunc (), which is the code to be executed by all worker threads. When a worker thread starts, it immediately locks the mutex object of the work queue, obtains a worker node (if any), and then processes it. If no job is available, call pthread_cond_wait (). You will notice that this call is very important in a very compact while () loop. When waking up from the pthread_cond_wait () call, you must never think that the condition must have happened -- it
PossibleOr it may not happen. If this happens, that is, the thread is awakened incorrectly and the list is empty, the while loop will call pthread_cond_wait () again ().

If there is a worker node, we only print its job number, release it, and exit. However, the actual code will perform some more substantive operations. At the end of the while () loop, we lock the mutex object to check the active variables and check the new worker nodes at the top of the loop. After executing this code, you will find that if WQ. Control. Active is 0, the while loop will terminate and the code at the end of threadfunc () will be cleared.

It is very interesting to clear code parts from worker threads. First, because pthread_cond_wait () returns a locked mutex object, it will unlock work_queue. Then, it locks the clearing queue and adds the clearing code (including tid, the main thread will use this TID to call pthread_join (), and then unlocks the clearing queue. After that, it sends signals to all CQ waits (pthread_cond_signal (& CQ. Control. Cond), so the main thread knows that there is a new node to be processed. We do not use pthread_cond_broadcast () because this is not necessary.
-- Only one thread (main thread) is waiting to clear new nodes in the queue. When it calls join_threads (), the worker thread prints the close message and terminates the message, waiting for the pthread_join () call from the main thread.

Join_threads ()

For a simple example of how to use conditional variables, see the join_threads () function. If there are working program threads, join_threads () will be executed all the time, waiting to clear the new nodes in the queue. If there is a new node, we will remove the node from the queue and unlock the clearing Queue (so that the worker program can add the clearing node), connect to a new worker thread (using the TID stored in the clearing node), release the clearing node, reduce the number of existing threads, and then continue.

Conclusion

Now, the POSIX thread detail series is coming to an end. We hope you are ready to add multi-threaded code to your own applications. For more information, see
The references section also contains the tar files of all source code used in this article. Goodbye in the next series!

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.