Advanced Programming in Unix Environment Chapter 11: Threads

.... Chapter 10 signal notes have not been written yet, I guess it's still too dish. Many demo effects in the book are totally different from those in the book. It's a bit difficult to add the signal chapter. I'll fill in the code pit in the book and then fill in the tenth chapter notes.
* 1 Thread
A typical unix process can be seen as having only one control thread: a process can only do one thing at a time. With multiple threads, a process can be designed to do more than one thing at a time. Each thread has its own task. Thread adoption has many advantages:
(1): By assigning separate processing threads to each event type, you can simplify the code for handling asynchronous events. Each thread can use synchronous programming, mode, synchronous programming mode is much simpler than asynchronous programming mode.
(2): Each process uses the complex mechanism provided by the operating system to share memory and file descriptors.
(3): Some problems can be decomposed to improve the throughput of the whole program. In the case of only one control thread, a single-threaded process needs to accomplish more than one task, and only needs to serialize these tasks. But when there are multiple threads, independent task processing can be carried out crosswise, at this time only a single thread is allocated for each task.
(4): Interactive programs can also improve response time by using multithreading.
Each thread contains the information necessary to represent the execution environment. Each process has a process ID, and each thread also has a thread ID, process ID.
* 2 Thread Identification
Thread ID is unique in the whole system, but different from thread ID, thread ID is meaningful only in the context of the process in which it is located.
Thread ID is expressed by pthread_t data type. When implemented, a structure can be used to represent the data type of pthread_t. Therefore, portable operating system implementation cannot treat it as an integer, so a function must be used to compare the two thread IDs.
#include<pthread.h>
int pthread_equal(pthread_t tid1,pthread_t tid2);
Threads can obtain their own thread ID by calling pthread_self.
Create threads in ___________ 3

#include"apue.h"
#include<pthread.h>

pthread_t ntid;

void printids(const char* s)
{
    pid_t pid;
    pthread_t tid;
    pid = getpid();
    tid = pthread_self();
    printf("%s pid %lu tid %lu (0x%lx)\n",s,(unsigned long)pid,(unsigned long)tid,(unsigned long)tid);
}
void* thr_fn(void* arg)
{
    printids("new thread:");
    return ((void*)0);
}

int main()
{
    int err;
    err = pthread_create(&ntid,NULL,thr_fn,NULL);
    if(err != 0)
    {
        err_exit(err,"can't create thread");
    }
    printids("main thread:");
    sleep(1);
    return 0;
}


* 4 Thread Termination
A single thread exits in three ways, stopping its control flow without terminating the entire process.
(1) Threads can simply return from the startup routine, and the return value is the exit code of the thread.
(2) Threads can be cancelled by other threads in the same process
(3) Thread calls pthread_exit
Within a thread, pthread_exit is called to exit the thread, and other threads in the process can also call pthread_join to put the thread in a separate state.

    #include"apue.h"
    #include<pthread.h>
    void* thr_fn1(void* arg)
    {
        printf("thread 1 returning\n");
        return ((void*)1);
    }
    
    void* thr_fn2(void* arg)
    {
        printf("thread 2 exiting\n");
        pthread_exit((void*)2);
    }
    
    int main()
    {
        int err;
        pthread_t tid1,tid2;
        void* tret;
        err = pthread_create(&tid1,NULL,thr_fn1,NULL);
        if(err != 0)
            err_exit(err,"can't create thread 1");
        err = pthread_create(&tid2,NULL,thr_fn2,NULL);
        if(err != 0)
            err_exit(err,"can't create thread 2");
        err = pthread_join(tid1,&tret);
        if(err != 0)
            err_exit(err,"can't join with thread 1");
        printf("thread 1 exit code %ld\n",(long)tret);
        err = pthread_join(tid2,&tret);
        if(err != 0)
            err_exit(err,"can't join with thread 2");
        printf("thread 1 exit code %ld\n",(long)tret);
        exit(0);
    }
