在Linux下使用多线程编程,主要是使用glibc库中的接口

线程基础

1.线程概述
  1. 线程是轻量级的进程(称作LWP)。进程是资源分配的最小单位,线程是OS调度执行的单位。
  2. 线程和进程的区别:
    1. 每个进程拥有自己独立的地址空间,而多个线程可以共用一个地址空间,代码段、堆区、全局数据区、打开的文件(内核中管理的文件描述符表)都是线程共享的
    2. 在一个地址空间中多个线程独享:每个线程都有自己的栈区,寄存器(在内核中管理的)。
    3. 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
    4. CPU的调度和切换:线程的上下文切换比进程快得多
2.线程的创建
  1. 线程的创建:
    1. 函数原型如下:
    #include <pthread.h>
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                       void *(*start_routine) (void *), void *arg);
    函数参数:
        thread:传出参数,线程创建成功,会将线程id写入到这个指针指向的内存中
        attr:线程的属性
        start_routine:函数指针,表示线程的处理动作
        arg:作为实参传递到start_routine函数指针指向的函数
    返回值:线程创建成功返回0,失败则返回对应的错误号
    
    1. 线程创建示例
    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    
    void* func(void* arg) {
        printf("子线程id:%ld\n", pthread_self());
        for (int i = 0; i < 5; i++) {
            printf("child:%d\n", i);
        }
        return NULL;
    }
    int main() {
        pthread_t tid;
        pthread_create(&tid, NULL, func, NULL);
        printf("子线程id:%ld,主线程id:%ld\n", tid, pthread_self());
        for (int i = 0; i < 5; i++) {
            printf("main:%d\n", i);
        }
        // 休眠一秒钟,让出CPU时间片
        // 防止主线程退出,导致虚拟地址空间被释放,子线程就一并被销毁
        sleep(1);
        return 0;
    }
    
  2. 如果将类方法作为新创建线程的执行函数,只能是静态方法,而不能是非静态方法。因为非静态方法在由编译器编译后,第一个参数是一个指向类类型的this指针不符合pthread_create第三个参数的声明。问题来了,只能传递静态成员方法,由于静态成员方法没有this指针,如何调用类中的实例方法呢?答案是创建线程时,传递类的实例对象作为线程执行函数的参数。示例如下:
#include <stdio.h>
#include <pthread.h>

class Test {
public:
    static void* threadFunc(void*);
    void other() {
        printf("other work\n");
    }
};

void* Test::threadFunc(void* arg) {
    Test* ptr = (Test*)arg;
    ptr->other();
    pthread_exit(NULL);
}

int main() {
    Test test;
    pthread_t tid;
    pthread_create(&tid, nullptr, Test::threadFunc, &test);
    pthread_join(tid, nullptr);
    
    return 0;
}
3.线程的退出
  1. 在编写多线程程序的时候,如果想让线程退出但是不会导致虚拟地址空间的释放,就可以调用线程库中的线程退出函数。只要调用该函数,调用函数的当前线程就会马上退出,并且不会影响到其他线程的执行。
    1. 函数原型如下:
    #include <pthread.h>
    void pthread_exit(void *retval);
    
    1. 使用示例:
    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    
    void* func(void* arg) {
        printf("子线程id:%ld\n", pthread_self());
        for (int i = 0; i < 100; i++) {
            printf("child:%d\n", i);
            sleep(1);
        }
        return NULL;
    }
    int main() {
        pthread_t tid;
        pthread_create(&tid, NULL, func, NULL);
        printf("子线程id:%ld,主线程id:%ld\n", tid, pthread_self());
        for (int i = 0; i < 5; i++) {
            printf("main:%d\n", i);
        }
        // 主线程调用线程退出函数退出,虚拟地址空间不会释放
        // 不会影响到子线程的执行
        pthread_exit(NULL);
        return 0;
    }
    
