并发编程学习
前言
什么是进程?
进程是操作系统进行资源分配和资源调度的最小单位
进程的构成:代码块 + 数据块 + 进程控制块(PCB->process contrl block)(跟电路板不是同一个东西)
代码块:进程运行时的指令
数据块:进程运行时需要的数据
进程控制块:指示线程运行到哪个地方,运行的状态
可以通过ps -elf查看当前正在运行的进程
进程可以分为两类:父子进程(子进程通过父进程调用fork得到) 没有关系的独立进程(2个独立运行的程序)
在父子进程中还可以具体分类
1.孤儿进程:当父进程结束后,但是子进程仍然运行,这时的子进程称为孤儿进程
系统对孤儿进程的处理:
当父进程结束后,操作系统不会让子进程被遗弃,而是"收养"这些孤儿系统,当父进程结束后,操作系统会检查
这些子检查的PPID(父进程id号),如果匹配刚刚结束的父进程,那么就会将这些子进程重新挂载到根进程名下:一般是PID = 1的根进程,等待这些孤儿进程运行结束后再wait回收,防止其成为僵尸进程
2.僵尸进程:子进程结束后,父进程没有回收子进程,此时的子进程称为僵尸进程
僵尸进程特点:
1.已死亡但未被清理
已死亡:进程运行结束,代码、数据、堆栈都被回收
未清理:进程描述符里面仍然会记录它的运行状态、结束状态等一些进程的状态信息,这些需要父进程wait来读取
2.无法被杀死
僵尸进程已经死亡了,无法再使用kill来结束进程,只能等待父进程调用wait()或者waitpid()来回收信息
可以通过ps aux,看到进程的状态stat列 显示为Z的表示是僵尸进程
僵尸进程的危害
1.占用PID号:PID号是有限的,如果过多的僵尸进程会导致PID号不足
2.占用内核资源:虽然释放了大部分的内存,但还有进程状态信息没有被完全释放,积累过多就会导致大问题
注意:僵尸进程不会占用CPU
如何释放僵尸进程呢?
1)找到父进程,如何将其父进程kill掉 ps -ef | grep defunct可以找到僵尸进程的父进程PPID
2)kill -9 PPID杀死父进程
3)父进程死后,这些僵尸进程会重新挂载到根进程,根进程会自动回收这些僵尸进程
进程函数
1.fork 创建子进程
pid_t fork(void); //pid_t:长整型
返回值:
>0:父进程
=0:子进程
-1:调用失败
创建单个子进程
#include <stdio.h>
#include <unistd.h>
int main(void) //父进程:编译运行的进程
{
pid_t id;
id = fork(); //子进程:在一个进程内又创建了一个新的进程
if(id > 0) //父进程运行这里
{
while(1)
{
printf("父进程在运行\n");
sleep(1);
}
}
else if(id == 0) //子进程运行这里
while(1)
{
printf("子进程在运行\n");
sleep(1);
}
else
printf("进程创建失败\n");
}
运行结果

原理:fork把父进程中的全部变量都拷贝了一份,然后单独开了一个进程作为子进程,所以能够实现并发运行
注意:父子进程不共享全局变量
int num = 20;
int main(void) //父进程:编译运行的进程
{
pid_t id;
id = fork(); //子进程:在一个进程内又创建了一个新的进程
int i = 1; //计数运行几次
if(id > 0) //父进程运行这里
{
while(1)
{
printf("(%d)父进程在运行num:%d\n", i, num);
i++;
if(i >= 5)
num = 666;
sleep(1);
}
}
else if(id == 0) //子进程运行这里
while(1)
{
printf("(%d)子进程在运行num:%d\n", i, num);
i++;
sleep(1);
}
else
printf("进程创建失败\n");
}

可以看到
一开始创建的全局变量n=20,在5次计时后,父进程把num改为了666,这是父进程的num打印为666,子进程的仍然为20
对于局部变量i:在父进程里面自增,在子进程里面也自增,但是他们的增加并不共享,可以得到父子进程不共享局部变量
由此可以知道,父子进程不共享全局变量和局部变量
创建多个子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid1, pid2;
pid1 = fork();
if(pid1 > 0)
{
pid2 = fork();
if(pid2 > 0)
{
while(1)
{
printf("这是父进程,ID:%d\n", getpid());
sleep(1);
}
}
else if(pid2 == 0)
{
while(1)
{
printf("这是第二个子进程,ID:%d\n", getpid());
sleep(1);
}
}
}
else if(pid1 == 0)
{
while(1)
{
printf("这是第一个子进程,ID:%d\n", getpid());
sleep(1);
}
}
return 0;
}
运行结果