![Insert a picture description here](https://img-blog.csdnimg.cn/2019081020111210.png)
#include"apue.h"
#include<pthread.h>
struct foo{
  int a,b,c,d;  
};

void printfoo(const char* s,const struct foo *fp)
{
    printf("%s",s);
    printf("  structure at 0x%lx\n",(unsigned long)fp);
    printf("  foo.a = %d\n",fp->a);
    printf("  foo.b = %d\n",fp->b);
    printf("  foo.c = %d\n",fp->c);
    printf("  foo.d = %d\n",fp->d);
}

void* thr_fn1(void* arg)
{
    struct foo foo = {1,2,3,4};
    printfoo("thread 1:\n",&foo);
    pthread_exit((void*)&foo);
}

void* thr_fn2(void* arg)
{
    printf("thread 2: ID is %lu\n",(unsigned long)pthread_self());
    pthread_exit((void*)0);
}


int main()
{
    int err;
    pthread_t tid1,tid2;
    struct foo *fp;
    err = pthread_create(&tid1,NULL,thr_fn1,NULL);
    if(err != 0)
        err_exit(err,"can't create thread 1");
    err = pthread_join(tid1,(void**)&fp);
    if(err != 0)
        err_exit(err,"can't join with thread 1");
    sleep(1);
    printf("parent starting second thread\n");
    err = pthread_create(&tid2,NULL,thr_fn2,NULL);
    if(err != 0)
        err_exit(err,"can't create thread 2");
    err = pthread_join(tid2,(void**)&fp);
    sleep(1);
    printfoo("parent:\n",fp);
    exit(0);
}


From the results of the above program, we can see that under the Linux operating system, when the parent process tries to access the structure passed to it by the first thread, because this is the end of the thread and memory is not valid, it gets a segmental error signal.
* 5 Thread Processing Function pthread_cleanup_push/pthread_cleanup_pop
Threads can arrange the functions they need to call when they exit. Such functions are called thread cleanup handlers. Threads can set up multiple cleanup handlers. Processors are recorded on the stack, meaning that they are executed in the reverse order as they are registered. When the thread exits normally, the cleanup function in the stack will not be executed.

#include"apue.h"
#include<pthread.h>
void cleanup(void* arg)
{
    printf("cleanup:%s\n",(char*)arg);
}

void* thr_fn1(void* arg)
{
    printf("thread 1 start\n");
    pthread_cleanup_push(cleanup,(void*)("thread 1 first handler"));
    pthread_cleanup_push(cleanup,(void*)("thread 1 second handler"));
    printf("thread 1 push complete\n");
    if(arg)
        return ((void*)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return ((void*)1);
}

void* thr_fn2(void *arg)
{
    printf("thread 2 start\n");
    pthread_cleanup_push(cleanup,(void*)("thread 2 first handler"));
    pthread_cleanup_push(cleanup,(void*)("thread 2 second handler"));
    printf("thread 2 push complete\n");
    if(arg)
        pthread_exit((void*)2);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_exit((void*)2);
}

int main(void)
{
    int err;
    pthread_t tid1,tid2;
    void *tret;
    err = pthread_create(&tid1,NULL,thr_fn1,(void*)1);
    if(err != 0)
        err_exit(err,"can't create thread 1");
    err = pthread_create(&tid2,NULL,thr_fn2,(void*)1);
    if(err != 0)
        err_exit(err,"can't create thread 2");
    err = pthread_join(tid1,&tret);
    if(err != 0)
        err_exit(err,"can't join with thread 2");
    printf("thread 1 exit code %ld\n",(long)tret);
    err = pthread_join(tid2,&tret);
    if(err != 0)
        err_exit(err,"can't join with thread 2");
    printf("thread 2 exit code %ld\n",(long)tret);
    exit(0);
} 


* 6 Thread Synchronization
When multiple control threads share the same memory, they need to ensure that each thread sees a consistent view of the data. When multiple threads can read and write the same memory, they need to be synchronized.
7 Mutual Exclusion
Use the mutex interface of pthread to protect data and ensure that only one thread accesses data at the same time.
Mutex is essentially a lock, which is set (locked) before accessing shared resources and released (unlocked) after accessing.
After locking the mutex, any other thread attempting to lock the mutex again will be blocked until the current thread releases the mutex.
If multiple threads are blocked waiting when the mutex is released, then all blocked threads on the lock become runnable. The first running thread can lock the mutex, and the other threads will see that the mutex is still locked, and then re-block the wait.
demo:
#include <stdlib.h>
#include <pthread.h>

struct foo{

int f_count;
pthread_mutex_t f_lock;
int f_id;

/*...more stuff here... */

};

struct foo foo_alloc(int id)/ apply for and allocate object space*/
{
struct foo *fp;
if ((fp = malloc(sizeof(struct foo)))!= NULL){

    fp->f_count = 1;
    fp->f_id = id;
    if (pthread_mutex_init(&fp->f_lock, NULL)!= 0){
        free(fp);
        return (NULL);
    }
    /* ... continue initialization ...*/
}
return (fp);

}

void foo_hold(struct foo fp) / Add a reference to the object/
{
Pthread_mutex_lock(& fp-> f_lock); // Mutex is required because there may be multiple threads creating and holding objects
FP - > f_count + +; / reference count plus 1*/
pthread_mutex_unlock(&fp->f_lock);
}

void foo_rele(struct foo fp)/Release reference objects/
{
pthread_mutex_lock(&fp->f_lock);
if (–fp->f_count == 0){ / last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}else{

    pthread_mutex_unlock(&fp->f_lock);
}

}
8 Avoiding deadlocks
If a thread tries to lock the same mutex twice, it will fall into a deadlock state. When using the mutex, there are many undefined places where deadlocks occur.
Deadlock can be avoided by carefully controlling the order of mutex locking. Or use pthread_mutex_trylock, pthread_mutex_timedlock to achieve.
The following example shows two methods of locking. In the first example, when two mutexes are needed at the same time, they are always locked in the same order, so that deadlocks can be avoided. The second mutex maintains a Hash list for tracking foo data structures. In this way, hashlock mutex can protect not only hash fh in foo data structure, but also hash chain field f_next. The f_lock mutex in the foo structure protects access to other fields in the foo structure. The second example uses hashlock mutexes to lock changes to the entire hash table to manipulate reference counts.
demo1:

#include <stdlib.h>
#include <pthread.h>

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo *fh[NHASH];

pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
    int             f_count;
    pthread_mutex_t f_lock;
    int             f_id;
    struct foo     *f_next; /* protected by hashlock */
    /* ... more stuff here ... */
};

struct foo *
foo_alloc(int id) /* allocate the object */
{
    struct foo    *fp;
    int            idx;

    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
        fp->f_id = id;
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
            free(fp);
            return(NULL);
        }
        idx = HASH(id);
        pthread_mutex_lock(&hashlock);
        fp->f_next = fh[idx];
        fh[idx] = fp;
        pthread_mutex_lock(&fp->f_lock);
        pthread_mutex_unlock(&hashlock);
        /* ... continue initialization ... */
        pthread_mutex_unlock(&fp->f_lock);
    }
    return(fp);
}