4.线程的回收
  1. 线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出,pthread_join函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。另外通过线程回收函数还可以获取到子线程退出时传递出来的数据。
    1. 函数原型如下:
    #include <pthread.h>
    // 这是一个阻塞函数, 子线程在运行这个函数就阻塞
    // 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
    int pthread_join(pthread_t thread, void **retval);
    函数参数:
        thread:要被回收的子线程的线程id
        retval:是一个传出参数,这个二级指针指向一级指针的地址,
        地址存储了子线程调用线程退出函数pthread_exit的退出状态
    返回值
        线程回收成功返回0,失败则返回错误号
    
  2. 线程之间的数据传递:在子线程退出的时候调用pthread_exit()函数,将返回值通过参数传递出去。这样主线程在回收这个子线程的时候可以通过phread_join()的第二个参数来接收子线程传递的返回值。主线程接受子线程传递的数据有很多种处理方式,下面来列举几种
    1. 使用全局变量或者静态变量或者堆分配内存的变量。位于同一虚拟地址空间中的线程,虽然不能够共享栈区数据,但是可以共享数据段和堆区数据。
    #include <pthread.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    struct Data {
        int age;
        char name[32];
    };
    
    // struct Data data;
    void* func(void* arg) {
        printf("子线程id:%ld\n", pthread_self());
        // static struct Data data;
        // data.age = 23;
        // strcpy(data.name, "NrvCer");
        // pthread_exit(&data);
    
        struct Data* data = (struct Data*)malloc(sizeof(struct Data));
        data->age = 23;
        strcpy(data->name, "NrvCer");
        pthread_exit(data);
    }
    
    int main() {
        pthread_t tid;
        pthread_create(&tid, NULL, func, NULL);
        // 主线程获取子线程返回的数据
        void* ptr = NULL;
        // 阻塞等待子线程退出
        pthread_join(tid, &ptr);
        struct Data* data = (struct Data*) ptr;
        printf("%d,%s,%ld\n", data->age, data->name, pthread_self());
        if (data) free(data);
    
        return 0;
    }
    
    1. 使用主线程栈:如果主线程是最后退出的,则可以将子线程返回的数据保存到主线程的栈区内存中。
    #include <pthread.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    
    struct Data {
        int age;
        char name[32];
    };
    
    void* func(void* arg) {
        printf("子线程id:%ld\n", pthread_self());
        struct Data* data = (struct Data*)arg;
        data->age = 23;
        strcpy(data->name, "NrvCer");
    
        pthread_exit(data);
    }
    int main() {
        pthread_t tid;
        struct Data data;
        pthread_create(&tid, NULL, func, &data);
    
        void* ptr;
        pthread_join(tid, &ptr);
        struct Data*  recvData= (struct Data*) ptr;
    
        printf("%d,%s,%ld\n", recvData->age, recvData->name, pthread_self());
        printf("%d,%s,%ld\n", data.age, data.name, pthread_self());
    
        return 0;
    }
    
5.线程分离
  1. 在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主线程的业务就不能执行了。
  2. 线程分离函数pthread_detach,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
    1. 函数原型如下:
    #include <pthread.h>
    // 参数为子线程的线程ID
    int pthread_detach(pthread_t thread);
    
    1. 使用示例:
    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    
    void* func(void* arg) {
        for (int i = 0; i < 100; i++) {
            printf("child:%ld,%d\n", pthread_self(), i);
            sleep(1);
        }
        
        return NULL;
    }
    
    int main() {
        pthread_t tid;
        pthread_create(&tid, NULL, func, NULL);
        // 设置主线程和子线程分离
        pthread_detach(tid);
        for (int i = 0; i < 5; i++)
            printf("main:%ld,%d\n", pthread_self(), i);
        // 主线程退出,进程的虚拟地址空间不会释放
        pthread_exit(NULL);
        return 0;
    }
    
