1、线程的同步机制
线程同步,提前已经知道线程的执行顺序,让线程顺序执行的过程就是同步。
典型的就是生产者和消费者模型。
1.1、无名信号量API
1 >> 定义无名信号量:sem_t sem
2 >>初始化无名信号量:sem_init()
3 >> 获取信号量(P操作):sem_wait() (获取不到就堵塞)
4 >> 释放信号量(V操作):sem_post()
5 >> 销毁信号量:sem_destory()
#include <head.h>
sem_t sem1, sem2;
void *task1(void *arg){
printf("生产者线程\n");
while(1){
sleep(1);
sem_wait(&sem2);
printf("生产了一辆汽车\n");
sem_post(&sem1);
}
}
void *task2(void *arg){
printf("消费者线程\n");
while(1){
sem_wait(&sem1);
printf("购买了一辆汽车\n");
sem_post(&sem2);
}
}
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2;
//中间的参数表示:0:两个线程同步,1:两个进程同步
//最后一个参数:非0:表示线程可以获取信号量,0:表示线程获取不到信号量,将阻塞
sem_init(&sem1, 0, 0);
sem_init(&sem2, 0, 1);
if(pthread_create(&tid1, NULL, task1, NULL)){
perror("create thread 1 error");
return -1;
}
if(pthread_create(&tid2, NULL, task2, NULL)){
perror("create thread 2 error");
return -1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&sem1);
sem_destroy(&sem2);
return 0;
}
执行结果:
1.2、条件变量API
条件变量要与互斥锁一块使用
1 >> 定义条件变量:pthread_cond_t cond
2 >> 初始化条件变量:pthread_cond_init()
3 >> 获取条件变量:pthread_cond_wait()
使用流程:
1.先获取互斥锁
2.调用pthread_cond_wait
2.1将当前的线程放到队列中
2.2解锁
2.3在队列中休眠
2.4重新获取锁
2.5从队列中移除
3.执行你的代码
4.解锁4 >> 释放条件变量:pthread_cond_signal()(释放一个资源)
pthread_cond_broadcast()(释放所有的资源)
5 >> 销毁条件变量:pthread_cond_destroy()
#include <head.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void *task1(void *arg){
while(1){
sleep(2);
printf("生产了4辆汽车\n");
pthread_cond_broadcast(&cond);
}
}
void *task(void *arg){
while(1){
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("tid = %ld,购买了1辆汽车\n", pthread_self());
pthread_mutex_unlock(&mutex);
}
}
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2, tid3, tid4, tid5;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
if(pthread_create(&tid1, NULL, task1, NULL)){
perror("create thread 1 error");
return -1;
}
if(pthread_create(&tid2, NULL, task, NULL)){
perror("create thread 2 error");
return -1;
}
if(pthread_create(&tid3, NULL, task, NULL)){
perror("create thread 3 error");
return -1;
}
if(pthread_create(&tid4, NULL, task, NULL)){
perror("create thread 4 error");
return -1;
}
if(pthread_create(&tid5, NULL, task, NULL)){
perror("create thread 5 error");
return -1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
pthread_join(tid5, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
执行结果:
1.3、无名信号量与条件变量的使用场景
无名信号量适合在线程数比较少的线程中实现同步过程,而条件变量适合在大量线程
实现同步过程。例如条件变量的使用场景如下:比如你要编写一个12306买票的服务器
当客户端访问服务器的时候,服务器会创建一个线程服务于这个用户。如果有多个用户
同时想买票,此时服务需要在瞬间创建一堆线程,这个时间比较长,对用户的体验感不好。
所以12306服务是在启动的时候都已经创建好一堆线程。调用pthread_cond_wait让这些
线程休眠,当有客户端请求买票的时候,只需要唤醒这些休眠的线程即可,有于省去了
创建线程的时候,所以这种方式的效率非常的高。
2、传统的进程间的通信方式
1、无名管道:只能用于亲缘间进程的通信,无名管道的大小是64K。
2、有名管道:可以用于亲缘间进程的通信,也可以用于非亲缘间的进程的通信。
3、信号:进程对信号的处理方式有三种:捕捉,忽略,默认
2.1、无名管道
#include <head.h>
int main(int argc, const char *argv[])
{
//数组pipefd用于返回两个指向管道末端的文件描述符。
//pipefd[0]指的是管道的读端。pipefd[1]指的是管道的写入端。
int pipefd[2];
pid_t pid;
char buf[128] = {0};
//创建管道
if(pipe(pipefd)){
perror("create pipe error");
return -1;
}
//创建进程
pid = fork();
if(pid == -1){
perror("fork error");
return -1;
}else if(pid == 0){
close(pipefd[0]);
while(1){
//从终端读取数据
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0';
//将数据通过管道写给父进程
write(pipefd[1], buf, strlen(buf));
if(strncmp(buf, "quit", 4) == 0){
break;
}
}
close(pipefd[1]);
exit(-1);
}else{
close(pipefd[1]);
while(1){
//先清空 buf , 再从管道中读取数据
memset(buf, 0, sizeof(buf));
read(pipefd[0], buf, sizeof(buf));
if(strncmp(buf, "quit", 4) == 0){
break;
}
printf("read = %s\n", buf);
}
close(pipefd[0]);
wait(NULL);
}
return 0;
}
执行结果:
特点:
1.只能用于亲缘间进程的通信
2.无名管道数据半双工的通信的方式
单工 : A -------------->B
半双工 : 同一时刻 A----->B B------>A
全双工 : 同一时刻 A<---->B
3.无名管道的大小是64K
4.无名管道不能够使用 lseek 函数
5.读写的特点
如果读端存在写管道:有多少写多少,直到写满为止(64k)写阻塞
如果读端不存写管道,管道破裂(SIGPIPE) (可以通过gdb调试看现象)
如果写端存在读管道:有多少读多少,没有数据的时候阻塞等待
如果写端不存在读管道:有多少读多少,没有数据的时候立即返回
2.2、有名管道
main.c
#include <head.h>
int main(int argc, const char *argv[])
{
if(mkfifo("./fifo", 0666)){
perror("create fifo error");
return -1;
}
//在这里堵塞,当不需要管道时,按回车清除管道
getchar();
system("rm ./fifo -f");
return 0;
}
write.c
#include <head.h>
int main(int argc, const char *argv[])
{
int fd;
char buf[128] = {0};
if((fd = open("./fifo", O_WRONLY)) == -1){
perror("open error");
return -1;
}
while(1){
printf("input >>");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0';
write(fd, buf, strlen(buf));
if(strncmp(buf, "quit", 4) == 0)
break;
}
close(fd);
return 0;
}
read.c
#include <head.h>
int main(int argc, const char *argv[])
{
int fd;
char buf[128] = {0};
if((fd = open("./fifo", O_RDONLY)) == -1){
perror("open error");
return -1;
}
while(1){
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf));
if(strncmp(buf, "quit", 4) == 0)
break;
printf("read = %s\n", buf);
}
close(fd);
return 0;
}
执行结果:
特点:
1.可以用于任意进程间的通信
2.有名管道数据半双工的通信的方式
3.有名管道的大小是64K
4.有名管道不能够使用lseek函数
5.读写的特点
如果读端存在写管道:有多少写多少,直到写满为止(64k)写阻塞
如果读端不存写管道
1.读端没有打开,写端在open的位置阻塞
2.读端打开后关闭,管道破裂(SIGPIPE) (可以通过gdb调试看现象)
如果写端存在读管道:有多少读多少,没有数据的时候阻塞等待
如果写端不存在读管道
1.写端没有打开,读端在open的位置阻塞
2.写端打开后关闭,有多少读多少,没有数据的时候立即返回
2.3、信号
常用的信号
1.在上述的信号中只有SIGKILL和SIGSTOP两个信号不能被捕捉也不能被忽略
2. 17)SIGCHLD,当子进程结束的时候,父进程收到这个SIGCHLD的信号
借助信号实现非阻塞方式回收子进程的资源
#include <head.h>
void signal_handle(int signo){
printf("收到子进程退出的信号\n");
//以非阻塞的方式回收
waitpid(-1, NULL, WNOHANG);
printf("子进程资源回收完成\n");
//给父进程发信号,使其停止运行
raise(SIGKILL);
}
int main(int argc, const char* argv[]) {
pid_t pid;
pid = fork();
if (pid == -1) {
perror("fork error");
return -1;
} else if (pid == 0) {
printf("子进程执行中。。。\n");
sleep(5);
printf("子进程执行结束\n");
exit(-1);
} else {
if (signal(SIGCHLD, signal_handle) == SIG_ERR) {
perror("signal error");
return -1;
}
while (1);
}
return 0;
}
执行结果: