第14章 多线程编程

14.1 Linux线程概述

14.1.1 线程模型

        线程是CPU调度的最小单位。线程可分为内核线程和用户线程内核线程:运行在内核空间,由内核来调度;用户线程:运行在用户空间,由线程库来调度。当进程的一个内核线程获得CPU使用权是,它就加载并运行一个用户线程。

        线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属于1:1模型。

(1)N:1用户线程模型

        在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理。N:1关系指N个用户线程对应同一个内核线程:

N:1模型的优点:

        用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快。

N:1模型的缺点:

        一个进程中的多个线程只能调度到一个CPU,这种约束限制了可用的并行总量。

        如果某个线程执行了一个“阻塞式”操作(如read),那么,进程中的所有线程都会阻塞,直至那个操作结束。

(2)1:1核心线程模型

        在1:1核心线程模型中,应用程序创建的每一个线程(也有书称为LWP)都由一个核心线程直接管理,也就是说一个用户线程对应一个内核线程。OS内核将每一个核心线程都调到系统CPU上。由于这种线程的创建与调度由内核完成,所以这种线程的系统开销比较大(但一般来说,比进程开销小)。

 (3)N:M混合线程模型

        LWP(light weight process):是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的

        N:M混合线程模型提供了两级控制,将用户线程映射为系统的可调度体LWP以实现并行,LWP再一一映射到内核线程。如下图所示。OS内核将每一个内核线程都调到系统CPU上,因此,所有线程实际上都直接和“系统范围”内的其他线程竞争。(M和N也可以都是1)

14.1.2 Linux线程库

        现代Linux默认使用的线程库是NPTL(Next Generation POSIX Threads),是采用1:1方式实现的。用户可以使用该命令查看当前系统使用的线程库:getconf GNU_LIBPTHREAD_VERSION

        NPTL的主要有以下几个优点:

  • 内核线程不再是一个进程,因此避免了很多用进程模拟内核线程导致的语义问题。
  • 摒弃了管理线程,终止线程、回收线程堆栈等工作都可以由内核来完成。
  • 一个进程的线程可以运行在不同的CPU上,能充分利用多处理器的系统的优势。
  • 线程的同步由内核完成。隶属于不同进程的线程之间也可能共享互斥锁,因此可实现跨进程的线程同步。

14.2 创建线程和结束线程

        Linux系统中,创建和结束线程的API都定义在pthread.h头文件中。

(1)pthread_create

        pthrea_create()用来创建一个线程:

#include <pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
参数解释:
thread:线程标识符
attr:设置新线程的属性。传递NULL表示使用默认属性
start_routine:指定新线程将运行的函数
arg:指定新城运行函数的参数
返回值:
成功时返回0,失败时返回错误码。

pthread_t的定义如下:
#include <bits/pthreadtypes.h>
typedef unsigned long int pthread_t;
实际上,Linux上几乎所有的资源标识符都是一个整型数,如socket、各种IPC标识符等

         一个用户可以打开的线程数量是有限的,系统上所有用户创建的线程总数不能超过/proc/sys/kernel/threads-max内核参数定义的值。

(2)pthread_exit

       在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出。线程一旦内创建好,内核就可以调度内核线程来执行start_routine函数指针所指的函数了。线程函数在结束时需调用pthrea_exit(),确保安全、干净的退出。

#include <pthread.h>
void pthread_exit(void* retval);
参数:
retval:可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。但是不能指向函数内部的局部数据。

它通过retval参数向线程的回首者传递其退出的信息。执行完之后不会返回到调用者,而且永远不会失败。

(3)pthread_join

        一个进程中的所有西安测绘给你都可以调用pthread_join来回收其他线程(如果目标线程可回收),即等待其他线程结束。类似于回收进程的wait()和waitpid()系统调用。

        该函数会一直阻塞,直到被回收的线程结束为止。

#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);
参数:
thread:目标线程的标识符
retval:目标线程返回的退出信息
返回值:
成功时返回0,失败则返回错误码如下表:

         使用代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include<unistd.h>

void* thread_main(void* arg)
{
    int cnt = *((int*)arg);
    char* msg = (char*)malloc(sizeof(char)*50);
    strcpy(msg, "hello, I'm thread\n");

    for(int i = 0; i < cnt; i++)
    {
        sleep(1);
        puts("running thread");
    }
    pthread_exit((void*)msg);
}

int main()
{
    pthread_t t_id;
    int thread_param = 3;
    void* thr_ret;

    if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
    {
        puts("pthread_create() error!");
        return -1;
    }
    if (pthread_join(t_id, &thr_ret)!=0)
    {
        puts("pthread_join() error!");
        return -1;
    }
    printf("thread return message:%s\n", (char*)thr_ret);
    free(thr_ret);
    return 0;
}
运行结果:
running thread
running thread
running thread
thread return message:hello, I'm thread

(4)pthread_cancel

        有时候希望异常终止同一进程中的一个线程,即取消线程:

#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数:
thread:目标线程标识符
返回值:
成功时返回0,失败则返回错误码。

         下面两个函数用来决定接收到取消信号的目标线程是否允许被取消以及如何取消:

#include <pthread.h>
int pthread_setcancelstate(int state, int* oldstate);
参数:
state:指定取消状态,有两个可选值:
    PTHREAD_CANCEL_ENABLE:允许线程被取消。它是线程被创建时的默认取消状态
    PTHREAD_CANCEL_DISABLE:禁止线程被取消。如果一个线程收到取消请求后,则它会将请求挂起,直到该线程允许被取消。