6.其他线程函数
  1. 线程id获取:
    1. 使用pthread_t pthread_self()函数
    2. 使用pthread_create函数创建线程时会将线程id传出
    3. 使用syscall库函数,方式3和方式12的区别是,方式12获取的线程id可能不是系统唯一的,而方式3获取的线程id在系统范围内全局唯一。
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <stdio.h>
    #include <pthread.h>
    
    int
    main(int argc, char *argv[])
    {
        pid_t tid;
    
        tid = syscall(SYS_gettid);
        // 139808617809728 126139
        printf("%ld\t%d\n", pthread_self(), tid);
    }
    
  2. 线程id比较:使用pthread_equal函数

线程同步

1.线程同步的概念
  1. 线程同步:多个线程按照先后顺序依次对共享内存区域中的共享资源进行访问。所谓的共享资源就是多个线程可以共同访问的变量,这些变量通常为数据段上的内容或者堆区变量,这些变量对应的共享资源称之为临界资源。
  2. 在Linux上线程同步方式有以下几种,如下所示:
    1. 互斥锁:mutex,这是两个单词的全称,Mutual Exclusion(相互排斥)
    2. 读写锁
    3. 条件变量
    4. 信号量
2.互斥锁
  1. 互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行,而不能并行执行。
  2. Linux中互斥锁的类型为pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁
  3. 在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程抢到锁后才能被解除阻塞。一般情况下,每一个共享资源对应一把互斥锁,锁的个数和线程的个数无关。
  4. 互斥锁的种类:有非递归锁、递归锁(又称可重入锁)、检错锁。对于递归锁,同一个线程可以对已经持有的锁重复加锁,这个重复加锁类似于引用计数,加锁次数需要和解锁次数相等。
  5. Linux下提供的互斥锁操作函数如下:
    1. 初始化互斥锁
    1. 方式1:使用pthread_mutex_init函数
    pthread_mutex_init (pthread_mutex_t *__mutex,
    		       const pthread_mutexattr_t *__mutexattr)
    函数参数:
        mutex:互斥锁变量的地址
        attr:互斥锁的属性,通常使用默认属性,这个参数指定为NULL。表示初始化非递归锁
            如果需要初始化递归锁,则需要使用pthread_mutexattr_settype函数设置互斥锁的属性
    返回值:
        函数调用成功会返回0
        调用失败会返回相应的错误号
    2. 方式2:使用这种方式初始化的互斥锁无需销毁
        使用PTHREAD_MUTEX_INITIALIZER 初始化非递归锁
        使用PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP初始化递归锁
    
    1. 释放互斥锁的资源
    int pthread_mutex_destroy (pthread_mutex_t *__mutex)
    
    1. 修改互斥锁的状态,将其设定为锁定状态
    int pthread_mutex_lock (pthread_mutex_t *__mutex)
    
    1. 尝试加锁
    int pthread_mutex_trylock (pthread_mutex_t *__mutex)
    函数返回值:
        如果锁没有被锁定,状态是打开的,则线程加锁成功
        如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,而是加锁失败返回错误号
    
    1. 对互斥量解锁
    int pthread_mutex_unlock (pthread_mutex_t *__mutex)
    
  6. 互斥锁的属性:
    1. 初始化互斥锁属性对象:int pthread_mutexattr_init(pthread_mutexattr_t *attr);
    2. 销毁互斥锁属性对象:int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
    3. 设置互斥锁的属性:
    int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);
    type的取值:
        PTHREAD_MUTEX_NORMAL:普通互斥锁,创建互斥锁默认就是这个
        PTHREAD_MUTEX_ERRORCHECK:检错锁,其他或者当前线程再次加锁会返回EDEADLOCK错误
        PTHREAD_MUTEX_RECURSIVE:递归锁,允许同一个线程对持有的锁进行重复加锁
    
    1. 获取互斥锁的属性:
    int pthread_mutexattr_gettype(const pthread_mutexattr_t* restrict attr, int* restrict type);
    
  7. 使用示例:四个售货员,售卖10张票。一个售货员就把票卖完了hhh。。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

// 创建一把互斥锁
// 一个全局变量,多个线程共享
pthread_mutex_t mutex;

