多进程及多线程编程
程序员文章站
2022-06-27 20:45:16
进程进程是操作系统中分配与调度的最小单位进程是程序下内存中的镜像进程不是凭空产生的,系统中所有的进程都是由systemd进程克隆而来进程通过fork()函数复制,而后通过exec()函数来进程“变身”,去完成各种各样的任务进程的创建在系统中,所有的进程都是由1号进程克隆⽽来,在我们进⾏系统编程的时候,可以使⽤fork函数来创建⼀个⼦进程,被克隆的进程叫做⽗进程。⽗⼦进程在创建后,基本相同,除了以下⼏点:(1)⼦进程的pid是新分配的,与⽗进程不同(2)⼦进程的ppid会设置为⽗进程的...
进程
- 进程是操作系统中分配与调度的最小单位
- 进程是程序下内存中的镜像
- 进程不是凭空产生的,系统中所有的进程都是由systemd进程克隆而来
- 进程通过fork()函数复制,而后通过exec()函数来进程“变身”,去完成各种各样的任务
进程的创建
- 在系统中,所有的进程都是由1号进程克隆⽽来,在我们进⾏系统编程的时候,可以使⽤fork函数来创建⼀个⼦进程,被克隆的进程叫做⽗进程。
- ⽗⼦进程在创建后,基本相同,除了以下⼏点:
(1)⼦进程的pid是新分配的,与⽗进程不同
(2)⼦进程的ppid会设置为⽗进程的pid
(3)⼦进程中的资源统计信息会清零
(4)所有挂起的信号都会被清除,不会被⼦进程继承
(5)所有⽂件锁也不会被⼦进程继承
进程的变身:exec
exec函数族说明
这些函数很好记,都是以exec开始,形式为exec[lv][pe]:
- 其中l和v分别表示⽤列表还是数组(向量)的⽅式提供参数
- p表示会在⽤户的path路径下查找可执⾏⽂件
- e表示会为新进程提供新的环境变量
exec函数做了什么
- 改变了地址空间和进程映像;
- 挂起的所有信号都会丢失;
- 后续捕捉到的所有信号都会还原为默认处理⽅式;
- 丢弃所有内存锁;
- ⼤多数线程属性会还原为默认值;
- 重置⼤多数进程相关的统计信息;
- 清空和进程内存地址空间相关的所有数据,包括所有映射的⽂件;
- 清空所有只存在于⽤户空间的数据。
进程的退出
#include <stdlib.h>
void exit(int status);
status可以使⽤两个宏定义:EXIT_SUCCESS
和 EXIT_FAILURE
在进程调⽤exit之后,C库会按顺序完成以下事件:
- 调⽤任何由atexit()或on_exit()注册的函数;
- 清空所有已打开的标准I/O流;
- 删除由tmpfile()函数创建的所有临时⽂件;
完成上⾯的操作后,exit()会调⽤_exit(),内核接管下⾯的事: - 清理进程所创建的、不再使⽤的所有资源;
- 销毁进程;
- 告知⽗进程其⼦进程已被终⽌。
wait() & waitpid()
- ⼦进程活着,⽗进程结束,则⼦进程变为孤⼉进程
- ⼦进程死了,但⽗进程不为其“收⼫”(释放资源),则⼦进程变为僵⼫进程
- 孤⼉进程可以被1号进程收养,对系统⽆害
- 僵⼫进程已经不⼯作,但是依然占有资源,对系统有害,需避免
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
- wait和waitpid最终都会将⼦进程的退出状态存放在status地址中
- wait需要等待任何一个子进程退出
- waitpid中的第⼀个参数pid,可以有以下⼏个值:
-
pid < -1
:等待⼀个进程组ID为pid绝对值的任何⼦进程退出,如果pid=-
500,则等待进程组ID为500的任何⼦进程退出; -
pid = -1
:等待任何⼀个⼦进程退出,⾏为和wait()⼀致; -
pid = 0
:等待同⼀个进程组中的任何⼦进程退出; -
pid > 0
:等待进程pid为pid的⼦进程退出。 - option参数⼀般默认为0
线程
线程特点
- 线程是操作系统调度器
可调度
的最小执行单元
; - 现代操作系统实现了两种对用户空间的基础虚拟抽象:虚拟内存和虚拟处理器;
- 进程感觉自己独占了内存,而线程感觉自己
独占了处理器
; - ⼀个进程至少包含⼀个线程,⼀个进程的多个线程共享内存空间;
- 每个线程都
独立调度
,调度和切换的代价低于进程
。
多线程的好处
- 编程抽象模块化
- 增加程序的并发性
- 提高响应能力
- IO阻塞可行
- 上下文切换代价小
- 内存保存,线程之间共享内存,切换无需置换内存
线程模式
每个连接对应⼀个线程
- ⼀个线程对应着⼀个连接或者请求,直到处理结束,这样线程就可以处理另⼀
个新的请求了; - 在此模式下,将线程换成进程,就是⽼版的Unix服务器了,Apache的标准,fork模式就是这种模式。
事件驱动的线程模式
- 将上个模式中的负荷(等待)剥离出来,搭配异步IO或者IO复⽤来管理服务器中
的数据流。 - 在此模式下,将处理请求转换为⼀系列的异步IO和与其关联的回调函数;
- 这些回调函数可通过IO多路复用的方式来等待,完成该操作的进程称为event_loop。
线程的竞争
- 同⼀个进程的线程共享内存空间,独⽴调度,并发执⾏时“重叠执行”,以不可预期的顺序执行,如果多个线程访问同⼀个资源,就产⽣了竞争,程序由于不确定哪个线程先执行而带来的行为不⼀致;
- 竞争的条件:两个或更多的线程对共享资源非同步访问;
- 共享资源可以是系统硬件、内核资源以及内存中的数据(data race);
- 竞争发⽣的窗口,也就是需要同步的代码区称为临界区。
线程的同步
- 解决线程竞争的办法主要是线程同步,也就是让原先不确定的访问顺序,变得确定;
- 也就是要在临界区这个窗口内,要保证执行同步访问操作,确保对临界区的访问是互斥的;
- 最常见的实现就是在临界区前加互斥锁,在临界区后释放互斥锁,这样当⼀个线程占有锁的时候,其他线程就会被阻塞,直到锁被释放才可以获得锁,继续执⾏。
创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
Compile and line with - pthread.
参数
-
thread
:如果线程创建成功,将会把线程的ID保存到thread指向的空间; -
attr
:⽤于改变新创建线程的默认线程属性,⼀般默认NULL就可以; -
start_routine
: 线程执⾏函数,形式应类似为 -void * start_thread(void *arg):- 参数为void指针
- 返回值也是void指针
-
arg
: arg为传递给第三个参数,也就是线程执⾏函数的参数,需要注意的是,如果要传递多个参数,应该⽤结构体封装。
线程ID
-
线程ID和进程ID相似,但有⼀个本质的区别:进程ID是由内核分
配的,而线程ID是由线程库分配的; -
获取自己的线程ID:
pthread_t pthread_self(void);
-
判断两个线程是否相同:
int pthread_equal(pthread_t t1, pthread_t t2);
-
因为POSIX不要求pthread_t是⼀个算数类型,所以不能保证可以使⽤
==
判断线程ID是否相同。
线程终止
-
线程会在下⾯的情况下退出,这和进程近似:
- 线程在启动时返回,该线程就结束
- 如果线程调⽤了
pthread_exit()
,线程就会终⽌ - 如果线程被另⼀个线程通过
pthread_cancel()
函数取消,
也会终⽌
-
在以下场景中,所有线程都会被杀死,进程被杀死:
- 进程从主函数中返回
- 进程通过exit()函数终结
-
线程的自杀
#include <pthread.h> void pthread_exit(void *retval); pthread_exit(NULL);
-
线程的他杀
#include <pthread.h> int pthread_cancel(pthread_t thread); pthread_cancel(you);
线程加入(join)和分离(detach)
-
由于线程创建和销毁很容易,必须对线程进⾏同步的机制,避免
被其他线程终止,对应的线程即wait(),也就是join线程; -
线程join:支持⼀个线程阻塞,等待另⼀个线程终止
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
-
线程detach:默认情况下,线程都是可join的,detach可以让其不可join
#include <pthread.h> int pthread_detach(pthread_t thread);
线程互斥
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
本文地址:https://blog.csdn.net/qq_42880329/article/details/107395438