oldstate:记录线程原来的取消状态
返回值:
成功时返回0,失败时返回错误码。
#include <pthread.h>
int pthread_setcanceltype(int type, int* oldtype);
参数解释:
type:指定线程是否允许取消,有两个可选值:
    PTHREAD_CANCEL_ASYSNCHRONOUS:目标线程接收到取消操作后,立即取消。
    PTHREAD_CANCEL_DEFERRED:线程接收到取消操作后,直到运行到“可取消点函数”后取消。取消点函数包括下面几个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timedwait、
sem_wait、sigwait。其他可能阻塞的系统调用也可成为取消点,如:read、wait。为了安全,我们最好在可能
会被取消的代码中调用pthread_testcancel函数以设置取消点。
oldtype:记录线程原来的取消类型。
返回值:
成功时返回0,失败时返回错误码。

        代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include<unistd.h>

void* pthread_func(void* arg)
{
    int oldstate;
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);
    for(int i=0; i<10; i++)
    {
        printf("i = %d\n", i);
    }
    printf("oldstate = %d\n", oldstate);
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, pthread_func, NULL);
    pthread_cancel(tid);
    pthread_join(tid, NULL);
    return 0;
}
运行结果:
i = 0
说明线程被取消了。
如果将pthread_setcancelstate中的PTHREAD_CANCEL_ENABLE改成PTHREAD_CANCEL_DISABLE,结果如下:
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
oldstate = 0
说明取消线程失败。

(5) pthread_detach

        线程的分离状态决定一个线程以什么样的方式来终止自己。在默认情况下线程是非分离状态的,这种状态下,原有的线程等待创建的线程结束。只有当pthread_join()返回时,创建的线程才算终止,才能释放自己占用的系统资源。分析线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源,再调用pthread_join会返回错误。

分离线程
int pthread_detach(pthread_t thread);
成功返回0,失败返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include<unistd.h>

void* pthread_func1(void* arg)
{
    for(int i=0; i<3; i++)
    {
        printf("hahahahahaha\n");
        sleep(1);
    }
    pthread_exit(NULL);
}
int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, pthread_func1, NULL);
    pthread_detach(tid);    //让线程分离  --线程自动退出,无系统残留资源
    return 0;
}

 14.3 线程属性

        pthread_attr_t结构体定义了一套完整的线程属性:

#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
    char __size[__SIZEOF_PTHREAD_ATTR_T];
    long int __align;
}pthread_attr_t;

        由此可见,各种线程属性全部包含在一个字符数组中。线程库定义了一系列函数来操作pthread_attr_t类型的变量,以方便我们获取和设置线程属性,这些函数包括:

#include <pthread.h>
//初始化线程属性对象
int pthread_attr_init(pthread_attr_t* attr);
//销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用
int pthread_attr_destroy(pthread_attr_t* attr);

        下面这些函数用于获取和设置线程属性对象的某个属性:

#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);

int pthread_attr_getstackaddr(const pthread_attr_t* attr, void** stackaddr);
int pthread_attr_setstackaddr(pthread_attr_t* attr, void* stackaddr);

int pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize);
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);

int pthread_attr_getstack(const pthread_attr_t* attr, void** stackaddr, size_t* stacksize);
int pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize);

int pthread_attr_getguardsize(const pthread_attr_t* attr, size_t* guardsize);
int pthread_attr_setguardsize(pthread_attr_t* attr, size_t guardsize);

int pthread_attr_getschedparam(const pthread_attr_t* attr, struct sched_param* param);
int pthread_attr_setschedparam(pthread_attr_t* attr, const struct sched_param* param);

int pthread_attr_getschedpolicy(const pthread_attr_t* attr, int* policy);
int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy);

int pthread_attr_getinheritsched(const pthread_attr_t* attr, int* inherit);
int pthread_attr_setinheritsched(pthread_attr_t* attr, int inherit);

int pthread_attr_getscope(const pthread_attr_t* attr, int* scope);
int pthread_attr_setscope(pthread_attr_t* attr, int scope);

        下面来介绍每个线程属性的含义:

  • detachstate:线程的脱离状态。它可取如下2个值:

        PTHREAD_CREATE_JOINABLE:指定线程是可回收的

        PTHREAD_CREATE_DETACH:使调用线程脱离与进程中其他线程的同步。