int ticket = 10;
void* sellTicket(void* arg) {
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            sleep(2);
            printf("线程id:%ld,售出第%d张票\n", pthread_self(), 10 - ticket + 1);
            ticket--;
        }
        pthread_mutex_unlock(&mutex);
    }
    pthread_exit(NULL);
}

int main() {
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    pthread_t threads[4];
    for (int i = 0; i < 4; i++) {
        int flag = pthread_create(&threads[i], NULL, sellTicket, NULL);
        if (flag != 0) { perror("线程创建失败!\n"); return 0; }
    }
    // 保证四个线程创建完成,即四个售票员
    sleep(10);
    for (int i = 0; i < 4; i++) {
        int flag = pthread_join(threads[i], NULL);
        if (flag != 0) { perror("等待失败!\n"); return 0; }
    }
    // 销毁互斥锁资源
    pthread_mutex_destroy(&mutex);
    
    return 0;
}
3.读写锁
  1. 读写锁是互斥锁的升级,在做读操作的时候可以提高程序的执行效率。如果所有的线程都是做读操作,那么读是并行的。但是使用互斥锁,读操作是和写操作一样都是串行的。Linux中的读写锁和互斥锁一样,也是一把锁,读写锁的类型为pthread_rwlock_t
  2. 读写锁的特点:
    1. 使用读写锁的读锁锁定了临界区,则线程对临界区的访问是并行的,因为读锁是共享的。
    2. 使用读写锁的写锁锁定了临界区,则线程对临界区的访问是串行的,因为写锁是独占的。
    3. 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问这两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞
  3. 读写锁的使用场景:如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源的读操作更多,则使用读写锁更有优势。
  4. 读写锁的相关操作函数:
    1. 读写锁的初始化
    // 方式1
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
    // 方式2
    使用PTHREAD_RWLOCK_INITIALIZER;
    
    1. 释放读写锁占用的系统资源
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    
    1. 在程序中对读写锁加读锁,锁定的是读操作
    int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock)
    
    1. 尝试加读锁,如果加读锁失败,不会阻塞当前线程,直接返回错误号
    int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock)
    
    1. 对读写锁加写锁,锁定的是写操作
    int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock)
    
    1. 尝试加写锁,如果加写锁失败,不会阻塞当前线程,直接返回错误号
    int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock)
    
    1. 解锁。不管锁定了读还是写,都可以解锁
    int pthread_rwlock_unlock (pthread_rwlock_t *rwlock)
    
    1. 读写锁类型的设置
    int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr, int pref);
    函数参数:
        attr:由int pthread_rwlockattr_init函数初始化的属性对象
        pref:设置读写锁的类型,其值有如下几种
        //读者优先(同时请求读锁和写锁时,请求读锁的线程优先获得锁)
        PTHREAD_RWLOCK_PREFER_READER_NP,  // 默认
        //读者优先
        PTHREAD_RWLOCK_PREFER_WRITER_NP, 
        //写者优先(同时请求读锁和写锁时,请求写锁的线程优先获得锁)
        PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP
    int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t* attr, int* pref);
    
    // 设置写锁优先示例
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    //设置成请求写锁优先
    pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
    pthread_rwlock_init(&myrwlock, &attr);
    
  5. 读写锁的使用示例:8个线程操作同一个全局变量,3个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int number = 0;
// 定义读写锁
pthread_rwlock_t rwlock;

void* writeNum(void* arg) {
    while(1) {
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur ++;
        number = cur;
        printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }
}
void* readNum(void* arg) {
    while(1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }
}
int main() {
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);
    pthread_t wthreads[5], rthreads[3];
    for (int i = 0; i < 5; i++) {
        pthread_create(&wthreads[i], NULL, writeNum, NULL);
    }
    for (int i = 0; i < 3; i++) {
        pthread_create(&rthreads[i], NULL, readNum, NULL);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(wthreads[i], NULL);
    }
    for (int i = 0; i < 3; i++) {
        pthread_join(rthreads[i], NULL);
    }

    // 释放读写锁的资源
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