可以看到运行顺序:父进程-->子进程2-->子进程1
这是因为Linux是后创建的进程先运行
创建多进程时容易出现的问题
1.连续写2个fork
int main()
{
pid_t id1, id2;
//连续创建2个子进程
id1 = fork();
id2 = fork();
//主进程
while(1)
{
sleep(1);
}
}
虽然代码这里写的是2个子进程,但是实际创建的进程数却为4个(3个子 + 1个主)
原因:id1这个线程被创建后(第一个子进程)没有被if(id1 == 0)给带入自己的进程逻辑中,他就会往下执行,这时id1这个进程遇到了id2的创建,那么id1就会创建一个id2的进程(第二个子进程)。id2这个代码运行的时候自己也会创建一个进程(第三个子进程)
这个创建的顺序是从上往下的
fork的原理:
调用fork的时候,父进程和子进程还是共享同一份物理内存页,还没有立刻拷贝变量(节省内存),当父子进程中的其中一个试图修改变量的时候,内存就会将被修改的那一页内存单独复制一份,作为发生修改线程单独的内存页(反正记住,一开始是共享同一个物理内存的(只读),等到发生修改(对只读进行修改)的时候才会将被修改内存页拷贝一份给到发生修改的线程(内核对被只读内存进行修改的处理))
理解了这一部分,其实上面的情况也就很好理解了
2.exit 退出进程
功能:退出进程(在退出的时候会把这个子进程中的所有东西都做清理,关闭文件流指针,刷新缓冲区等等)
#include <stdlib.h>
void exit(int status)
参数:
-
status:进程的退出状态码。-
0或EXIT_SUCCESS(通常定义为 0)表示成功。 -
EXIT_FAILURE(通常定义为 1)表示失败。 -
其他整数值由用户自定义,父进程可以通过
wait()或waitpid()获取。
-
函数行为:
-
调用
atexit()注册的退出处理函数(按注册顺序的逆序)。 -
刷新并关闭所有标准 I/O 流(如
stdout、stderr)。 -
删除
tmpfile()创建的临时文件。 -
调用
_exit()系统调用进入内核,由内核回收进程资源。
_exit()函数
#include <unistd.h>
void _exit(int status);
参数:
-
status:退出状态码,含义同exit()。
行为
-
不执行任何清理工作:
-
不刷新标准 I/O 缓冲区。
-
不调用
atexit注册的函数。 -
直接进入内核,由内核回收进程资源(内存、文件描述符、进程表项等)。
-
根据上面的对比我们可以看到,如果需要结束进程的话,使用exit是最佳方法,因为它会自动释放子进程所创建和打开的内存,清空缓冲区
3.wait 回收进程
功能:阻塞等待子进程结束,然后对子进程进行回收
pid_t wait(int *stat_loc);
4.waitpid 更强大的回收方式
进程组:多个进程组成的集合,假如有三个进程构成了一个进程组,他们三个的进程号分别是123
124,125,从他们中选一个作为组长(程序员自定),那么这个组长的进程号就能代表这个进程组
pid_t waitpid(pid_t pid, int *stat_loc, int options)
返回值
成功:回收到进程的ID
失败:0
参数
pid:
进程号:回收指定进程号的进程(指定一个)(最常用)
=0:等待与调用进程同进程的任意子进程
-1:等待回收任意一个子进程
-进程号:回收这个(指定进程号)的进程组中某个进程(组里面收一个)
stat_loc:传入一个指针,保存进程结束的信息(用宏定义来解析)
option:等待方式(一般写0)
0:阻塞父进程,等待子进程被释放
WNOHANG:非阻塞轮询,如果没有子进程被释放,父进程就先去做其他事,等下一次轮询再检查
调用wait/waitpid回收到的信息不是单纯的exit发送过来的信息,而是整个进程退出状态码值,如下图所示
真正由exit传过来的数据为是8-15位,如果需要获取的话
1.位操作:将state右移8位获取
2.宏定义:使用系统给的宏定义 WEXITSTATUS
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0)
{
// ---------- 子进程 ----------
exit(42);
}
else if(pid > 0)
{
// ---------- 父进程 ----------
int status; // 这个整型变量将承载所有信息
pid_t ret = wait(&status); // 阻塞等待任意子进程结束
if (ret > 0)
{
int exit_code = WEXITSTATUS(status);
printf("子进程 %d 退出,退出码为:%d\n", ret, exit_code);
}
}
return 0;
}
其他相关宏定义