这种线程称为“脱离线程”。脱离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE。我们也可以直接使用pthread_detach()直接将线程设为脱离线程。

  • stackaddrstacksize:线程堆栈的起始地址和大小。我们不需要自己来管理线程堆栈,因为Linux默认为每个线程分配了足够的堆栈空间(一般是8MB)。可以使用ulimt -s命令查看或修改这个默认值。
  • guardsize:保护区域大小。

        如果guardsize>0,则系统创建线程的时候会在其堆栈的尾部额外分配guardsize字节的空间,作为保护堆栈不被错误的覆盖的区域。

        如果guardsize=0,则系统不为新创建的线程设置堆栈保护区。

        如果使用者通过pthread_attr_setstackaddr或者pthread_attr_setstack函数手动设置线程的堆栈,则guardsize属性将被忽略。

  • schedparam:线程调度参数。其类型是sched_param结构体,结构体中唯一的成员sched_priority表示线程的运行优先级。
  • schedpolicy:线程调度策略。该属性可取以下3个值:

        SCHED_FIFO:采用先进先出的方法调度。

        SCHED_RR:采用采用轮转算法调度。它和SCHED_FIFO都具备实时调度功能,但只能用于以超级用户身份运行的进程。

        SCHED_OTHER:默认值。

  • inheritsched:是否继承调用线程的调度属性。该属性可取以下2个值:

        PTHREAD_INHERIT_SCHED:新线程沿用其创建者的线程调度参数,这种情况下再设置新线程的调度参数将没有任何效果。

        PTHREAD_EXPLICIT_SCHED:调用者要明确地指定新线程的调度参数。

  • scope:线程间竞争CPU的范围,即各线程在哪种范围内竞争“被调度的CPU时间”。该属性可取以下2值:

        PTHREAD_SCOPE_SYSTEM:表示目标线程与系统中所有线程一起竞争CPU的使用

        PTHREAD_SCOPE_PROCESS:表示目标线程仅与同进程内的其他线程竞争CPU的使用。

        目前Linux只支持PTHREAD_SCOPE_SYSTEM这一种取值。

        使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include<unistd.h>

void* pthread_func(void* arg)
{
    for(int i=0; i<3; i++)
    {
        printf("i = %d\n", i);
        sleep(1);
    }
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_attr_t attr = {0};
    pthread_attr_init(&attr);
    struct sched_param sched = {99};
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    pthread_attr_setschedparam(&attr, &sched);
    pthread_create(&tid, NULL, pthread_func, NULL);
    pthread_join(tid, NULL);
    int policy;
    pthread_attr_getschedpolicy(&attr, &policy);
    printf("policy = %d\n", policy);
    pthread_attr_destroy(&attr);
    return 0;
}
运行结果:
i = 0
i = 1
i = 2
policy = 1

14.4 线程间互斥与同步的实现

        在多线程环境中,对于共享资源,如果没有上锁,可能发生意想不到的错误。

        众所周知,线程是CPU调度的基本单位,进程是资源分配的基本单位。线程之间可以共享进程的资源,如:代码段、堆空间、数据段、打开的文件等,但每个线程都有自己独立的栈空间:

        那么问题来了,为了防止多个线程竞争共享资源产生错误,需要使用互斥的方式让线程访问共享资源,即:在任意时刻只能有一个线程可以访问共享资源,其他想访问共享资源的线程必须等待这个线程的访问结束。 

        此外,在程序中,多个线程需要密切合作,以实现一个共同的任务,这又涉及到了同步。同步就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。例如:你去餐厅吃饭,需要厨师做好饭你才能吃。在厨师做好饭之前,你必须阻塞等待,等厨师做好饭端到你面前,你才能进行吃饭。

        因此可以发现,互斥和同步是两个不同的概念:

  • 同步就好比:操作 A 应在操作 B 之前执行」或者「操作 C 必须在操作 A 和操作 B 都完成之后才能执行」等
  • 互斥就好比:「操作 A 和操作 B 不能在同一时刻执行」

14.4.1 POSIX信号量

        在Linux上,信号量API有两组。一组是第13章介绍过的IPC信号量,另外一组是即将讨论的POSIX信号量。这两种信号量的语义完全相同。POSIX信号量函数的名字都以sem_开头,常用的POSIX信号量函数有以下5个:

#include <semaphore.h>
//初始化信号量
int sem_init(sem_t* sem, int pshared, unsigned int value);
//销毁信号量
int sem_destroy(sem_t* sem);
//以原子操作的方式将信号量值减1,P操作
int sem_wait(sem_t* sem);
//sem_wait的非阻塞版本
int sem_trywait(sem_t* sem);
//以原子操作的方式将信号量值加1,V操作
int sem_post(sem_t* sem);

这些函数更加详细的介绍见13.5.2

        利用信号量实现生产者和消费者:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <semaphore.h>

#define NUM 5

int queue[NUM];     //全局数组实现环形队列
sem_t blank_num, product_num;   //空格子信号量,产品信号量

void *producer(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&blank_num);           //生产者将空格子数--,为0则阻塞等待
        queue[i] = rand() % 1000 + 1;   //生产一个产品
        printf("==== producer =%d\n", queue[i]);//临界资源
        sem_post(&product_num);         //将产品数++

        i = (i+1) % NUM;                //实现环形队列
        sleep(rand()%1);
    }
}

void* consumer(void* arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&product_num);           //消费者将空格子数--,为0则阻塞等待
        printf("==== consumer =%d\n", queue[i]);    //临界资源
        queue[i] = 0;                 //消费掉一个产品
        sem_post(&blank_num);         //消费掉后,将格子数++

        i = (i+1) % NUM;                //实现环形队列
        sleep(rand()%3);
    }
}

 int main()
 {

    pthread_t  pid, cid;
     
    sem_init(&blank_num, 0, NUM);   //初始化空格子信号量为NUM,线程间共享 
    sem_init(&product_num, 0, 0);   //产品数为0
     
    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);
          
    sem_destroy(&blank_num);
    sem_destroy(&product_num);
    return 0;
 }

