线程编程模型和进程间通信概述
文章目录
一、前言
前面学了什么是线程,今天我们就来深入剖析一下线程;进程既然是独立的,那进程之间是怎样进行通信的呢?
二、线程编程模型
3.1 进程 & 线程
- 进程:资源独立
- 线程:资源共享
- 共同点:都具有调度能力(抢占CPU)
3.2 实现线程
3.2.1 目的
为了方便调用,这里的调用还是指系统调用(即:OS内核所支持的)
3.2.2 调度角度
OS内核不支持线程的话,只能是用户空间来描述;如果是支持,就可以通过软件的方式告诉系统调用是用户空间的线程还是内核空间的线程
默认是内核空间的线程(这个居多)
3.2.3 编程模型
工具
-
strace跟踪进程使用的syscall -
线程的第一个接口是
pthread_create注意这里不是标准C库 ,而是
libpthread.so,调用的是第三方库 实现,所以说明线程不是所有系统都支持这里的标准C库是指:以
printf为例,无论是Linux还是Windows,大家统一调用这个接口来打印,但这并不意味着接口内部的实现是一样的,这个是由厂家决定的。
编程
进程 VS 线程
c
#include <stdio.h>
#include <pthread.h> // 不是标准头文件,必须系统支持线程才可以
#include <unistd.h>
#include <stdlib.h>
// 被动调用:callback回调函数(系统准备好资源后才调用,不是我们主动调用的)
// 在图形页面开发中会用种思想(一般按钮的触发)
void task1(int flags)
{
for(int i = 0; i < 5; ++i)
{
printf("task running: %d\n", flags);
sleep(1);
}
// 这里不能用进程的退出模式exit,进程没了,线程也就随之消失
pthread_exit(NULL); // ()里面是接收地址的
}
int main()
{
pthread_t tid;
// C语言中,void*的本意是任意类型
// C++是强类型语言(需要进行强转)
int ret = pthread_create(&tid, NULL, (void *(*)(void *))task1, (void *)100);
// NULL:表示线程属性,一般为NULL,这里的属性控制当前产生的线程是内核级线程(调度的优先级更高)还是用户级线程
// void *(*start_routine)(void *) => (void *(*)(void *))task1是强类型转换,强制类型转换就是把名字删了(看起来复杂而已)
// 100 这里是可以传任意值的
// 函数成功返回0,失败返回非0值
if(ret)
{
perror("pthread create");
exit(-1);
}
printf("main process[%d] create thread: %lu\n", getpid(), tid);
// 这里说是主进程,也可以是主线程
// tid的类型是unsigned long
return 0;
}
运行结果:

解释:
-
现代OS把线程当作任务调度的单位,把进程当作资源管理的单位
-
tid:进程号和线程号是两个完全不一样的东西,完全没有关系 -
-lpthread:-l告诉编译器链接时找一个lib的前缀,pthread作为一个中间变量,.so的库,找到了就编译进去,否则会报错 -
运行结果为什么没有打印
task running: %d\n呢?创建线程执行之后,就会有两个调度体 ,它们就开始抢CPU,具体谁先执行是随机 的,万一进程执行完了,直接
return 0执行完了,资源回收了,这时线程资源也没有了,就会死掉因此线程虽然是共享资源,但是它也是依附于进程的资源,进程消失,共享进程的线程资源也统统消失
pthread_join可以等待线程退出
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void task1(int flags)
{
for(int i = 0; i < 5; ++i)
{
printf("task running: %d\n", flags);
sleep(1); // 时间睡觉
}
pthread_exit(NULL);
// pthread_exit(void *__retval);
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
printf("main process[%d] create thread: %lu\n", getpid(), tid);
// 父进程死掉了,子进程并没有死掉,而是成为了"孤儿进程"
// pthread_join(pthread_t __th, void **__thread_retur);
pthread_join(tid, NULL); // 这里是主进程在执行:阻塞(等待线程退出),不去抢占CPU
printf("main process exit!\n");
return 0;
}
运行结果:

解释:
-
操作系统有一个核心:节拍,根据节拍判断经历了多少时间
sleep(1)告诉OS在下一个1s的节拍去等着,一敲就唤醒睡觉的进程,就开始抢占CPU了(循环,直到完成退出)在睡眠(1s)的时候,CPU还有别的事情要干:屏幕刷新等。。。
-
跟踪可执行文件使用的系统调用
shellstrace ./build
任务创建最终映射的系统调用------
clone,复制资源,形成子任务。正所谓:进程要保护现场和恢复现场,都是靠栈。同样作为一个资源管理者,也需要有自己的栈(新的栈空间),但是占用的空间还是原来的进程(克隆的客体)的空间------共享作为一个任务须有虚拟内存,文件系统,文件等
虚拟内存是现代操作系统中的核心概念,它通过将物理内存和磁盘空间结合使用,为每个进程提供一个独立、连续且受保护的地址空间,从而解决了物理内存不足、内存碎片化、进程隔离和内存保护等关键问题。
多进程
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void task1(int flags)
{
for(int i = 0; i < 5; ++i)
{
printf("task running: %d\n", flags);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid1, tid2;
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
printf("main process[%d] create thread: %lu\n", getpid(), tid1);
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
printf("main process[%d] create thread: %lu\n", getpid(), tid2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process exit!\n");
return 0;
}
运行结果:

解释:
-
线程创建自己有自己的栈指针

OS对内存溢出有严苛的检测算法,因此在这里申请task1和task2,中间会存放一些特殊值,当访问越界立刻触发异常。
虽然任务体一致,但是栈指针不同。(尽管形参
flags一致,但是映射实参不一样) -
线程执行先后是随机的
后期会通过一定手段进行先后的控制,也就是同步机制。
多线程下的全局变量
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
int abc = 2; // 全局变量-数据区
void task1(int flags)
{
for(int i = 0; i < 5; ++i)
{
sleep(2);
abc = 100;
}
pthread_exit(NULL);
}
void task2(int flags)
{
for(int i = 0; i < 5; ++i)
{
abc = 200;
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid1, tid2;
printf("abc = %d\n", abc);
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
sleep(1);
printf("abc = %d\n", abc); // 第二个任务先执行
sleep(2);
printf("abc = %d\n", abc); // 看第一个任务能不能把abc改掉
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process exit!\n");
return 0;
}
运行结果:

解释:
- 虽然栈空间是独立的,如果能获得地址,依然可以共享。(栈平时只是通过传变量名进行修改,因此是不共享的)
- 全局变量在去全局中的变量名是统一的,因此是共享的
- 堆空间只要把地址传过去,就能共享
多线程下的栈空间
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
int *p;
void task1(int flags)
{
int a1 = 2;
p = &a1; // 栈空间的地址共享出去了
for(int i = 0; i < 8; ++i)
{
sleep(2);
printf("a1 = %d\n", a1);
}
pthread_exit(NULL);
}
void task2(int flags)
{
sleep(2);
*p = 200;
pthread_exit(NULL);
}
int main()
{
pthread_t tid1, tid2;
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process exit!\n");
return 0;
}
运行结果:

解释:
- 尽管是局部变量,还是有机会修改(地址暴露了)
线程安全
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
// 线程非安全函数
int *getAddr()
{
static int abc = 100;
return &abc; // 防止返回后变量不在了
}
void task1(int flags)
{
for(int i = 0; i < 8; ++i)
{
sleep(1);
printf("abc = %d\n", *getAddr());
}
pthread_exit(NULL);
}
void task2(int flags)
{
sleep(2);
int *p = getAddr();
*p = 200;
}
int main()
{
pthread_t tid1, tid2;
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process exit!\n");
return 0;
}
运行结果:

解释:
- 线程不安全,可通过加锁避免(后续讲)
读写冲突
c
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
int value1 = 0;
int value2 = 0;
void task1(int flags)
{
while(1)
{
value1++;
value2++;
}
}
void task2(int flags)
{
while(1)
{
if(value1 != value2)
{
printf("value1 = %d, value2 = %d\n", value1, value2);
}
}
}
int main()
{
pthread_t tid1, tid2;
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process exit!\n");
return 0;
}
运行结果:

解释:
-
可能在任务1执行完成之后,时间片到了,任务2没有执行,会造成不等的情况(一般是
value1 > value2) -
为什么会出现相等还是会打印呢?
整个执行++的过程分为好多步骤,比如:
LDR r0,[value1] ADD R0,R0,#1 STR R0,[value1] LDR R0,[value2] ADD R0,R0,#1 STR r0,[value2]多任务执行时就是抢占CPU,比较时也是分为很多步骤,彼此间错位会造成相等且打印的情况
读写锁:读是并发的,但是写就不可以读和写了(不同OS不一样)
时间片打断下多线程产生的执行冲突
c
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
int value1 = 0;
void task1(int flags)
{
for(int i = 0; i < 1000; i++)
{
value1++;
}
}
void task2(int flags)
{
for(int i = 0; i < 1000; i++)
{
value1++;
}
}
int main()
{
pthread_t tid1, tid2;
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process value1 = %d\n", value1);
return 0;
}
运行结果:

解释:
-
++:实际是读,加,写3步。读完还没有加,时间片到了,开始进行任务2,任务2执行完1000次,但是这时,任务1,开始加和写,任务2的"努力"就化为乌有了 -
临界资源:共同争抢的东西,访问临界资源的代码就是临界区
锁的力度:访问临界资源需要保护(锁就是特殊的PV)后面详细讲
三、进程间通信
调度体通信:进程间通信、线程间通信
今天我们先着眼于进程间通信吧~
3.1 三大挑战:
-
消息传递机制
不同的协议,发数据时就会没法复原(粘包)------属于计算机网络的事
-
同步与互斥★
同步:任务A先于B还是后于B
互斥:我做事时你别插手(我用的时候,其他人都不能访问)
-
数据顺序与同步
3.2 竞态条件
我在用的时候,你可能也要用,就要考虑竞争

A和B就是处于竞争状态:A干活的时候被打断,B可能进行了修改,等到A恢复的时候就被打乱了
四、小结
线程的共享的理解又上了一个新台阶:线程依附于进程,因此进程的资源消失线程也会随之一起消失;线程的共享本质是通过传栈的地址,从而使得进程维护的栈空间被共享。共享也引出来了一些非安全的问题:时间片的打断使得线程之间发生了冲突,本身线程之间就存在抢占CPU的关系,两者还共享一个资源,这就对于资源的访问产生了一定的冲突。
进程间的通信同样也因为竞争产生了同步和互斥的问题,我想,这将会在下一个篇章中得到解决~敬请期待