5.getpid 获取进程号
功能:获取当前进程的进程号
pid_t getpid(void)
6.getppid 获取父进程的进程号
功能:获取当前进程父进程的进程号
pid_t getppid(void)
进程间通信
1.无名管道 有关系进程间的通信
作用范围:父子进程、兄弟进程(有关系的进程之间)
#include <unistd.h>
int pipe(int fd[2])
返回值:
成功:0
失败:-1
参数:
fd[2]:2个文件描述符,fd[0]为读端、fd[1]为写端 记忆(读写读写,先读后写)
使用举例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
static char buf[20];
int main() {
pid_t pid1, pid2;
int ret;
int fd[2]; //创建2个文件描述符
//1.创建管道
ret = pipe(fd);
if(ret == -1)
{
perror("创建无名管道失败");
return -1;
}
//2.通过管道进行通信 write & read 进行通信(不能使用标准IO,这个是系统IO的)
pid1 = fork();
//父进程写入
if(pid1 > 0)
{
while(1)
{
write(fd[1], "hello", strlen("hello"));
sleep(5);
}
}
//子进程读取
else if(pid1 == 0)
{
while(1)
{
//注意:read是会堵塞进程的,如果一直没有读取到的话就会一直堵塞
read(fd[0],buf, 1); //这里读取多少个数据无所谓,管道中有多少就会读多少
printf("(子进程)读取到的信息:%s\n", buf);
memset(buf, 0, sizeof(buf));
}
exit(0); //结束子进程
}
else
{
perror("进程创建失败");
return -1;
}
waitpid(pid1, NULL, 0); //NULL表示父进程不关系退出状态信息
//3.使用完毕,关闭无名管道
close(fd[0]);
close(fd[1]);
return 0;
}
可以看到,无名管道功能是比较羸弱的,只能实现单工通信(一端发送,一端接收)
如果想要实现双工通信,那就需要创建2个无名管道,使用效果会好一点
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
static char bufp[20]; //父进程读取到的数据
static char bufc[20]; //子进程读取到的数据
int main() {
pid_t pid1, pid2;
int ret;
int fd1[2]; //创建2个文件描述符
int fd2[2];
//1.创建管道
ret = pipe(fd1);
if(ret == -1)
{
perror("创建无名管道失败");
return -1;
}
ret = pipe(fd2);
if(ret == -1)
{
perror("创建无名管道失败");
return -1;
}
//2.通过管道进行通信 write & read 进行通信(不能使用标准IO,这个是系统IO的)
pid1 = fork();
//父进程
if(pid1 > 0)
{
while(1)
{
memset(bufp, 0, sizeof(bufp));
memset(bufc, 0, sizeof(bufc));
//父进程写入数据
printf("(父进程)写入:");
scanf("%s", bufp);
write(fd1[1], bufp, strlen(bufp));
if(strcmp(bufp, "quit") == 0)
{
break;
}
//父进程读取数据
read(fd2[0], bufp, sizeof(bufp));
printf("(父进程)读取:%s\n", bufp);
if(strcmp(bufc, "quit") == 0)
{
printf("退出子进程\n");
write(fd2[1], "quit", strlen("quit"));
exit(0); //结束子进程
}
}
}
//子进程
else if(pid1 == 0)
{
while(1)
{
//注意:read是会堵塞进程的,如果一直没有读取到的话就会一直堵塞
read(fd1[0],bufc, sizeof(bufc)); //这里的设置为每次读取多少个管道内的数据,只要有数据就会读取
printf("(子进程)读取到:%s\n", bufc);
if(strcmp(bufc, "quit") == 0)
{
printf("退出子进程\n");
exit(0); //结束子进程
}
//父进程写入数据
printf("(子进程)写入:");
scanf("%s", bufc);
write(fd2[1], bufc, strlen(bufc));
if(strcmp(bufc, "quit") == 0)
{
printf("退出子进程\n");
exit(0); //结束子进程
}
}
}
else
{
perror("进程创建失败");
return -1;
}
waitpid(pid1, NULL, 0); //NULL表示父进程不关系退出状态信息
printf("子进程回收完成\n");
//3.使用完毕,关闭无名管道
close(fd1[0]);
close(fd1[1]);
close(fd2[0]);
close(fd2[1]);
return 0;
}
注意:在使用无名管道的时候要注意阻塞与非阻塞特性
read:阻塞,等到通道内有数据发送过来
wirte:如果通道内的数据满了,那么wirte就会阻塞,等到通道内数据不满后再往里面写入
2.有名管道 没关系的进程间通信
作用范围:两个没有关系的进程,比如 task1 和 task2 这2个不同的进程
使用有名管道进行不同进程之间的通信流程
创建有名管道-->双方打开管道-->开始通过管道通信-->结束,关闭管道
有名管道:mkfifo() 实现进程与进程之间的通信
1.进程1跟进程2之间没有任何关系(之前的关系是兄弟进程、父子进程,注意区分)
2.管道不能在共享文件夹里面创建(Linux特有的文件类型,Windows不支持)
3.有名管道只有一个文件描述符(前面无名管道有2个),既可以读也能写
创建有名管道文件
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode)
返回值:
成功:0
失败:-1
参数:
pathname:管道创建的路径
mode:管道的权限
使用管道通信
代码如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
static char bufc[20];
int main()
{
int ret;
//1.创建有名管道(不能重复创建)
if(access("/home/gec/桌面/fifo", F_OK) == 0)
{
printf("%s文件存在\n", "/home/gec/桌面/fifo");
}
else
{
ret = mkfifo("/home/gec/桌面/fifo", 0777);
if(ret == -1)
{
perror("创建有名管道失败");
return -1;
}
printf("%s创建成功\n", "/home/gec/桌面/fifo");
}
//2.打开有名管道
int fd = open("/home/gec/桌面/fifo", O_RDWR);
if(fd == -1)
{
perror("有名管道打开失败");
return -1;
}
while(1)
{
memset(bufc, 0, sizeof(bufc));
printf("请输入需要发送的数据:");
scanf("%s", bufc);
write(fd, bufc, strlen(bufc));
}
close(fd);
return 0;
}
注意!!
1.在打开有名管道的时候,如果一段使用的只读(O_RDONLY),另一端忘记以写模式打开,那么程序就会被永久挂起
2.如果写端开始write的时候发现没有读端,那么read会触发SIGPIPE信号,导致写端进程终止(如果需要这样操作,可以使用signal忽略这个信号(下面信号部分有讲))
3.当写端close后,读端调用read,这时read会返回0,这个可以作为读端的退出标志。
信号
什么是信号:信号是发送给进程的异步通知,告诉进程有一件事情要发送了,你需要去处理一下
可以通过 命令:kill -l 来查看所有的信号