14.4.2 互斥锁

        互斥锁(也称互斥量)可以用于保护关键代码段,以确保其独占式访问,这跟二进制信号量很像。当进入关键代码段时,需要获得互斥锁并将其加锁,这等价于二进制信号量的P操作;当离开关键代码段时,需要对互斥锁解锁,这等价于二进制信号量的V操作。

14.4.2.1 互斥锁基础API

        POSIX互斥锁的相关函数主要有以下5个:

#include <pthread.h>

初始化互斥锁。mutexattr指定互斥锁的属性,如果将它设置为NULL,表示使用默认属性。
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
还可以用这种方式初始化互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
宏PTHREAD_MUTEX_INITALIZER实际上只是把互斥锁的各个字段都初始化为0

销毁互斥锁,以释放其占用的内核资源。销毁一个已经加锁的互斥锁将导致不可预期的后果。
int pthread_mutex_destroy(pthread_mutex_t* mutex);

以原子操作的方式给一个互斥锁加锁。如果目标互斥锁已经被锁,则pthread_mutex_lock将阻塞,直到该互斥锁的占有者将其解锁。
int pthread_mutex_lock(pthread_mutex_t* mutex);

pthread_mutex_lock()的非阻塞版本。它始终立即返回,而不论被操作的互斥锁是否已经被加锁。当目标互斥锁未被加锁时,它对互斥锁执行加锁操作。当互斥锁已经被加锁时,它将返回错误码EBUSY。
(注意:这里讨论的pthread_mutex_lock和pthread_mutex_trylock的行为是针对普通锁而言的,对于其他类型的锁,这两个加锁函数会有不同的行为)
int pthread_mutex_trylock(pthread_mutex_t* mutex);

以原子操作的方式给一个互斥锁解锁。如果此时有其它线程正在等待这个互斥锁,则这些线程中的某一个将获得它。
int pthread_mutex_unlock(pthread_mutex_t* mutex);

参数解释:
mutex:要操作的目标互斥锁。
返回值:
成功时返回0,失败则返回错误码。

 代码示例:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
 
pthread_mutex_t mutex;
int num = 0;
void *add(void *arg){
    for(int i=0; i < 30; i++){
        pthread_mutex_lock(&mutex);
        num++;
        printf("num++ : %d\n",num);
        pthread_mutex_unlock(&mutex);
    }
}

void *des(void *arg){
    for(int i=0; i < 30; i++){
        pthread_mutex_lock(&mutex);
        num--;
        printf("num-- : %d\n", num);
        pthread_mutex_unlock(&mutex);
    }
}
int main(int argc,char** argv){
        pthread_t id1;
        pthread_t id2;
        pthread_mutex_init(&mutex, NULL);
        pthread_create(&id1,NULL, add, NULL);
        pthread_create(&id2,NULL, des, NULL);
        pthread_join(id1, NULL);
        pthread_join(id2, NULL);
        pthread_mutex_destroy(&mutex);
        return 1;
}

14.4.2.2 互斥锁属性

        pthread_mutexattr_t结构体定义了一套完整的互斥锁属性。可以通过如下函数来获取和设置互斥锁属性:

#include <pthread.h>

//初始化互斥锁属性对象
int pthread_mutexattr_init(pthread_mutexattr_t* attr);

//销毁互斥锁属性对象
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);

互斥锁有两种常用的属性:pshared和type。

//获取和设置互斥锁的pshared属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t* attr, int* pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t* attr, int pshared);

//获取和设置互斥锁的type属性
int pthread_mutexattr_gettype(const pthread_mutexattr_t* attr, int* type);
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);

参数解释:
pshared:指定是否允许跨进程共享互斥锁,可选值有2个:
    PTHREAD_PROCESS_SHARED:互斥锁可以被跨进程共享。
    PTHREAD_PROCESS_PRIVATE:互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
type:指定互斥锁的类型。Linux支持如下4种类型的互斥锁:
    PTHREAD_MUTEX_NORMAL:普通锁。是互斥锁默认的类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获得它。
    这种锁类型保证了资源分配的公平性。但容易引发问题:
        (1)一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;
        (2)对一个已经被其他线程加锁的普通锁解锁引发不可预期的后果;
        (3)对一个已经解锁的普通锁再次解锁,将引发不可预期的后果。
    PTHREAD_MUTEX_ERRORCHECK:检错锁。一个线程对一个已经加锁的检错锁加锁,则加锁操作返回EDEADLK;对已被其他线程加锁的检错锁解锁或对已解锁的检错锁解锁的操作会返回EPERM。
    PTHREAD_MUTEX_RECURSIVE:嵌套锁。一个线程在释放锁之前可多次对它加锁而不发生死锁。锁的拥有者必须执行相应次数的解锁操作,其他线程才能获取该锁。对一个已被其他线程加锁的嵌套锁解锁或对一个已解锁的嵌套锁解锁,解锁操作返回EPERM。
    PTHREAD_MUTEX_DEFAULT:默认锁。一个线程如果对一个已经加锁的默认锁加锁,或对一个已被其他线程加锁的默认所解锁,或对一个已解锁的默认锁解锁,会导致不可预期后果。这种锁在实现时可能被映射为上面三种锁之一。
#include <pthread.h>
#include <stdio.h>

int main()
{
    pthread_mutex_t mutex;
    pthread_mutexattr_t attr;
    
    pthread_mutexattr_init(&attr);  //初始化attr为默认属性
    int pshared;
    //获取互斥锁的pshared属性,默认为PTHREAD_PROCESS_PRIVATE,即0
    pthread_mutexattr_getpshared(&attr, &pshared);
    printf("pshared = %d\n", pshared);
    //设置互斥锁的pshared属性为PTHREAD_PROCESS_SHARED,即1
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);

    // 设置attr属性为PTHREAD_MUTEX_TIMED_NP,即默认属性(当一个线程加锁后,其余请求锁的线程形成等待队列,在解锁后按优先级获得锁)
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_TIMED_NP);
    int type;
    //获取互斥锁的type属性
    pthread_mutexattr_gettype(&attr, &type);
    printf("type = %d\n", type);
    pthread_mutex_init(&mutex, &attr);
    return 0;
}
运行结果:
pshared = 0
type = 0

14.4.2.3 死锁举例

        使用互斥锁的一个噩耗就是死锁。死锁使得一个或多个线程被挂起而无法继续执行,而且这种情况还不容易被发现。在一个线程中对一个已加锁的普通锁再次加锁,将导致死锁。此外,如果两个线程按照不同的顺序来申请两个互斥锁,也容易产生死锁。

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

void* another(void* arg)
{
    pthread_mutex_lock(&mutex_b);
    printf("in child thread, got mutex b, waiting for mutex a\n");
    sleep(5);
    ++b;
    pthread_mutex_lock(&mutex_a);
    b += a++;
    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);
    pthread_exit(NULL);
}

int main()
{
    pthread_t id;

    pthread_mutex_init(&mutex_a, NULL);
    pthread_mutex_init(&mutex_b, NULL);
    pthread_create(&id, NULL, another, NULL);

    pthread_mutex_lock(&mutex_a);
    printf("in parent thread, got mutex b, waiting for mutex a\n");
    sleep(5);
    ++a;
    pthread_mutex_lock(&mutex_b);
    a += b++;
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    
    return 0;
}
运行:
[root@localhost test3]# g++ deadLock.cpp -o deadLock -lpthread
[root@localhost test3]# ./deadLock 
in parent thread, got mutex b, waiting for mutex a
in child thread, got mutex b, waiting for mutex a
^C

        上面的代码中,主线程占用互斥锁mutex_a,子线程占用互斥锁mutex_b。主线程抱着mutex_a不释放去申请mutex_b,子线程抱着mutex_b不释放去申请mutex_a,最终谁也申请不到,导致死锁。

14.4.3 条件变量

        条件变量是利用线程间共享的变量进行同步的一种机制,是在多线程程序中用来实现"等待–>唤醒"逻辑常用的方法,用于维护一个条件,线程可以使用条件变量来等待某个条件为真,当条件不满足时,线程将自己加入等待队列,同时释放持有的互斥锁; 当一个线程唤醒一个或多个等待线程时,此时条件不一定为真(虚假唤醒)。

        为了避免多线程之间发生“抢夺资源”的问题,条件变量在使用过程中必须和一个互斥锁搭配使用。

        条件变量的相关函数主要有如下5个,成功时返回0,失败时返回错误码:

(1)初始化条件变量

#include <pthread.h>

int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
还可以使用下面的方式初始化一个条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
宏PTHREAD_COND_INITIALIZER实际上只是把条件变量的各个字段都初始化为0

参数解释:
cond:指向要操作的目标条件变量
cond_attr:指定条件变量的属性。如果为NULL,则使用默认属性。条件变量的属性和互斥锁属性相似。

返回值:
成功时返回0,失败时返回错误码。

 (2)阻塞当前线程,等待条件成立

        当条件不成立时,条件变量可以阻塞当前线程,所有被阻塞的线程会构成一个等待队列。阻塞线程可以借助以下两个函数实现:

int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime); 

参数解释:
cond :表示已初始化好的条件变量
mutex :表示与条件变量配合使用的互斥锁
abstime :表示阻塞线程的时间
注意,abstime 参数指的是绝对时间,例如您打算阻塞线程 5 秒钟,那么首先要得到当前系统的时间,然后再加上 5 秒,最终得到的时间才是传递的实参值。

         调用两个函数之前,我们必须先创建好一个互斥锁并完成“加锁”操作,然后才能作为实参传递给 mutex 参数两个函数会完成以下两项工作:

  • 阻塞线程,直至接收到“条件成立”的信号;
  • 当线程被添加到等待队列上时,将互斥锁“解锁”。

也就是说,函数尚未接收到“条件成立”的信号之前,它将一直阻塞线程执行。注意,当函数接收到“条件成立”的信号后,它并不会立即结束对线程的阻塞,而是先完成对互斥锁的“加锁”操作,然后才解除阻塞。

        两个函数都以“原子操作”的方式完成“阻塞线程+解锁”或者“重新加锁+解除阻塞”这两个过程。所谓“原子操作”,即当有多个线程执行相同的某个过程时,虽然它们都会访问互斥锁和条件变量,但之间不会相互干扰。

        两个函数的区别:

pthread_cond_wait() 函数可以永久阻塞线程,直到条件变量成立的那一刻;pthread_cond_timedwait() 函数只能在 abstime 参数指定的时间内阻塞线程,超出时限后,该函数将重新对互斥锁执行“加锁”操作,并解除对线程的阻塞,函数的返回值为 ETIMEDOUT。

        如果函数成功接收到了“条件成立”的信号,重新对互斥锁完成了“加锁”并使线程继续执行,函数返回数字 0,反之则返回非零数。