4.条件变量
  1. 条件变量:在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。条件变量只有在满足指定条件下才会阻塞线程。
  2. 条件变量的类型为:pthread_cond_t
  3. 条件变量的相关操作函数如下:
    1. 初始化条件变量
    方式1:
    int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
    方式2:
    使用PTHREAD_COND_INITIALIZER
    
    1. 销毁条件变量
    int pthread_cond_destroy (pthread_cond_t *cond)
    
    1. 条件变量等待被唤醒:当条件不满足,wait系列函数将会阻塞调用线程,并且释放持有的互斥锁。因此在调用wait系列函数前会有一个上锁操作;同理,当收到条件信号,当前阻塞的线程被唤醒,pthread_cond_wait调用将会返回并对互斥锁进行上锁。因此在调用wait系列函数后会有一个解锁操作。
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
    函数参数:
        mutex:在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
        
    // 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
    int pthread_cond_timedwait(pthread_cond_t *restrict cond,
               pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
    
    1. 唤醒阻塞在条件变量上的线程
    // 至少有一个被解除阻塞
    int pthread_cond_signal (pthread_cond_t *cond)
    // 被阻塞的线程全部解除阻塞
    int pthread_cond_broadcast(pthread_cond_t *cond);
    
  4. 虚假唤醒问题:pthread_cond_signal或者pthread_cond_broadcast唤醒多个wait的线程。产生虚假唤醒的一个原因是一个原因是wait系列函数是futex系统调用,属于阻塞型的系统调用,当系统调用被信号中断的时候,会返回-1,并且把errno置为EINTR。很多这种系统调用为了防止被信号中断都会重启系统调用,但是在wait系列函数返回之后到重新调用之前,条件信号已经到达,一旦错失可能导致wait系列调用永久阻塞下去。因此为了避免这种情况,宁可虚假唤醒,也不能再次调用wait系列函数,以免wait系列调用永久阻塞下去。
    1. 示例:使用条件变量结合互斥锁实现生产者、消费者模型,其中五个生产者,五个消费者
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    // 链表的结点
    typedef struct Node {
        int number;
        struct Node* next;
    }Node;
    // 指向头节点的指针
    struct Node* head = NULL;
    
    // 条件变量
    pthread_cond_t cond;
    // 互斥锁
    pthread_mutex_t mutex;
    
    void* producer(void* arg) {
        while (1) {
            pthread_mutex_lock(&mutex);
            Node* node = (Node*)malloc(sizeof(Node));
            node->number = rand() % 1000;
            node->next = head;
            head = node;
            printf("生产者线程id:%ld,number值:%d\n", pthread_self(), node->number);
            pthread_mutex_unlock(&mutex);
            // 通知所有消费者消费
            pthread_cond_broadcast(&cond);
            // 生产慢一点
            sleep(rand() % 3);
        }
    }
    
    void* consumer(void* arg) {
        while (1) {
            pthread_mutex_lock(&mutex);
            // 使用while而不用if
            while (head == NULL) {
                pthread_cond_wait(&cond, &mutex);
            }
            Node* node = head;
            head = node->next;
            printf("消费者线程id:%ld,number值:%d\n", pthread_self(), node->number);
            if (node) free(node);
            pthread_mutex_unlock(&mutex);
            sleep(rand() % 3);
        }
    }
    
    int main() {
        pthread_cond_init(&cond, NULL);
        pthread_mutex_init(&mutex, NULL);
    
        pthread_t proThreads[5], conThreads[5];
        for (int i = 0; i < 5; i++) {
            pthread_create(&proThreads[i], NULL, producer, NULL);
            pthread_create(&conThreads[i], NULL, consumer, NULL);
        }
        for (int i = 0; i < 5; i++) {
            pthread_join(proThreads[i], NULL);
            pthread_join(conThreads[i], NULL);
        }
    
        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&mutex);
    
        return 0;
    }
    
    1. 上例中的生产者消费者模型中,如果在consumer函数中使用if会出现虚假唤醒问题:使用pthread_cond_broadcast唤醒了不该醒过来的消费者线程,因为执行的是if判断,不用继续判断任务队列是否为空,所以进行消费,此时可能由于任务队列已经为空,所以将产生运行时的段错误。 而使用while进行判断,后续的消费者线程还会进行任务队列是否为空的判断。
  5. 为什么条件变量需要和互斥锁结合?因为释放互斥锁和条件变量等待唤醒必须是一个原子操作。如下所示,假设不是原子操作,线程A执行完第三行代码后CPU时间片被剥夺,此时另外一个线程B获得该互斥锁m,然后发送条件信号通知等待此条件信号的线程A。等线程A重新获得时间片后,由于该信号已经被错过了,这样可能会导致线程A在第6行pthread_cond_wait(&cv);无限阻塞下去。在pthread_cond_wait相关函数实现中就将代码3和6执行的功能放在一个原子操作中。