每个信号具体含义如上图,说一下比较重要的几个
18)SIGSCONT-->让进程继续运行
19)SIGSTOP-->让进程暂停
2)SIGINT--> 等同于ctrl+c
9)SIGKILL--> 把进程杀死
信号有3个要素构成
编号 名称 对应动作
1.kill 发送信号
int kill(pid_t pid, int sig);
返回值:
成功:0
失败:-1
参数
pid:进程的id号
sig:需要发送的信号
调用这个函数,就能对对应的进程进行操作(具体操作根据设置的sig决定)(相当于是命令行里面使用kill,这里是给了在程序里面也使用的接口)
2.信号重定义 重新定义信号的反应动作
void (*signal(int sig, void(*func)(int)))(int)
这是一个函数指针,其返回值为void(*)(int)类型,参数为int sig, void(*func)(int),函数名为signal
1)重定义信号功能
void func(int sig)
{
if(sig == 4)
{
printf("当前的信号为:%d,动作修改为:\"hello world\"\n", sig);
}
else if(sig == 2)
{
printf("当前的信号为:%d,动作修改为:\"你好\"\n", sig);
}
}
int main(int argc, char **argv)
{
signal(SIGINT, func); //signal(2, func);
signal(SIGILL, func); //signal(4, func);
while(1)
{
}
return 0;
}
通过使用signal,将对应的信号重定义功能(回调函数),就能实现将信号更改为其他的动作
注意:9号(SIGKILL)和19(SIGSTOP)不可以被重定义
2)忽略信号操作
接收到这个信号后,不对这个信号进行操作或者反应
signal(sig, SIG_IGN);
/*
前面可以看到,大部分的信号都是终止进程的作用
void (*signal(int sig, void(*func)(int)))(int)
signal的作用
1.改变信号的默认响应动作(本来是终止的,修改后变为打印hello world)
2.忽略信号,接收到信号后对这个信号不进行反应
*/
int main(int argc, char **argv)
{
for(int i = 0; i <= 64; i++)
{
if(i == 32 || i == 33) //32和33不存在,需要跳过他们
continue;
signal(i, SIG_IGN);
}
while(1)
{
}
return 0;
}
注意:9号(SIGKILL)和19(SIGSTOP)不可以被忽略
上面循环虽然设置了,但不会生效
3)恢复信号原本功能
signal(sig, SIG_DFL);
int main(int argc, char **argv)
{
for(int i = 0; i <= 64; i++)
{
if(i == 32 || i == 33) //32和33不存在,需要跳过他们
continue;
signal(i, SIG_IGN);
}
for(int i = 0; i <= 64; i++)
{
if(i == 32 || i == 33) //32和33不存在,需要跳过他们
continue;
signal(i, SIG_DFL); //恢复全部信号的原本功能
}
while(1)
{
}
return 0;
}
这个时候再发送对应的信号,就会对进程反应原本设置的信号功能
3.pause
功能:堵塞进程,等待信号到来
#include <unistd.h>
int pause(void);
使用这个函数,能够让程序等待接收到信号后才解除堵塞
上面的例子都是通过while(1)来实现等待信号(因为signal函数不会堵塞,不使用while1,程序就直接结束了)
4.将信号阻塞
阻塞信号:将接收到的信号挂起(信号还在,只是暂时不然信号起作用),等到挂起结束,信号再起作用
创建阻塞信号的相关函数
#include <signal.h>
集合变量:sigset_t myset //sigset_t是一个数组
int sigeptyset(sigset_t *set) //清空集合
int sigfillset(sigset_t *set) //将62个信号都加入到集合中
int sigaddset(sigset_t *set, int signum) //把signum信号添加到集合中
int sigdelset(sigset_t *set, int signum) //把signum信号从集合中删除
int sigismember(const sigset_t *set, int signum) //判断signum是否在集合中
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数:
how:
set:需要阻塞的集合
oset:存放当前进程已经被设置阻塞的信号 (不用的话NULL)
int main(void)
{
//创建一个集合
sigset_t myset;
//清空集合
sigeptyset(&myset);
//添加需要阻塞的信号
sigaddset(&myset, 1);
sigaddset(&myset, 2);
sigaddset(&myset, 3);
//启用阻塞
sigprocmask(SIG_BLOCK, &myset, NULL); //NULL表示不用备份已经被阻塞的信号
//解除阻塞
sigprocmask(SIG_UNBLOCK, &myset, NULL);
}
如果需要获取已经被阻塞的信号,可以参考下面的写法
int main(void)
{
//创建一个集合
sigset_t oset;
//清空集合
sigeptyset(&oset)
//启用阻塞
sigprocmask(SIG_BLOCK, NULL, &oset);
}
5.进程的多种状态切换
1.就绪态:假设有3个进程同时存在,在某一时刻,cpu在运行进程1,那么此时的进程2和进程3就是就绪态(进程2和进程3已经创建了,只是在等待运行)
2.执行态:还是上面这个例子,cpu在运行进程1,那么进程1就是执行态
3.睡眠态/挂起态:
1)睡眠态:进程在执行sleep()时,这个进程就是睡眠态
2)挂起态:运行带有阻塞功能的函数(pause()、wait()、waitpid()(这个根据设置来看)、read())这4个函数具有阻塞功能,此时进程就是挂起态
4.暂停态:进程收到了暂停信号(SIGSTOP(19)、(20)、(21)、(22)都是)
5.僵尸态:子进程退出了,但是父进程没有调用wait/waitpid回收进程,此时子进程称为僵尸态
6.死亡态:进程退出后被回收了,此时子进程称为死亡态
线程
线程是什么?
线程是从进程的资源中分割一部分资源,用来并行执行,为了实现多任务并发的功能
线程:操作系统能够进行调度运算的最小单位 进程:资源分配的最小单位
线程和进程有什么关系:
线程是从进程身上取出一部分资源,并发运行的一条工作线
线程的特点:
多个线程会共享同一份内存,共享同一份数据
1.pthread_create 创建线程
#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:传给线程回调函数的数据
注意:在使用线程相关的功能时,编译需要带上 -pthread,将线程库编译进来
gcc main.c -phtread (也支持)
gcc main.c -lpthread (更标准)
举个例子
#include <stdio.h>
#include <pthread.h> //进程
#include <unistd.h> //延时
void *task(void *arg)
{
while(1)
{
printf("(子线程1)正在运行\n");
sleep(1);
}
}
int main(void)
{
//创建一个线程
pthread_t pth_id1;
pthread_create(pth_id1, NULL, task, NULL);
printf("线程1的id号为%ld\n", pth_id1);
while(1)
{
printf("(主线程)正在运行\n");
sleep(1);
}
}
这样我们就能看到主线程和子线程都在运行