(3) 解除线程的“阻塞”状态

        对于被 pthread_cond_wait() 或 pthread_cond_timedwait() 函数阻塞的线程,我们可以借助如下两个函数向它们发送“条件成立”的信号,解除它们的“被阻塞”状态:

int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);

参数解释:
cond :表示初始化好的条件变量。
当函数成功解除线程的“被阻塞”状态时,返回数字 0,反之返回非零数。

        两个函数都能解除线程的“被阻塞”状态,区别在于:

  • pthread_cond_signal() 函数至少解除一个线程的“被阻塞”状态,如果等待队列中包含多个线程,优先解除哪个线程将由操作系统的线程调度程序决定;
  • pthread_cond_broadcast() 函数可以解除等待队列中所有线程的“被阻塞”状态。

         由于互斥锁的存在,解除阻塞后的线程也不一定能立即执行。当互斥锁处于“加锁”状态时,解除阻塞状态的所有线程会组成等待互斥锁资源的队列,等待互斥锁“解锁”。

(4) 销毁条件变量

        对于初始化好的条件变量,我们可以调用 pthread_cond_destory() 函数销毁它。

int pthread_cond_destroy(pthread_cond_t *cond);

参数解释:
cond :表示要销毁的条件变量。
如果函数成功销毁 cond 参数指定的条件变量,返回数字 0,反之返回非零数。

        值得一提的是,销毁后的条件变量还可以调用 pthread_cond_init() 函数重新初始化后使用。 

        条件变量使用示例:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
//初始化互斥锁
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
//初始化条件变量
pthread_cond_t myCond = PTHREAD_COND_INITIALIZER;
//设置全局变量
int x = 0;
//线程执行的函数
void * waitForTrue(void *args) {
    int res;
    //条件变量阻塞线程之前,先对互斥锁执行“加锁”操作
    res = pthread_mutex_lock(&myMutex);
    if (res != 0) {
        printf("waitForTrue 加锁失败\n");
        return NULL;
    }
    printf("------等待 x 的值为 10\n");
    if (pthread_cond_wait(&myCond, &myMutex) == 0) {
        printf("x = %d\n", x);
    }
    //最终将互斥锁解锁
    pthread_mutex_unlock(&myMutex);
    return NULL;
}
//线程执行的函数
void * doneForTrue(void *args) {
    int res;

    while (x != 10) {
        //对互斥锁执行“加锁”操作
        res = pthread_mutex_lock(&myMutex);
        if (res == 0) {
            x++;
            printf("doneForTrue:x = %d\n", x);
            sleep(1);
            //对互斥锁“解锁”
            pthread_mutex_unlock(&myMutex);
        }
    }
    //发送“条件成立”的信号,解除 mythread1 线程的“被阻塞”状态
    res = pthread_cond_signal(&myCond);
    if (res != 0) {
        printf("解除阻塞失败\n");
    }
    return NULL;
}
int main() {
    int res;
    pthread_t mythread1, mythread2;
    res = pthread_create(&mythread1, NULL, waitForTrue, NULL);
    if (res != 0) {
        printf("mythread1线程创建失败\n");
        return 0;
    }
    res = pthread_create(&mythread2, NULL, doneForTrue, NULL);
    if (res != 0) {
        printf("mythread2线程创建失败\n");
        return 0;
    }
    //等待 mythread1 线程执行完成
    res = pthread_join(mythread1, NULL);
    if (res != 0) {
        printf("1:等待线程失败\n");
    }
    //等待 mythread2 线程执行完成
    res = pthread_join(mythread2, NULL);
    if (res != 0) {
        printf("2:等待线程失败\n");
    }
    //销毁条件变量
    pthread_cond_destroy(&myCond);
    return 0;
}
执行结果:
------等待 x 的值为 10
doneForTrue:x = 1
doneForTrue:x = 2
doneForTrue:x = 3
doneForTrue:x = 4
doneForTrue:x = 5
doneForTrue:x = 6
doneForTrue:x = 7
doneForTrue:x = 8
doneForTrue:x = 9
doneForTrue:x = 10
x = 10

         程序中共创建了 2 个线程 mythread1 和 mythread2,其中 mythread1 线程借助条件变量实现了“直到变量 x 的值为 10 时,才继续执行后续代码”的功能,mythread2 线程用于将 x 的变量修改为 10,同时向 mythread1 线程发送“条件成立”的信号,唤醒 mythread1 线程并继续执行。

14.5 多线程环境

14.5.1 可重入函数

        如果一个函数能被多个线程同时调用且不发生竞态条件,则我们称它是线程安全的,或者说它是可重入函数。Linux库函数只有小部分是不可重入的,如:inet_ntoa、getservbyname、getservbyport。不可重入主要因为其内部使用了静态变量。很多不可重入函数都有对应的可重入版本(原函数尾部加_r),如:localtime的可重入函数是localtime_t。

        在多线程程序中调用库函数,一定要使用其可重入版本。