//m的类型是已初始化的互斥锁,cv是条件变量
1    pthread_mutex_lock(&m)
2    while(condition_is_false) {
3         pthread_mutex_unlock(&m);
4         //解锁之后,等待之前,可能条件已经满足5,信号已经发出,但是该信号可能会被错过
6         pthread_cond_wait(&cv);
7         pthread_mutex_lock(&m);
8    }
  1. 条件变量信号丢失的问题:A线程通过调用pthread_cond_signal或者pthread_cond_broadcast唤醒阻塞在条件变量上的其他线程时,如果没有相关的线程调用wait系列函数捕捉该信号,则该信号就丢失了,再次调用pthread_cond_wait函数将阻塞。
5.信号量(信号灯)
  1. 信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
  2. 信号的类型:sem_t
  3. 信号量操作函数:
    1. 信号量的初始化:
    int sem_init (sem_t *__sem, int __pshared, unsigned int __value)
    函数参数:
        sem:信号量变量地址
        pshared:
            0表示在同一个进程的线程之间共享
            非0表示在多个进程之间共享
        value:初始化当前信号量拥有的资源数,如果资源数为0,线程就被阻塞了。
    
    1. sem 中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数- 1,直到 sem 中的资源数减为0时,资源被耗尽,因此线程也就被阻塞
    int sem_wait(sem_t *sem);
    
    1. 和sem_wait函数一样,但是当sem中的资源数减为0时,资源被耗尽,线程不会被阻塞,直接返回错误号EAGAIN。
    int sem_trywait(sem_t *sem);
    
    1. 和sem_wait函数一样,但是当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞,超时返回。
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    
    1. 给sem中的资源数+1
    // 调用这个函数将使得给sem中的资源数加一
    // 因为调用sem_wait、sem_trywait、sem_timedwait处于阻塞的线程将解除阻塞。
    int sem_post(sem_t *sem);
    
    1. 查看信号量sem中的整型数的当前值
    int sem_getvalue(sem_t *sem, int *sval);
    函数参数:
        sval:传出参数
    
    1. 销毁信号量:
    int sem_destroy(sem_t *sem);
    
  4. 使用信号量实现生产者消费者模型
    1. 信号量总资源数为1的情况:不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。
    #include <pthread.h>
    #include <semaphore.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h> // for sleep
    
    typedef struct Node {
        int number;
        struct Node* next;
    }Node;
    // 指向头节点的指针
    Node* head = NULL;
    
    // 生产者线程信号量
    sem_t proSem;
    // 消费者线程信号量
    sem_t conSem;
    
    void* producer(void* arg) {
        while (1) {
            sem_wait(&proSem);
            Node* node = (Node*)malloc(sizeof(Node));
            node->next = head;
            node->number = rand() % 1000;
            printf("生产者线程id:%ld,number的值:%d\n", pthread_self(), node->number);
            head = node;
            // 通知消费者线程消费,给消费者增加信号灯
            sem_post(&conSem);
            // 生产慢一点
            sleep(rand() % 3);
        }
    }
    void* consumer(void* arg) {
        while (1) {
            sem_wait(&conSem);
            Node* node = head;
            printf("消费者线程id:%ld,number的值:%d\n", pthread_self(), node->number);
            head = node->next;
            free(node);
            // 通知生产者生产,给生产者增加信号灯
            sem_post(&proSem);
            sleep(rand() % 3);
        }
    }
    int main() {
        sem_init(&proSem, 0, 1);
        sem_init(&conSem, 0, 0);
        pthread_t proThreads[5];
        pthread_t conThreads[5];
        for (int i = 0; i < 5; i++) {
            pthread_create(&proThreads[i], NULL, producer, NULL);
            pthread_create(&conThreads[i], NULL, consumer, NULL);
        }
        for (int i = 0; i < 5; i++) {
            pthread_join(proThreads[i], NULL);
            pthread_join(conThreads[i], NULL);
        }
        sem_destroy(&proSem);
        sem_destroy(&conSem);
        
        return 0;
    }
    
    1. 总资源数大于1:可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行同步
    #include <pthread.h>
    #include <semaphore.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h> // for sleep
    
    typedef struct Node {
        int number;
        struct Node* next;
    }Node;
    // 指向头节点的指针
    Node* head = NULL;
    
    // 生产者线程信号量
    sem_t proSem;
    // 消费者线程信号量
    sem_t conSem;
    // 互斥锁
    pthread_mutex_t mutex;
    
    void* producer(void* arg) {
        while (1) {
            // 注意下面两行代码的调用次序,防止死锁
            sem_wait(&proSem);
            pthread_mutex_lock(&mutex);
            Node* node = (Node*)malloc(sizeof(Node));
            node->next = head;
            node->number = rand() % 1000;
            printf("生产者线程id:%ld,number的值:%d\n", pthread_self(), node->number);
            head = node;
            // 通知消费者线程消费,给消费者增加信号灯
            sem_post(&conSem);
            pthread_mutex_unlock(&mutex);
            // 生产慢一点
            sleep(rand() % 3);
        }
    }
    void* consumer(void* arg) {
        while (1) {
            // 注意下面两行代码的调用次序,防止死锁
            sem_wait(&conSem);
            pthread_mutex_lock(&mutex);
            Node* node = head;
            printf("消费者线程id:%ld,number的值:%d\n", pthread_self(), node->number);
            head = node->next;
            if (node) free(node);
            // 通知生产者生产,给生产者增加信号灯
            sem_post(&proSem);
            pthread_mutex_unlock(&mutex);
            sleep(rand() % 3);
        }
    }
    int main() {
        pthread_mutex_init(&mutex, NULL);
        sem_init(&proSem, 0, 5);
        sem_init(&conSem, 0, 0);
    
        pthread_t proThreads[5];
        pthread_t conThreads[5];
        for (int i = 0; i < 5; i++) {
            pthread_create(&proThreads[i], NULL, producer, NULL);
            pthread_create(&conThreads[i], NULL, consumer, NULL);
        }
        for (int i = 0; i < 5; i++) {
            pthread_join(proThreads[i], NULL);
            pthread_join(conThreads[i], NULL);
        }
        
        sem_destroy(&proSem);
        sem_destroy(&conSem);
        pthread_mutex_destroy(&mutex);
    
        return 0;
    }
    
    1. 上个程序中,注意代码的调用次序,否则可能产生死锁。考虑这么一种情况:某个消费者线程先运行,对互斥锁枷锁成功,然后调用sem_wait(),由于没有资源将被阻塞。而后生产者和其余的消费者进行加锁也将被阻塞。