void
foo_hold(struct foo *fp) /* add a reference to the object */
{
    pthread_mutex_lock(&fp->f_lock);
    fp->f_count++;
    pthread_mutex_unlock(&fp->f_lock);
}

struct foo *
foo_find(int id) /* find an existing object */
{
    struct foo    *fp;

    pthread_mutex_lock(&hashlock);
    for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
        if (fp->f_id == id) {
            foo_hold(fp);
            break;
        }
    }
    pthread_mutex_unlock(&hashlock);
    return(fp);
}

void
foo_rele(struct foo *fp) /* release a reference to the object */
{
    struct foo    *tfp;
    int            idx;

    pthread_mutex_lock(&fp->f_lock);
    if (fp->f_count == 1) { /* last reference */
        pthread_mutex_unlock(&fp->f_lock);
        pthread_mutex_lock(&hashlock);
        pthread_mutex_lock(&fp->f_lock);
        /* need to recheck the condition */
        if (fp->f_count != 1) {
            fp->f_count--;
            pthread_mutex_unlock(&fp->f_lock);
            pthread_mutex_unlock(&hashlock);
            return;
        }
        /* remove from list */
        idx = HASH(fp->f_id);
        tfp = fh[idx];
        if (tfp == fp) {
            fh[idx] = fp->f_next;
        } else {
            while (tfp->f_next != fp)
                tfp = tfp->f_next;
            tfp->f_next = fp->f_next;
        }
        pthread_mutex_unlock(&hashlock);
        pthread_mutex_unlock(&fp->f_lock);
        pthread_mutex_destroy(&fp->f_lock);
        free(fp);
    } else {
        fp->f_count--;
        pthread_mutex_unlock(&fp->f_lock);
    }
}