14.5.2 线程和进程

        如果一个多线程的程序的某个线程调用的fork函数,那么新创建的子进程不会自动创建和父进程相同数据的线程。子进程只拥有一个执行线程,该线程是调用fork的哪个线程的完整复制。而且子进程将自动继承父进程中的互斥锁(或条件变量、信号量)的状态。如:父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就引起一个问题:子进程可能不清楚从父进程继承而来的互斥锁的状态(是加锁状态还是解锁状态),这个互斥锁可能不是由调用fork函数的哪个线程锁住的,而是由其他线程锁住的。如果是这种情况,则子进程若再次对该互斥锁加锁就会导致死锁。代码如下:

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>

pthread_mutex_t mutex;

void* another(void* arg)
{
    printf("in child thread, lock the mutex\n");
    pthread_mutex_lock(&mutex);
    sleep(5);
    pthread_mutex_unlock(&mutex);
}

int main()
{
    pthread_mutex_init(&mutex, NULL);
    pthread_t id;
    pthread_create(&id, NULL, another, NULL);
    //父进程中的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex
    sleep(1);
    pid_t pid = fork();
    if (pid < 0)
    {
        pthread_join(id, NULL);
        pthread_mutex_destroy(&mutex);
        return 1;
    }
    else if (pid == 0)
    {
        printf("I am in the child process, want to get the lock\n");
        //子进程从父进程继承了互斥锁mutex的状态,该锁处于锁住状态,且是由父进程中的子线程执行的加锁操作。
        //因此下面的加锁操作会产生死锁
        pthread_mutex_lock(&mutex);
        printf("I can not run to here,... \n");
        pthread_mutex_unlock(&mutex);
        exit(0);
    }
    else
    {
        wait(NULL);
    }
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}
运行结果:
[root@localhost test3]# ./lock 
in child thread, lock the mutex
I am in the child process, want to get the lock
^C

        不过,pthread提供了一个专门的函数pthread_atfork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态。

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

pthread_atfork()在fork()之前调用。
当调用fork时,内部创建子进程前在父进程中会调用prepare;
内部创建子进程成功后且fork返回前,父进程会调用parent,子进程会调用child。

用pthread_atfork之后的代码如下:

#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <wait.h>

pthread_mutex_t mutex;

void* doit(void *arg)
{
    printf("pid = %d ******* begin doit *******\n", getpid());
    pthread_mutex_lock(&mutex);
    sleep(1);
    pthread_mutex_unlock(&mutex);
    printf("pid = %d ******* end doit *******\n", getpid());
}
void prepare()
{
    pthread_mutex_unlock(&mutex);
}
void parent()
{
    pthread_mutex_lock(&mutex);
}
int main()
{
    pthread_mutex_init(&mutex, NULL);

    pthread_atfork(prepare, parent, NULL);
    printf("pid = %d Enter main ...\n", getpid());
    pthread_t tid;
    pthread_create(&tid, NULL, doit, NULL);
    sleep(1);
    pid_t pid = fork();
    if (pid < 0)
    {
        pthread_join(tid, NULL);
        pthread_mutex_destroy(&mutex);
        return 1;
    }
    else if (pid == 0)
    {
        doit(NULL);
    }
    else
    {
        wait(NULL);
    }
    pthread_join(tid, NULL);
    printf("pid = %d Exit main ...\n", getpid());
    pthread_mutex_destroy(&mutex);
    return 0;
}
运行结果:
[root@localhost test3]# ./lock 
pid = 27018 Enter main ...
pid = 27018 ******* begin doit *******
pid = 27021 ******* begin doit *******
pid = 27018 ******* end doit *******
pid = 27021 ******* end doit *******
pid = 27021 Exit main ...
pid = 27018 Exit main ...
[root@localhost test3]# 

在执行fork() 创建子进程之前,先执行prepare(), 将子线程加锁的mutex 解锁下,然后为了与doit() 配对,在创建子进程成功后,父进程调用parent() 再次加锁,这时父进程的doit() 就可以接着解锁执行下去。而对于子进程来说,由于在fork() 创建子进程之前,mutex已经被解锁,故复制的状态也是解锁的,所以执行doit()就不会死锁了

14.5.3 线程和信号

        每个线程都可以独立地设置信号掩码。第10章介绍过设置进程信号掩码的函数sigprocmask,下面介绍设置线程信号掩码的函数pthread_sigmask

#include <pthread.h>
#include <signal.h>

int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);
参数解释:
该函数的参数与sigprocmask()的参数完全相同。

返回值:
成功时返回0,失败返回错误码。

        由于进程中所有的线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。此外,所有线程共享信号处理函数,也就是说,当在一个线程中设置了某个信号的信号处理函数后,它也成为了其他线程中该信号的处理函数。这两点说明,应该定义一个专门的线程来处理所有的信号。这可通过如下两个步骤实现:

(1)在主线程创建出其他子线程之前就调用pthread_sigmask来设置信号掩码。所有新创建的子线程继承这个信号掩码。这样使得所有线程都无法接受被屏蔽的信号了。

(2)在某个线程中调用sigwait函数来等待信号并处理之。这样就可以灵活指定那个线程可以接受哪些信号了。

#include <signal.h>
int sigwait(const sigset_t* set, int* sig);

参数解释:
set:指定需要等待的信号的集合。
sig:指向的整数用于存储该函数返回的信号值。

