1.条件变量
条件变量 是线程同步 的一种机制。通过pthread_cond_t 来定义出条件变量。条件变量的理解 :所谓条件变量,就是在使用时程序能否向下执行就需要一个满足的条件来判断 ,条件变量的个数 可以通过应用程序中的条件个数进行定义。
pthread_cond_init 函数的作用是初始化条件变量 ,第一个参数 是条件变量的地址 ,第二个参数 是条件变量的属性 一般使用默认属性即传入参数NULL。
pthread_cond_destroy 函数的作用是销毁条件变量 ,参数 是需要销毁的条件变量地址。
pthread_cond_wait 函数的作用是申请/获取条件变量 ,当线程应用程序中出现某个条件时执行到该函数后就被阻塞 啦。第一个参数 是条件变量的地址 ,第二个参数 是互斥锁的地址 ,条件变量需要与互斥锁共同作用来实现线程同步以及生产者消费者模型。
pthread_cond_timedwait 函数的作用也是申请/获取条件变量 ,只是多了一个超时判定机制 当线程被阻塞到一定时间后,若还没有被唤醒则该线程会自动苏醒去执行别的程序 。第一个参数 是条件变量的地址 ,第二个参数 是结构体指针 ,该结构体是一个与时间相关 的结构体其时间是从1970年1月1日,我们要设置超时时间就需要通过time来获取到现在的时间 戳将其加上所要等待的超时时间然后写入结构体的第一个秒数的变量 中,第二个纳秒数的变量一般不用但是要设置为0 ,因为总的等待时间是二者相加。
pthread_cond_signal 函数的作用是唤醒至少一个阻塞的线程 来进行往下执行。参数 是条件变量的地址。
pthread_cond_broadcast 函数的作用是唤醒所有的阻塞线程 ,参数 是条件变量的地址。
2.信号量
信号量 也可以实现线程同步 ,同时也可以用来实现生产者消费者模型 。通过sem_t来定义。对信号量的理解:所谓信号就是可以在线程间有一个携带信息传递的作用,所谓量就是信号量是一个数量,所设定的数量就是刚开始线程所拥有的资源。
sem_init 函数的作用是初始化信号量 ,第一个参数 是信号量的地址 ,第二个参数 是一个整型值 当该整型值为0时 表示该信号量在同一进程不同线程间使用 ,当为非0值时 表示在不同的进程间使用 。所以信号量也可以在进程间使用,第三个参数 也是一个整型值 ,用于设置该信号量的初始值 ,当设置为1时可以实现线程同步。
sem_destroy 函数的作用是销毁信号量 ,参数 是信号量的地址。
sem_wait 函数的作用是申请/获取信号量 ,执行一次该函数对应的信号量的值减一 ,当该信号量的值减为0后,线程会被阻塞。参数 是信号量的地址。
sem_trywait 函数的作用是尝试申请/获取信号量 ,同样执行一次该函数对应的信号量的值减一 ,但是当该值减到0后,线程不会被阻塞而是回去执行别的程序 。参数 是信号量的地址。
sem_post 函数的作用是释放信号量 ,执行一次该函数对应的信号量的值就会增加1 ,当信号量的值增加到不为0后被阻塞的线程就可以抢占其中的信号量来继续向下执行程序。参数 是信号量的地址。
3.生产者消费者模型
生产者消费者模型 指的是有一个指定的容器 ,生产者向容器里面生产数据消费者消费容器里面的数据 ,当然生产者可以有多个消费者也可以有多个,在多线程环境下就是多个线程。而条件变量、互斥锁、信号量 等就保证了其中数据的准确性和安全性。下面我们来详细解释一下两段程序来理解生产者和消费者模型:
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
//mutex and cond
pthread_mutex_t mutex;
pthread_cond_t cond;
//list node
struct Node
{
int number;
struct Node* next;
};
//head node
struct Node *head;
//producer thread task
void* producer(void*)
{
while(1)
{
//lock mutex
pthread_mutex_lock(&mutex);
//malloc Node
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->next = head;
node->number = rand()%1000;
head = node;
printf("producer id:%ld,number:%d\n",pthread_self(),head->number);
//unlock mutex
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&cond);
sleep(rand()%3);
}
}
//consumer thread task
void* consumer(void*)
{
while(1)
{
//lock mutex
pthread_mutex_lock(&mutex);
//wait
while(head == NULL)
{
pthread_cond_wait(&cond,&mutex);
}
//free Node
struct Node *node = head;
printf("consumer id:%ld,number:%d\n",pthread_self(),node->number);
head = node->next;
free(node);
//unlock mutex
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
}
int main(int argc, char const *argv[])
{
//init mutex and cond
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
//create producer and consumer thread
pthread_t p1[5],p2[5];
for (int i = 0; i < 5; i++)
{
pthread_create(&p1[i],NULL,producer,NULL);
}
for (int i = 0; i < 5; i++)
{
pthread_create(&p2[i],NULL,consumer,NULL);
}
//recycle producer and consumer thread
for (int i = 0; i < 5; i++)
{
pthread_join(p1[i],NULL);
}
for (int i = 0; i < 5; i++)
{
pthread_join(p2[i],NULL);
}
//destroy mutex and cond
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
在该段程序中我们使用链表 来表示生产者和消费者共同的共享数据 。因为这个链表是一个共享的数据所以我们要通过加互斥锁来实现线程同步 ,我们可以知道该链表有一个边界线即当链表为空 ,所以我们需要通过定义一个条件变量来限制这个边界 即当链表为空时要阻塞消费者线程让生产者线程来生产数据,当生产者生产出了数据后我们就可以通过条件变量来唤醒 消费者线程继续消费。以上就是通过条件变量和互斥锁实现的一个生产者消费者模型。
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <semaphore.h>
//sem
sem_t semp;
sem_t semc;
//mutex
pthread_mutex_t mutex;
//list node
struct Node
{
int number;
struct Node* next;
};
//head node
struct Node *head = NULL;
//producer thread task
void* producer(void*)
{
while(1)
{
//wait producer sem
sem_wait(&semp);
//lock mutex
pthread_mutex_lock(&mutex);
//malloc Node
struct Node *node = (struct Node*)malloc(sizeof(struct Node));
node->next = head;
node->number = rand()%1000;
head = node;
printf("producer id:%ld,number:%d\n",pthread_self(),head->number);
//unlock mutex
pthread_mutex_unlock(&mutex);
//post consumer sem
sem_post(&semc);
sleep(rand()%3);
}
}
//consumer thread task
void* consumer(void*)
{
while(1)
{
//wait consumer sem
sem_wait(&semc);
//lock mutex
pthread_mutex_lock(&mutex);
//free Node
struct Node *node = head;
printf("consumer id:%ld,number:%d\n",pthread_self(),node->number);
head = node->next;
free(node);
//unlock mutex
pthread_mutex_unlock(&mutex);
//post producer sem
sem_post(&semp);
sleep(rand()%3);
}
}
int main(int argc, char const *argv[])
{
//init sem
sem_init(&semp,0,5);
sem_init(&semc,0,0);
//init mutex
pthread_mutex_init(&mutex,NULL);
//create producer and consumer thread
pthread_t p1[5],p2[5];
for (int i = 0; i < 5; i++)
{
pthread_create(&p1[i],NULL,producer,NULL);
}
for (int i = 0; i < 5; i++)
{
pthread_create(&p2[i],NULL,consumer,NULL);
}
//recycle producer and consumer thread
for (int i = 0; i < 5; i++)
{
pthread_join(p1[i],NULL);
}
for (int i = 0; i < 5; i++)
{
pthread_join(p2[i],NULL);
}
//destroy sem
sem_destroy(&semp);
sem_destroy(&semc);
//destory mutex
pthread_mutex_destroy(&mutex);
return 0;
}
在该段程序中我们定义了两个信号量,一个信号量是生产者资源,一个信号量是消费者资源初始化时消费者资源为0 因为生产者还没有生产数据所以没有消费资源。当有生产者生产数据后生产者的资源减1 ,生产者生产出资源后我们就可以使用函数使消费者信号量加1 ,消费者处理同理。这里就时使用的信号量和互斥锁实现的生产者消费者模型。
4.信号的概念和常见信号
信号是当某个事件发生时对进程的一种通知机制 。信号的处理 主要有三种:忽略信号 、捕获信号、交由系统进行信号的默认处理 。信号的本质 就是一个整型的数字编号。
信号可以分为可靠信号和不可靠信号 ,可靠信号和不可靠信号的主要分类依据 是该信号会不会丢失 ,其中信号根据时效性 也可以分为实时信号和非实时信号 ,实时信号和可靠信号对应,非实时信号和不可靠信号对应,不可靠信号也是标准信号。
常见的信号及其功能有:SIGINT 该信号是中断信号 ,当在终端按下中断字符即ctrl+c 时系统内核就会向前台进程发送一个SIGINT信号 或者也可以由其他进程通过kill命令来发送信号 ,该信号的默认操作是终止该进程, 前台进程指的是运行在终端的进程 ,SIGQUIT 该信号是退出信号 ,当在终端按下退出即ctrl+\ 时系统内核就会向前台进程发送一个SIGQUIT信号或者也可以由其他进程通过kill命令 发送信号,该信号的默认操作也是终止该进程 。SIGKILL 信号该信号不可以由进程忽略捕获或阻塞 ,该信号的操作就是终止该进程 也是必杀信号 。SIGTERM 信号是kill命令默认发送的信号 ,该信号的默认操作也是终止进程 。SIGTSTP 信号的默认操作是暂停或停止进程 。SIGSTOP 信号的默认操作也是暂停或停止进程 ,且该信号不能被进程忽略捕获和阻塞 ,是一个必停信号 。SIGCONT 信号是继续信号 ,该信号的默认操作是将恢复被停止的信号 。SIGCHLD 信号该信号是当子进程的状态 改变如子进程被终止 、子进程被停止或恢复 都内核都会向子进程的父进程发送的一个信号。
signal 函数的作用是为信号注册一个信号处理函数 ,第一个参数 是需要处理的信号 ,第二个参数 是一个函数指针 用来指向信号的处理函数,当填入的是函数指针时 表示该信号的处理方式为捕获信号 ,系统还提供了两个宏来进行填写SIG_DFL 表示该信号的处理方式为交由系统进行信号的默认处理 方式,SIG_IGN 表示该信号的处理方式是忽略该信号 ,其返回值是该信号先前的信号处理函数指针 。sigaction 函数的作用也是为信号注册一个信号处理函数 并且可以不改变当前信号的处理方式来获取到先前信号的处理方式 ,第一个参数 是需要处理的信号 ,第二个参数 是一个结构体指针 该结构体中就包含了一个信号处理函数的指针,对于这类参数的传递我们可以通过先定义 一个结构体然后将其结构体内容赋值 后再将该结构体指针填入到参数 中即可,第三个参数 是一个和第二个参数一样的结构体指针 当需要获取到该信号的先前处理方式时就可以通过该参数传递出来,成功返回0失败返回-1.
kill 函数的作用是用来向指定进程发送一个信号 ,第一个参数 是进程号 ,第二个参数 是需要发送的信号 。raise 函数的作用是向本进程发送一个信号 ,参数 是需要发送的信号。
alarm函数的作用是给进程设定一个闹钟 ,参数 是要设定的定时秒数 ,返回值 是当前面设定的闹钟还没有到达时间时再次给该进程设定一个闹钟返回的就是前面闹钟剩余的秒数 ,一个进程只允许设定一个闹钟且闹钟是一次性的 ,要是想要得一个循环的闹钟则可再信号处理函数中再次设定。pause 函数的作用是暂停该进程 ,当该进程接收了信号 之后该进程就会恢复当然若该信号被阻塞了或者被忽略了则不会恢复,没有参数。
信号集 是一组信号的集合 ,是一个数据结构。sigemptyset 函数的作用是设置该信号集为空 ,参数 是信号集的地址 。sigfillset 函数的作用是将所有标准信号填入到信号集 ,参数 是信号集的地址 。sigaddset 函数的作用是向信号集里面添加信号 ,第一个参数 是信号 ,第二个参数 是信号集地址 。sigdelset 函数的作用是从信号集里面删除信号 ,第一个参数 是信号 ,第二个参数 是信号集地址。sigismember 函数的作用是判断一个信号是否在该信号集中 ,第一个参数 是需要判断的信号 ,第二个参数 是信号集的地址。
当一个进程正在处理信号时此时又来了一个信号,那么对该信号的处理方式就有两种一个是打断 原来信号的处理来处理现在的信号,一个是后面的信号阻塞等待 。而阻塞等待的实现就依赖于信号掩码 ,当一个信号被进程处理时 该信号就会自动被添加 到信号掩码中或者可以手动进行其他信号的添加,进程处理完该信号时进程就会自动将该信号移除 出信号掩码中。sigprocmask 函数的作用是在信号掩码中添加或删除一个信号 ,第一个参数 是该函数的操作指向,有三个宏SIG_BLOCK添加信号到信号掩码中 、SIG_UNBLOCK从信号掩码中删除信号 、SIG_SETMASK替换整个信号掩码 ,第二个参数 是信号集地址 ,返回值成功返回0失败返回-1 .sigpending 函数的作用是获取到进程中等待的信号 ,参数 是信号集地址,获取到的等待的信号都被存放在信号集中。
实时信号 的编号 是从34到64 一共有31个实时信号,实时信号相较于标准信号的优势 是实时信号都可以在应用程序中自定义 、实时信号实行队列化管理 不会造成信号的丢失、实时信号可以传递伴随数据 携带更多的信息量、实时信号的传递顺序 会按编号从小到大 排列当编号一样时则按照信号发送顺序排序。sigqueue 函数的作用是向进程发送一个信号该信号可以携带一个伴随数据 ,第一个参数 是进程号 ,第二个参数 是信号 ,第三个参数 是伴随数据结构体指针 ,该结构体里面有一个int类型的整型数据 和一个指针 。实时信号的接收必须通过sigaction函数来接收 ,在该函数的第二个参数的结构体中可以通过结构体中的内容来指定处理该实时信号的信号处理函数。
abort 函数的作用就是异常终止该进程 ,正常终止有return返回 、使用库函数或系统调用exit()和_exit()来终止进程 ,异常终止 就是该进程还没有进行内存释放关闭文件描述符等操作就被终止了 ,异常终止有信号终止 、abort函数终止。