demo2:

#include <stdlib.h>
#include <pthread.h>

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
    int             f_count; /* protected by hashlock */
    pthread_mutex_t f_lock;
    int             f_id;
    struct foo     *f_next; /* protected by hashlock */
    /* ... more stuff here ... */
};

struct foo *
foo_alloc(int id) /* allocate the object */
{
    struct foo    *fp;
    int            idx;

    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
        fp->f_id = id;
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
            free(fp);
            return(NULL);
        }
        idx = HASH(id);
        pthread_mutex_lock(&hashlock);
        fp->f_next = fh[idx];
        fh[idx] = fp;
        pthread_mutex_lock(&fp->f_lock);
        pthread_mutex_unlock(&hashlock);
        /* ... continue initialization ... */
        pthread_mutex_unlock(&fp->f_lock);
    }
    return(fp);
}

void
foo_hold(struct foo *fp) /* add a reference to the object */
{
    pthread_mutex_lock(&hashlock);
    fp->f_count++;
    pthread_mutex_unlock(&hashlock);
}

struct foo *
foo_find(int id) /* find an existing object */
{
    struct foo    *fp;

    pthread_mutex_lock(&hashlock);
    for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
        if (fp->f_id == id) {
            fp->f_count++;
            break;
        }
    }
    pthread_mutex_unlock(&hashlock);
    return(fp);
}

void
foo_rele(struct foo *fp) /* release a reference to the object */
{
    struct foo    *tfp;
    int            idx;

    pthread_mutex_lock(&hashlock);
    if (--fp->f_count == 0) { /* last reference, remove from list */
        idx = HASH(fp->f_id);
        tfp = fh[idx];
        if (tfp == fp) {
            fh[idx] = fp->f_next;
        } else {
            while (tfp->f_next != fp)
                tfp = tfp->f_next;
            tfp->f_next = fp->f_next;
        }
        pthread_mutex_unlock(&hashlock);
        pthread_mutex_destroy(&fp->f_lock);
        free(fp);
    } else {
        pthread_mutex_unlock(&hashlock);
    }
}

The following example demonstrates the use of pthread_mutex_timedlock

#include"apue.h"
#include<pthread.h>
int main(void)
{
    int err;
    struct timespec tout;
    struct tm* tmp;
    char buf[64];
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    
    pthread_mutex_lock(&lock);
    printf("mutex is locked\n");
    clock_gettime(CLOCK_REALTIME,&tout);
    tmp = localtime(&tout.tv_sec);
    strftime(buf,sizeof(buf),"%r",tmp);
    printf("current time is %s\n",buf);
    tout.tv_sec += 10;
    err = pthread_mutex_timedlock(&lock,&tout);
    clock_gettime(CLOCK_REALTIME,&tout);
    tmp = localtime(&tout.tv_sec);
    strftime(buf,sizeof (buf),"%r",tmp);
    printf("the time is now%s\n",buf);
    if(err == 0)
        printf("mutex locked again!\n");
    else {
        printf("can't lock mvtex again:%s\n");
    }
    exit(0);
}

Tags: Programming Unix Linux

Posted on Sat, 10 Aug 2019 05:34:12 -0700 by peachsnapple