线程局部存储

为了数据操作的安全,多个线程访问位于共享内存区域的共享资源,需要线程同步技术。那么如果各个线程将数据置于线程局部存储区又当如何呢?

  1. 线程局部存储(Thread Local Storage,TLS):对于一个包含多个线程的进程来说,每个线程可以操作自己的这份数据,这样的数据称之为线程局部存储,对应的存储区域叫做线程局部存储区。
  2. 方式1:Linux提供了一套函数接口来实现线程局部存储
    1. 函数接口
    // 创建键,所有线程都可以通过键获取各自线程局部存储区上的数据
    int pthread_key_create(pthread_key_t* key, void (*destructor)(void*));
    函数参数:
        key:这个key应当是所有线程都可以访问到的
        destructor:线程终止时,如果key关联的值不是NULL,那么会自动执行定义的destructor 函数
    
    // 通过键删除数据
    int pthread_key_delete(pthread_key_t key);
    // 通过键值设置数据
    int pthread_setspecific(pthread_key_t key, const void* value);
    // 通过键获取数据
    void* pthread_getspecific(pthread_key_t key);
    
  3. Linux gcc编译器提供了一个关键字__thread定义线程局部变量。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 定义线程局部变量
__thread int g_numer;

void* read(void* arg) {
    while (1) {
        printf("g_numer:%d, tid:%ld\n", g_numer, pthread_self());
        sleep(1);
    }
}