返回值:
成功时返回0,失败则返回错误码。

        sigwait()是用来解除阻塞的信号,函数所监听的信号在之前必须被阻塞,函数将阻塞调用他的线程,直到收到它所监听的信号发生了。sigwait()所做的工作只有两个:第一,监听被阻塞的信号;第二,如果所监听的信号产生了,则将其从未决队列中移出来。sigwait()并不改变信号掩码的阻塞与非阻塞状态。

        下面展示使用上述2步骤实现在一个线程中统一处理所有信号:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

#define handle_error_en(en, msg) \
do {errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

static void* sig_thread(void* arg)
{
    sigset_t* set = (sigset_t*) arg;
    int s, sig;
    for(; ;)
    {
        //第二个步骤,调用sigwait等待信号
        s = sigwait(set, &sig);
        if (s != 0)
        {
            handle_error_en(s, "sigwait");
        }
        printf("signal handling thread got signal %d\n", sig);
    }
}

int main()
{
    pthread_t tid;
    sigset_t set;
    int s;

    //第一个步骤,在主线程中设置信号掩码
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);   //添加退出进程CTRL + \信号
    sigaddset(&set, SIGUSR1);   //添加用户自定义信号
    s = pthread_sigmask(SIG_BLOCK, &set, NULL); //往当前信号屏蔽集中加入set
    if (s !=0)
        handle_error_en(s, "pthread_sigmask");

    s = pthread_create(&tid, NULL, &sig_thread, (void*)&set);
    if (s != 0)
        handle_error_en(s, "pthread_create");

    pause();
    return 0;
}
运行结果:
[root@localhost test3]# ./sigmask 
^\signal handling thread got signal 3

        最后,pthread还提供了下面的方法,使得我们可以明确地将一个信号发送给指定的线程:

#include <signal.h>
int pthread_kill(pthread_t thread, int sig);

参数解释:
thread:指定目标线程ID
sig:指定待发送的信号。如果sig=0,则pthread_kill()不发送信号。我们可以用这种方式检测目标线程是否存在。

返回值:
成功返回0,失败返回错误码。

        下面用该函数检测子线程是否存在:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<signal.h>
#include<errno.h>
 
void* thread_fun(void* arg)
{
    printf("i am new thread.\n");
}
 
int main()
{
    pthread_t tid;
    int err;
    int res_kill;
 
    err = pthread_create(&tid, NULL, thread_fun, NULL);
    if(err != 0)
    {   
        printf("new thread create is failed.\n");
        return 0;
    }   
    sleep(1);
    res_kill = pthread_kill(tid, SIGQUIT);
    if(res_kill == ESRCH)
    {   
        printf("new thread tid is not found.\n");
        printf("ret_kill = %d\n",res_kill);
    }

    int thread_join = pthread_join(tid, NULL);
    printf("i am main thread .\n");
    return 0;
}
运行结果:
[root@localhost test3]# ./threadKill 
i am new thread.
new thread tid is not found.
ret_kill = 3
i am main thread .

        运行结果表明:子线程已经不存在,因为子线程创建出来后瞬间就执行完毕然后

14.6 线程私有数据

        在多线程程序中,所有线程共享进程中的变量。现在有一全局变量,所有线程都可以使用它,改变它的值。而如果每个线程希望能单独拥有它,那么就需要使用线程存储了。
表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独存储的。这就是线程存储(线程私有数据 Thread- specific Data,或TSD)的意义。

        线程存储是实现同一个线程中不同函数间共享数据的一种很好的方式。

线程存储具体用法

  • 创建一个类型为pthread_key_t类型的变量。
  • 调用pthread_key_create()来创建该变量。该函数有两个参数,第一个参数就是上面声明的pthread_key_t变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成NULL,这样系统将调用默认的清理函数。该函数成功返回0,其他任何返回值都表示出现了错误。
  • 当线程中需要存储特殊值的时候,可以调用pthread_setspcific()。该函数有两个参数,第一个为前面声明的pthread_key_t变量,第二个为void*变量,这样你可以存储任何类型的值。
  • 如果需要取出所存储的值,调用pthread_getspecific()。该函数的参数为前面提到的pthread_key_t变量,该函数返回void *类型的值。

函数原型

int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
pthread_key_delete(key);

使用例子

struct test_struct { // 用于测试的结构
	int i;
	float k;
};

void *child1(void *arg)
{
	struct test_struct struct_data; // 首先构建一个新的结构
	struct_data.i = 10;
	struct_data.k = 3.1415;
	pthread_setspecific(key, &struct_data); // 设置对应的东西吗?
	printf("child1--pthread_getspecific(key):struct_data.i:%d, struct_data.k: %f\n",
		((struct test_struct *)pthread_getspecific(key))->i, ((struct test_struct *)pthread_getspecific(key))->k);
}
void *child2(void *arg)
{
	int temp = 20;
	sleep(2);
	pthread_setspecific(key, &temp); // 好吧,原来这个函数这么简单
	printf("child2--pthread_getspecific(key):%d\n", *((int *)pthread_getspecific(key)));
}
int main(void)
{
	pthread_t tid1, tid2;
	pthread_key_create(&key, NULL); // 这里是构建一个pthread_key_t类型,确实是相当于一个key
	pthread_create(&tid1, NULL, child1, NULL);
	pthread_create(&tid2, NULL, child2, NULL);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	pthread_key_delete(key);
	return (0);
}
运行结果:
child1--pthread_getspecific(key):struct_data.i:10, struct_data.k: 3.141500
child2--pthread_getspecific(key):20

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值