2.pthread_exit 退出线程
#include <pthread.h>
void pthread_exit(void *retval)
参数:
retval:线程退出时的退出值,用户可自定义 由pthread_join(线程回收函数)接收
3.pthread_join 线程回收函数
#include <ptthread.h>
int pthread_join(pthread_t thread, void **retval)
参数:
thread:需要回收的线程id
retval:pthread_exit传输过来的线程退出后的数据(这里是二级指针)
线程的退出和回收中,其传送过来的值需要注意:
1.线程的回调函数本质上还是一个函数,线程结束了,那么函数也会被销毁,函数返回过来的又是一个指针,如果利用指针将函数中的局部变量传过来,那么函数销毁后得到的数据就会出现乱码的问题(很重要)
解决方法
1.用static定义
2.传常量
3.使用malloc定义,将变量留在堆内存里面(函数是在栈内存的)(不过使用完数据记得free)
4.pthread_cancel 取消线程
#include <pthread.h>
int pthread_cancel(pthread_t thread)
参数
thread:需要被取消的线程
只要在主函数内调用这个函数,就能够把线程给删掉(强制终止)
5.pthread_setcancelstate 设置线程的取消状态
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldtstate)
参数:
state:设置取消的状态
PTHREAD_CANCEL_ENABLE 线程可以取消(默认)
PTHREAD_CANCEL_DISABLE 线程不可以被取消
oldstate:记录上一次的取消状态
将这个函数放在线程回调函数内,就可以设置这个线程的取消状态了
6.pthread_set_canceltype 设置线程的取消类型
#include <pthread.h>
int pthread_canceltype(int type, int *oldtype)
参数:
type:设置取消类型
PTHREAD_CANCEL_DEFERRED延时取消(当遇到取消点函数时函数才线程在被取消)(默认为延时取消)
PTHREAD_CANCEL_ASYNCHRONOUS立刻取消(调用取消函数就立马停止)
oidtype:保留上一次的取消状态类型
在线程处于可以取消的状态下,可以通过这个函数来设置线程的取消类型
取消点函数:这个在man手册里面有介绍,感兴趣可以了解一下
下面给出2个例子:展示立即取消和延时取消
int num_t = 100;
void *func1(void * arg)
{
//设置当前线程为立即取消
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
//这里for循环是为了延时(sleep函数是一个退出点函数)
for(int i = 0; i < 200000; i++) //大约3s延时
{
for(int j = 0; j < 50000; j++)
{
}
}
fputs("exit\n", stdout); //也是退出点函数
}
int main(int argc, char **argv)
{
//创建一个线程:这个线程是从主函数这个进程中分出来的
pthread_t pth_id1;
pthread_create(&pth_id1, NULL, func1, NULL); //传普通的参数:把地址发送过去即可
printf("(main)线程1的id为:%ld\n", pth_id1);
//暂停1s
sleep(1);
//取消线程
pthread_cancel(pth_id1);
while(1)
{
printf("(main)主函数运行\n");
sleep(1);
}
return 0;
}
延时取消
int num_t = 100;
void *func1(void * arg)
{
//延时取消(默认也是这个type,不用设置也ok)
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
//这里for循环是为了延时(sleep函数是一个退出点函数)
for(int i = 0; i < 200000; i++) //延时大约3s
{
for(int j = 0; j < 50000; j++)
{
}
}
fputs("exit\n", stdout); //也是退出点函数
}
int main(int argc, char **argv)
{
//创建一个线程:这个线程是从主函数这个进程中分出来的
pthread_t pth_id1;
pthread_create(&pth_id1, NULL, func1, NULL); //传普通的参数:把地址发送过去即可
printf("(main)线程1的id为:%ld\n", pth_id1);
//暂停3s
sleep(1);
//取消线程
pthread_cancel(pth_id1);
while(1)
{
printf("(main)主函数运行\n");
sleep(1);
}
return 0;
}
7.线程属性(pthread_create的第二个参数)