void* write(void* arg) {
    while (1) {
        g_numer++;
        printf("g_numer:%d, tid:%ld\n", g_numer, pthread_self());
        sleep(1);
    }
}

int main() {
    pthread_t tids[2];
    pthread_create(&tids[0], nullptr, read, nullptr);
    pthread_create(&tids[1], nullptr, write, nullptr);
    for (int i = 0; i < 2; ++i) {
       pthread_join(tids[i], nullptr);
    }
    return 0;
}

死锁

  1. 死锁:当多个线程访问共享资源,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的
  2. 造成死锁的场景如下:
    1. 加锁之后忘记解锁
    #include <pthread.h>
    #include <stdio.h>
    pthread_mutex_t mutex;
    
    void* func(void* arg) {
        // 上锁但是没有解锁,则下一个抢到CPU时间片的线程将阻塞在这
        pthread_mutex_lock(&mutex);
    }
    int main() {
        pthread_mutex_init(&mutex, NULL);
        
        pthread_t threads[2];
        for (int i = 0; i < 2; i++) {
            pthread_create(&threads[i], NULL, func, NULL);
        }
        for (int i = 0; i < 2; i++) {
            pthread_join(threads[i], NULL);
        }
        
        pthread_mutex_destroy(&mutex);
        return 0;
    }
    
    1. 重复加非递归锁,造成死锁
    void* func(void* arg) {
        pthread_mutex_lock(&mutex);
        // 重复加锁
        pthread_mutex_lock(&mutex);
        pthread_mutex_unlock(&mutex);
    }
    
    1. 在程序中有多个共享资源,因此存在很多把锁。有的时候锁的次序推进不当将导致互相被阻塞。
    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
    
    void* funcA(void* arg) {
        pthread_mutex_lock(&mutexA);
        // 保证线程t2获得锁mutexB
        sleep(1);
        pthread_mutex_lock(&mutexB);
        pthread_mutex_unlock(&mutexB);
        pthread_mutex_unlock(&mutexA);
    
        pthread_exit(NULL);
    }
    void* funcB(void* arg) {
        pthread_mutex_lock(&mutexB);
        pthread_mutex_lock(&mutexA);
        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    
        pthread_exit(NULL);
    }
    int main() {
        pthread_t t1, t2;
        pthread_create(&t1, NULL, funcA, NULL);
        pthread_create(&t2, NULL, funcB, NULL);
    
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
        return 0;
    }
    
  3. 在使用多线程的时候,如何避免死锁
    1. 对共享资源访问完毕后,一定要解锁。或者在加锁的时候使用trylock
    2. 如果程序中包含多把锁,可以控制对锁的访问顺序。也可以在对其他互斥锁做加锁操作前,先释放当前线程拥有的互斥锁。
    3. 在项目中引入专门用于死锁检测的模块
    4. 项目开发中能不使用锁就尽量不要使用锁,无锁队列了解一下hhh
  4. 实际运行的项目可能出现死锁,如何排查?