线程主要有以上这些属性
其中比较常设置的为分离状态,我们着重介绍一下分离状态
分离属性:
分离属性介绍:分离属性有2种
PTHREAD_CREATE_DETACHED可分离:线程退出的时候,不需要主线程调用pthread_join进行回收,也无法通过pthread_join函数回收到
PTHREAD_CREATE_JOINABLE不可分离:线程退出的时候,需要主线程调用pthread_join进行回收(默认)
可分离的应用场景:
需要线程执行完成任务后直接结束,主线程不用对其进行回收,这时可以设置为可分离类型(如果不是可分离,主线程回收会比较困难)
需要注意的是:如果设置为了可分离,用pthread_join回收这个线程将不会堵塞程序(对这个设置为可分离的线程)
如何设置线程属性
int main(void)
{
//创建属性结构体
pthread_attr_t myattr;
//初始化结构体
pthread_attr_init(&myattr);
//设置属性(这里演示设置分离属性)
pthread_attr_setdetachstate(&myattr, PTHREAD_CREATE_DETACHED);
//创建线程
pthread_t pth;
pthraed_create(&pth, &myattr, task, NULL);
}
以下是其他类型的设置函数,与设置分离属性大相径庭

互斥锁
为什么要互斥锁?
前面说过,所有的线程是共享一个内存页的,大家都能够对内存中的数据进行操作,那么假设有多个线程同时对同一个数据进行操作,那这个数据最终的结果是什么呢?这会导致数据出现不确定性,而不确定性不会是程序员想要的,所以才有互斥锁这个工具
作用:通过上锁和解锁的操作,实现在任意时刻只有一个线程在访问共享资源
互斥锁的原理
当某个线程上锁了,那么这个时候其他的线程就会被阻塞,无法使用该共享资源,等到上锁的那个线程解锁后,其他的线程才能去重新上锁使用
特点:
多个线程只使用同一把锁,谁先上锁,谁先使用,其他线程阻塞等待
互斥锁中有一种情况被称为死锁
定义:互斥锁被上锁后,上锁的那个线程没用进行解锁的操作,导致这把锁把其他使用这个互斥锁的线程全都阻塞了。
产生的原因:
1.忘记解锁--代码加上
2.线程在上锁期间被取消了,没来得及解锁,导致死锁的出现--为线程设置不可取消
创建互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
参数:
mutex:互斥锁参数
mutexattr:互斥锁的属性(NULL,一般使用系统默认设置的)
使用互斥锁的操作
上锁: int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁: int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁: int pthread_mutex_destroy(ptread_mutex_t *mutex);
#include <stdio.h>
#include <unistd.h>
#include <pthread.h> //线程头文件
//创建互斥锁:由于要给线程使用,所以创建为全局变量最佳
pthread_mutex_t mutex
int num = 20;
void *task1(void *arg)
{
//设置不可取消
pthread_setcancelstate(PHTREAD_CANCEL_DISABLE, MULL);
while(1)
{
//上锁
pthread_mutex_lock(&mutex)
num++;
printf("(线程1)num++ num:%d\n", num);
sleep(1);
//解锁
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
void *task2(void *arg)
{
//设置不可取消
pthread_setcancelstate(PHTREAD_CANCEL_DISABLE, MULL);
while(1)
{
//上锁
pthread_mutex_lock(&mutex)
num--;
printf("(线程2)num-- num:%d\n", num);
sleep(1);
//解锁
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
int main(void)
{
pthread_t pth1, pth2;
pthread_create(&pth1, NULL, task1, NULL);
pthread_create(&pth2, NULL, task2, NULL);
while(1)
{
printf("(main)正在运行\n");
sleep(1);
}
//回收线程
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
//使用完毕,销毁互斥锁
pthread_mutex_destroy(&mutex);
}
条件变量
功能:当线程在不满足某个运行条件的时候,能够让线程进入睡眠等待,直到满足条件后才能继续运行(要搭配互斥锁一起使用)
创建条件变量
#Include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
参数:
cond:条件变量参数
cond_attr:条件变量属性(NULL,使用默认即可)
使用条件变量
阻塞线程: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
唤醒某个线程: int pthread_cond_signal(pthread_cond_t *cond); 不能指定线程,随机唤醒
唤醒所有线程: int pthread_cond_broadcast(pthread_cond_t *cond);
销毁: int pthread_cond_destroy(pthread_cond_t *cond);
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
/*输入子系统模型头文件*/
#include <linux/input.h>
#include <sys/mman.h>
#include <signal.h> //信号头文件
#include <pthread.h> //线程头文件
/*
条件变量:不能单独使用,需要配合互斥锁实现线程的协调控制
功能:1.阻塞线程
2.解除阻塞
举例使用:
1.创建5个线程--代表5个售票口
2.主线程代表铁道部,负责放票
3.当票卖完了,就要阻塞线程,停止卖票,直到铁道部重新给出新的票
4.创建条件变量:当ticket<5时阻塞线程,停止售票
1)创建条件变量
pthread_cond_t mycond;
2)初始化
条件变量 属性
pthread_cond_init(&mtcond, NULL);
3)使用 条件变量 互斥锁
阻塞线程 pthread_cond_wait(&mtcond, &mutex); 这里需要互斥锁参数的原因是为了防止出现前面说到的上锁了但是没有解锁的问题,这里的功能是 阻塞 + 解锁
解除阻塞 pthread_cond_signal(); //只能唤醒一个线程,从所有阻塞的线程里面随机唤醒一个,不一定是哪一个
pthread_cond_broadcast();//唤醒所有线程
注意:在使用条件变量阻塞线程的时候最好使用while而不是if,这样能减少状态导致的问题
4)销毁
pthread_cond_destroy(&mycond);
*/
pthread_mutex_t mutex;
pthread_cond_t cond;
int ticket_num = 20;
static void *task1(void *arg)
{
//设置为不可取消,这样就能避免第二种情况的出现
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
while(1)
{
pthread_mutex_lock(&mutex);
while(ticket_num <= 10)
{
//阻塞 条件变量 互斥锁(被上了锁后才能阻塞程序)
printf("(窗口1)暂停售票\n");
pthread_cond_wait(&cond, &mutex);
}
ticket_num--;
printf("(窗口1)出售 剩余:%d\n", ticket_num);
sleep(1);
pthread_mutex_unlock(&mutex);
usleep(10000);
}
//退出线程
pthread_exit(NULL);
}
static void *task2(void *arg)
{
//设置为不可取消,这样就能避免第二种情况的出现
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
while(1)
{
pthread_mutex_lock(&mutex);
while(ticket_num <= 10)
{
//阻塞 条件变量 互斥锁(被上了锁后才能阻塞程序)
printf("(窗口2)暂停售票\n");
pthread_cond_wait(&cond, &mutex);
}
ticket_num--;
printf("(窗口2)出售 剩余:%d\n", ticket_num);
sleep(1);
pthread_mutex_unlock(&mutex);
usleep(10000);
}
//退出线程
pthread_exit(NULL);
}
static void *task3(void *arg)
{
//设置为不可取消,这样就能避免第二种情况的出现
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
while(1)
{
pthread_mutex_lock(&mutex);
while(ticket_num <= 10)
{
//阻塞 条件变量 互斥锁(被上了锁后才能阻塞程序)
printf("(窗口3)暂停售票\n");
pthread_cond_wait(&cond, &mutex);
}
ticket_num--;
printf("(窗口3)出售 剩余:%d\n", ticket_num);
sleep(1);
pthread_mutex_unlock(&mutex);
usleep(10000);
}
//退出线程
pthread_exit(NULL);
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
//初始化条件变量
pthread_t pth_id1, pth_id2, pth_id3;
//创建线程
pthread_create(&pth_id1, NULL, task1, NULL);
pthread_create(&pth_id2, NULL, task2, NULL);
pthread_create(&pth_id3, NULL, task3, NULL);
while(1)
{
if(ticket_num <= 10)
{
printf("补票中\n");
sleep(5);
ticket_num = 9;
printf("(main)重新开始售票\n");
pthread_cond_broadcast(&cond);
}
sleep(1);
}
//回收线程
pthread_join(pth_id1, NULL);
pthread_join(pth_id2, NULL);
pthread_join(pth_id3, NULL);
//销毁互斥锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
如代码中演示的,当判断条件变量的时候,最好使用while进行判断,能够避免很多其他的逻辑错误。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)