并发
1.进程
1.运行程序的过程:写出程序 (工程)-->编译生成 可执行文件 (二进制文件)a.out -->运行./a.out
2.CPU执行程序 --- 执行指令:将程序 加载到内存, 取指-执行-写回
3.指令流水线 :利用上述 步骤所需资源不同 使用不同的硬件来执行上述操作 提高CPU的执行效率
我们称这种执行过程为 并发执行
4.并发 -- 进程 之间的关系
现代操作系统为了实现 并发,引入了进程这一概念 (进程是基于操作系统之上的概念)
1.1进程
./a.out 进程 正在进行的程序 动态
a.out 程序文件 静态
linux 操作系统 描述一个进程 PCB(进程控制块),task_struct 来描述一个进程
task_struct /* 保存程序代码(命令行参数、环境变量...) mm 进程的工作目录-用户 filesystems 进程文件表项 files 信号 signal */
进程地址空间:
每运行一个进程,操作系统就会为它开辟一块空间(进程地址空间)来保存相应的数据/指令
根据数据属性的不同 进行了分段保存
栈(.stack):保存局部变量、函数栈,向下生长。
堆(.heap):malloc申请的空间,向上生长。
未初始化数据段(.bss):保存未初始化的局部变量和未初始化的static修饰的局部变量。
初始化数据段(.data):保存初始化的局部变量和初始化的static修饰的局部变量。
字符常量区(.rodata):只读数据段,例如char *p = "hello world";
中的 hello world
。
代码区(.text):文本段,保存代码。
共享区:映射mmap
进程和程序的区别:
- 程序是静态的 进程是动态的
- 进程是程序的一次执行活动 ; 一个程序可以对应多个进程
- 进程是一个独立的活动单位 进程是竞争系统资源(进程地址空间)的基本单位
1.2进程的状态
OS 把一个程序的执行过程,分为不同的几个阶段(状态):
就绪态(Ready) 准备工作已做好 获得CPU即可执行
运行态(Running) 获得CPU 进程执行
阻塞态(Wait/Blocking) 进程等待其他外部事件的发生
进程的这些状态之间可以进行切换。
就绪队列:Ready Queue 所有处于 Ready 状态的进程 都在后备队列
调度程序:调度策略 (分时系统、实时系统)
分时系统:调度策略以 时间片轮转 为主要策略的系统
实时系统:调度策略 以实时(任务的优先级)策略 为主要的策略
时间片轮转:分时,每个进程执行一段时间
僵尸态:进程已经退出不再调度,但是这个进程的资源还没有完全被释放。
僵尸进程的产生:子进程先于父进程退出,但是父进程没有关注到子进程的退出,因此系统不会完全释放子进程的资源,这个子进程进入僵尸状态。子进程退出后,在进程pcb中保存了自己的退出返回值,在父进程没有关注处理的情况下,pcb资源是不会被释放。
孤儿进程:父进程先于子进程退出,子进程就会成为孤儿进程,运行在后台,孤儿进程的父进程就会成为1号进程(1号进程早期名字叫init
进程,后期叫systemd
进程)
1.3进程相关API
1.3.1创建一个新进程
fork
函数用来创建一个进程 子进程的数据 (数据+指令) 来源于父进程 ,fork函数 首先拷贝(clone)了本身的数据到子进程的进程地址空间中(父进程的全部用户数据、标准IO缓冲区、文件描述符、信号处理方式 ...)像父进程的pid 以及ppid 没有拷贝,拷贝完之后,子进程才独立。
//头文件 #include <sys/types.h> #include <unistd.h> //声明 pid_t fork(void); /** *返回值: * 0 子进程返回 * >0 父进程返回,返回子进程的pid * -1 失败,errno被设置 */
1.3.2进程号
linux系统会为每一个进程 分配唯一的一个进程号 PID 使用类型 pid_t 进行描述
linux 系统提供了两个API -- getpid
getppid
用来获取自身以及父进程的PID
//头文件 #include <sys/types.h> #include <unistd.h> //声明 pid_t getpid(void); /** 返回值: 返回自身进程号 */ pid_t getppid(void); /** 返回值: 返回父进程进程号 */
1.3.3进程的退出状态
进程的退出状态 可以通过 进程号 来获取的
wait waitpid
进程退出的状态:
-
自杀
-
main函数退出
-
调用
exit
/_exit
//头文件 #include <stdlib.h> //声明 void exit(int status); //头文件 #include <unistd.h> //声明 void _exit(int status); /** status:退出码 退出状态 由程序员 自己指定含义 exit:正常退出 -- 做一些清理工作 (将缓冲区数据同步...) _exit:快速退出 -- 不做清理工作 */
-
-
他杀
被 操作系统 杀死
等待子进程状态改变:
wait
waitpid
//头文件 #include <sys/types.h> #include <sys/wait.h> //声明 pid_t wait(int *wstatus);// 等待任意子进程的状态改变 /** wstatus:保存退出码 返回值: 成功 退出进程的PID 失败 返回-1 errno被设置 */ pid_t waitpid(pid_t pid, int *wstatus, int options);// 等待PID指定的子进程的状态改变 /** pid:你要等待哪个子进程 pid < -1 表示等待组ID 为 pid的绝对值 的那个组的任意进程 pid > 0 等待指定的进程 退出 wstatus:保存退出码 options:等待选项 0 阻塞 WNOHANG 非阻塞等待 返回值: 成功 退出进程的PID 失败 返回-1 errno被设置 */ //wstatus:保存了 对应进程的结束原因 WIFEXITED(wstatus)//为真 表示进程正常退出(自杀) WEXITSTATUS(wstatus)//当WIFEXITED(wstatus) 为真 才解析返回值 返回 进程的返回值/退出值 WIFSIGNALED(wstatus)//为真 表示被信号干掉 WTERMSIG(wstatus)//返回 信号编号
1.3.4 exec
函数族
exec函数族主要作用是 让一个进程去执行另一个指定的程序文件,换句话说就是 把一个指定的程序文件中的 数据和指令 替换掉 调用进程的数据和指令
shell 终端也是一个进程 bash 如何执行 ./a.out 先fork一个进程 再调用exec函数族 将fork出来的进程 的 进程地址空间中的数据全部替换成 a.out 的数据
//头文件 #include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ... /* (char *) NULL */); /** *path:程序文件路径名 *arg, ...:程序运行参数 *返回值: * 成功永不返回 * 失败返回-1 */ int execlp(const char *file, const char *arg, ... /* (char *) NULL */); /** *file:程序文件名 *arg, ...:程序运行参数 *返回值: * 成功永不返回 * 失败返回-1 */ int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
1.3.5 system
可以实现在进程内部调用其他进程,但是并不改变当前进程的内容
//头文件 #include <stdlib.h> //声明 int system(const char *command);
2.进程间通信
2.1管道
管道其实就是内核中的一段空间,但是管道的使用和文件类型(使用的是文件IO的接口),管道文件在外部,内容在内核中。
管道分为 有名管道 和 无名管道。
2.1.1无名管道pipe
无名管道在文件系统中 没有inode
,只有内容保存在内核中, 但是可以使用read/write
进行读写
无名管道的使用不能使用open
,使用的打开/创建接口是 pipe
pipe
在创建无名管道时 会在内核开辟一块缓冲区 作为pipe
文件内容的存储空间,并且会返回两个文件描述符 一个用来读,一个用来写
//头文件 #include <unistd.h> //声明 int pipe(int pipefd[2]); /** pipefd: 2个元素 元素类型为int pipefd[0] 保存了read的文件描述符 pipefd[1] 保存了write的文件描述符 返回值: 成功,返回0 失败,返回-1 errno 被设置 */
pipe
无名管道的特点:
pipe
有两端 一端用来读 一端用来写- 按顺序读取 不能
lseek
- 内容读完了就没有了,水流
pipe
无名管道随内核持续pipe
只能在亲缘(类似于父子)进程之间进行通信pipe
虽然既可以读 也可以写,但是这样会产生自收自发的情况,所以一般在项目中使用多个管道实现全双工通信
2.1.2有名管道fifo
fifo
是在 pipe
的基础上 多了一个inode
,使得进程可以通过路径来打开这个管道
fifo
虽然有inode
,但是文件内容还是存在于内核中
fifo
有名之后 就可以使用 open
进行打开了
mkfifo
:创建一个有名管道
//头文件 #include <sys/types.h> #include <sys/stat.h> //声明 int mkfifo(const char *pathname, mode_t mode); /** pathname:fifo 在linux文件系统中的路径 mode:要创建文件的权限 返回值: 成功,返回0 失败,返回-1, 并且errno被设置 */
指令中使用管道的例子:
ps -ef | grep a.out # grep a.out 查找 ps -ef 列举出的进程中带 a.out 名字的进程 # | 表示管道
2.2信号
信号也是进程间通信的一种,但是不是用来传输数据的
只是内核中传递的一个信号/整数,信号的表示是一个整数。不同的信号值表示不同的含义,当然用户可以自定义信号的含义。信号值不要和系统的信号重复。
First the signals described in the original POSIX.1-1990 standard. Signal Value Action Comment ────────────────────────────────────────────────────────────────────── SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process 控制终端的挂起操作,或者控制进程死亡时 控制终端上的所有进程都会收到 SIGHUP SIGINT 2 Term Interrupt from keyboard 从键盘上收到中断信号 ctrl + c SIGQUIT 3 Core Quit from keyboard 从键盘上收到退出信号 ctrl + \ SIGILL 4 Core Illegal Instruction 非法指令 SIGABRT 6 Core Abort signal from abort(3) 调用abort函数 就会收到这个信号 SIGFPE 8 Core Floating-point exception 浮点运算异常 SIGKILL 9 Term Kill signal 不能捕获 kill -9 就是发送 SIGKILL SIGSEGV 11 Core Invalid memory reference 非法内存访问 段错误 SIGPIPE 13 Term Broken pipe: write to pipe with no readers; see pipe(7) 管道写时没有读端 收到SIGPIPE SIGALRM 14 Term Timer signal from alarm(2) 定时信号,定时多长时间后这个进程就会结束,调用alarm这个函数,定时时间到了,产生SIGALRM SIGTERM 15 Term Termination signal 终止信号 SIGUSR1 30,10,16 Term User-defined signal 1 SIGUSR2 31,12,17 Term User-defined signal 2 预留给用户自定义的信号 SIGCHLD 20,17,18 Ign Child stopped or terminated 当子进程停止/终止 父进程会收到 SIGCHLD SIGCONT 19,18,25 Cont Continue if stopped 继续运行 如果被停止 SIGSTOP 17,19,23 Stop Stop process 将进程 停止 SIGTSTP 18,20,24 Stop Stop typed at terminal ctrl + z 会发送这个信号 输入fg/bg 可以继续运行的 SIGTTIN 21,21,26 Stop Terminal input for background process SIGTTOU 22,22,27 Stop Terminal output for background process The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
- term:terminate 终止
- Ign:ignore 忽视
- core:输出信息,在终止
- Stop:停止
- Cont:继续
2.2.1信号的处理
进程收到信号,处理方式有三种:
-
捕获。把一个信号 与 用户自定义的信号处理函数 关联起来。当收到这个信号的时候,就会自动调用 自定义的 信号处理函数
-
默认行为。收到信号时,采用操作系统默认的行为
-
忽略。ignore 收到信号相当于没有收到
2.2.2信号相关API
-
发送信号
//头文件 #include <sys/types.h> #include <signal.h> //声明 int kill(pid_t pid, int sig); /** pid:指定你要发送信号的那个进程 > 0 发送方 对应pid 的那个进程 = 0 发送给同组的进程 = -1 发送给所有进程 (有权限) < -1 发送给 进程组id=abs(pid) 的所有进程 sig:你要发生的那个信号 返回值: 成功,返回0 失败,返回-1,errno被设置 */ -
raise
:发送信号给自己//头文件 #include <signal.h> //声明 int raise(int sig); -
alarm
:设置一个发送信号的闹钟//头文件 #include <unistd.h> //声明 unsigned int alarm(unsigned int seconds); /** seconds:多少秒之后 发送一个闹钟信号 返回值: 返回上一个闹钟的剩余秒数 */ - 每个进程有且仅有一个闹钟
-
捕获信号
signal
//头文件 #include <signal.h> // 函数指针类型 指向的函数类型为 void (int) typedef void (*sighandler_t)(int); //声明 sighandler_t signal(int signum, sighandler_t handler); /** signum:你要捕获的信号 handler:信号处理函数 返回值: 成功,返回 修改前的信号处理函数 失败,返回SIG_ERR errno被设置 */ -
等待信号
pause
//让进程/线程休眠直到有信号来临 //头文件 #include <unistd.h> //声明 int pause(void); /** 返回值: 成功,返回信号值 失败,返回-1, errno == EINTR */
2.3共享内存
进程间通信方式之一,多个进程可以共享一段内存 “共享内存”
这段内存 存在于 内核中,多个进程都可以访问,但是这种方式相较于其他 IPC 内存拷贝会少一次。
实现方式:
在内核中开辟块内存,其他进程通过 映射的方式 获取这段共享内存的首地址
进程1可以映射这个内存,同时进程2也可以映射这块内存 ...
进程1 往共享内存中输入数据,相当于就是 往进程2的进程地址空间中写入数据。
2.3.1 System V 共享内存
共享内存操作流程:
- ftok 创建/获取 System V IPC对象 的 key
- 创建一个共享内存
- 映射
- 读写 System V IPC 的 API 接口
- 解除映射关系 和 关闭 共享内存
生成key
//头文件 #include <sys/types.h> #include <sys/ipc.h> //声明 key_t ftok(const char *pathname, int proj_id); /** pathname:路径名 proj_id:工程id 返回值: 成功,返回 System V IPC 的 key 失败,返回-1, errno被设置 */
创建共享内存
//头文件 #include <sys/ipc.h> #include <sys/shm.h> //声明 int shmget(key_t key, size_t size, int shmflg); /** key:ftok的返回值 size:指定共享区域的大小 创建时,区域大小 为4096的倍数 打开时,size == 0 shmflg:标志位 1. 创建 IPC_CREAT | 权限 IPC_CREAT | 0664 2. 打开 0 返回值: 成功,返回 共享内存的id id 标识这块内存 失败,返回-1, 并且errno 被设置 */
映射/解除映射
//头文件 #include <sys/types.h> #include <sys/shm.h> //声明 void *shmat(int shmid, const void *shmaddr, int shmflg); /** shmid:shmget 的返回值 共享内存的标识ID shmaddr:指定 将共享内存 映射到 进程地址空间的哪个区域 NULL 由系统自行分配 shmflg:进程对映射区域的操作方式 SHM_WRONLY 只写 SHM_RDONLY 只读 0 读写 返回值: 成功,返回映射区域的首地址 失败,返回NULL errno 被设置 */ //声明 int shmdt(const void *shmaddr); /** shmaddr:映射区域的首地址 返回值: 成功,返回0 失败,返回-1,errno 被设置 */
其他操作shmctl
//头文件 #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); /** shmid:共享内存的标识ID cmd:控制指令 IPC_RMID 删除指定id的那个共享内存 buf:保存第二参数相关的结果 if cmd == IPC_RMID buf == NULL IPC_STAT 获取属性 获取到的属性就会保存在buf中 IPC_SET 设置属性 待设置的属性需要保存在buf中 返回值: 成功,返回0 失败,返回-1,errno 被设置 */
2.4信号量
如果有两个进程 对共享内存中的 变量a 进行自加运算,各自加十万次,请问这个a 最终值为多少?a初值为0。
这个值不确定,i++ 这个操作不是一步到位,有可能执行中间某一步骤时,时间片结束,让出CPU,导致结果异常。
对于上述 a 是一个共享资源,对于a的访问,需要进行控制/保护,使之变成有序访问。不保护就会出现上述这个情况。
2.4.1信号灯/信号量
信号量 (semaphore)是一种 用于提供 不同进程/一个进程中不同线程间的同步的一种机制。
进程/线程:并发的实体
同步:并发间的实体 相互等待,相互制约 ,有序的、有条件的访问。
信号量的作用就是为了保护共享资源而存在,让共享资源有序访问。
信号量机制 其实就是 程序员之间的约定,用来保护共享资源:
进程A和进程B都需要访问一个互斥设备,那么可以使用信号量来表示这个设备能不能被访问。约定,只要进程在访问这个设备之前,先访问信号量,如果能访问设备,则将信号量设置为上锁状态,然后再去访问该设备,访问完设备之后,应当将信号量设置为解锁状态。
2.4.2信号量如何实现
- 创建一个信号量
- 等待一个信号量(p操作)
- 释放一个信号量(v操作)
上锁 -> 临界操作 -> 解锁
- p操作和v操作都是原子操作
- 原子操作:不可被打断执行的操作,配合硬件实现的。
2.4.3 System V信号量API
创建打开一个信号量semget
//头文件 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> //声明 int semget(key_t key, int nsems, int semflg); /** *key: ftok的返回值 *nsems:要创建的信号量集中的信号量个数 * nsems 在创建时 表示制定生成的 信号量集中元素的个数 * 在打开时 指定为0即可 *semflg:标志位 * 1. 创建 IPC_CREAT | 权限位 * 2. 打开 0 *返回值: * 成功,返回id,这个id 标识这个信号量集 * 失败,返回-1,errno被设置 */ //信号的初始值 决定了 对应的资源是互斥的还是非互斥,信号量集一旦被创建就需要马上指定其中信号量的初始值
控制操作semctl
//头文件 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...); /** *semid:semget 的返回值, 信号集的标识id *semnum:你要操作信号量集中的哪一个信号量,就是信号量数组的下标 *cmd:命令号,你要进行什么操作 * GETVAL 获取 semnum 所对应的信号量的值 * SETVAL 设置 semnum 所对应的信号量的值 * GETALL 获取 semid 所对应的信号量集中所有信号量的值 * SETALL 设置 semid 所对应的信号量集中所有信号量的值 * IPC_RMID 删除 semid 信号量集 * ... * ...:依赖于 cmd 的含义而决定是否需要 * cmd == IPC_RMID 不需要第四个参数 * == GETVAL 不需要第四个参数,信号量的值将通过返回值返回 * == SETVAL 第四个参数表示你要设置的int VAL 值 * == GETALL 第四个参数 需要一个 unsigned short 数组 * 保存所获取的信号量集中所有信号量的值 * == SETALL 第四个参数 需要一个 unsigned short 数组 * 保存所设置的信号量集中所有信号量的值 *返回值: * 会根据cmd 的不同而有不同含义 * 失败,返回-1,并且errno被设置 */
P/V 操作:semop / semtimeop
//头文件 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> //声明 int semop(int semid, struct sembuf *sops, size_t nsops); /** *semid:要操作的信号量集 id *sops:要操作的信号量(集) *nsops:你要操作多少个信号量 *返回值: * 成功,返回0 * 失败,返回-1,errno 被设置 */ // 限定等待时间,由timeout指定,超过指定时间没获取到就不等待了 int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout); struct timespec { time_t tv_sec; // 秒数 long tv_nsec; // 纳秒数 } // 通过设置 struct sembuf 来确定现在是P操作还是V操作 struct sembuf { unsigned short sem_num; /* semaphore number */ //sem_num:你要操作的信号量集中的信号量的下标 short sem_op; /* semaphore operation */ /* sem_op >0 V操作 == 0 尝试获取,看是否会阻塞 <0 P操作 */ short sem_flg; /* operation flags */ /* sem_flg == 0 默认 如果P操作获取不了,则会 阻塞 IPC_NOWAIT 非阻塞 获取不了 就获取 SEM_UNDO 撤销 为了防止 进程带锁退出 撤销的操作将由 内核来进行维护, 在进程退出时,内核会还原该进程对信号量的操作 */ } //如果 一个信号量集中 需要操作多个信号量,需要用到多个struct sembuf(数组)
2.4.4 POSIX信号量API
打开/创建一个有名信号量sem_open
//头文件 #include <fcntl.h> /* For O_* constants */ #include <sys/stat.h> /* For mode constants */ #include <semaphore.h> sem_t *sem_open(const char *name, int oflag); sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); /** *name:要创建/打开 有名信号量的在文件系统中的文件名 * 路径名 中只能有 一个 "/" * 如 "/test.sem" OK * "/test/test.sem" 不OK *oflag: 1.创建 O_CREAT * 2.打开 0 *mode:权限位 宏 0666 -- 八进制 *value:指定创建的有名信号量的初始值 *返回值: * 成功,返回sem_t 指针 * 失败,返回SEM_FAILED , errno 被设置 */ //Link with -pthread. 使用这套接口需要再编译时指定 pthread这个库
创建/初始化一个无名信号量sem_init
//头文件 #include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); /** *sem:指向要初始化的一个无名信号量 *pshared:该无名信号量的共享方式 * 0:表示进程内部 的线程共享 sem 指向的是进程内部 * 1:不同进程间的共享 sem 指向的是 内核共享区域 *value:指定无名信号量的初始值 *返回值: * 成功,返回sem_t 指针 * 失败,返回SEM_FAILED , errno 被设置 */
p操作
//头文件 #include <semaphore.h> // 阻塞获取 对应的信号量,不能获取就阻塞 int sem_wait(sem_t *sem); // 非阻塞获取,不能获取 就不结束 int sem_trywait(sem_t *sem); // 等待一段时间,在这段时间内获取不到 就不获取了 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); /** *abs_timeout:绝对时间 *返回值: * 成功,返回0 * 失败,返回-1,errno被设置 */
v操作
//头文件 #include <semaphore.h> int sem_post(sem_t *sem); /** * sem:你要释放的那个信号量 *返回值: * 成功,返回0 * 失败,返回-1,errno被设置 */
其他操作sem_getvalue
//头文件 #include <semaphore.h> int sem_getvalue(sem_t *sem, int *sval); /** *sem:你要获得的那个信号量值的指针 *sval:用来保存获取的信号量的值 *返回值: * 成功,返回0 * 失败,返回-1,errno被设置 */
有名信号量的关闭/删除:
SYNOPSIS #include <semaphore.h> int sem_close(sem_t *sem); /** *sem:你要关闭的那个信号量值的指针 *返回值: * 成功,返回0 * 失败,返回-1,errno被设置 */ int sem_unlink(const char *name); /** *name:你要删除的那个有名信号量在文件系统中的路径 *返回值: * 成功,返回0 * 失败,返回-1,errno被设置 */
无名信号量的销毁:
//头文件 #include <semaphore.h> int sem_destroy(sem_t *sem); /** *sem:你要销毁的那个无名信号量 *返回值: * 成功,返回0 * 失败,返回-1,errno被设置 */
3.线程
为了程序 能够实现并发,引入了进程的概念:存在某些问题
- 创建一个进程的开销比较大, 申请一个新的进程地址空间
- 进程间通信需要第三方:文件 内核 云端
于是,有人提出能不能再一个进程内部实现 并发? ---- 线程/LWP 轻量级进程
3.1线程
线程是比进程更小的活动单位,它是进程中的一个执行分支,线程也是并发的一种情形。进程内部可以有多个线程,他们之间并发执行,且线程之间共享整个进程的资源。
进程是系统资源分配的最小单位,线程是任务调度的最小单位。
线程的特点:
- 创建一个线程的开销比较小
- 实现线程间通信比较方便
- 线程也是一个动态概念,线程本体是需要 被调度 (状态切换)
- 线程是任务调度的最小单位。
共享资源:
- 内存地址空间:同一进程中的多个线程共享相同的内存地址空间。这意味着它们可以访问相同的内存区域,例如全局变量和共享数据。
- 全局变量和共享数据:在进程范围内定义的变量和数据结构可以被多个线程访问和修改。
- 文件描述符:线程可以共享打开的文件描述符,以便它们可以访问相同的文件和 I/O 资源。
- 信号处理器:线程可以共享信号处理器,以便它们可以处理相同的信号。
- 当前目录和环境变量:线程在创建时继承了其父进程的当前目录和环境变量。
- 用户 ID 和组 ID:线程继承了其父进程的用户 ID 和组 ID,这有助于确定线程对系统资源的访问权限。
独立资源:
- 栈:每个线程都有自己的栈,用于存储局部变量、函数调用和返回地址等信息。线程之间不能直接访问彼此的栈。
- 线程 ID:每个线程都有唯一的线程 ID,用于在系统中标识和区分不同的线程。
- 进程 ID:每个进程都有唯一的进程 ID,用于在系统中标识和区分不同的进程。线程可以访问其所属进程的进程 ID,但不能访问其他进程的进程 ID。
3.2线程函数
void *threadfunc(void *arg);
3.3线程API
创建一个线程pthread_create
//头文件 #include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); /** *thread:保存线程号,每一个线程都有一个属于自己的id,称之为tid,类似于pid,用来唯一标识一个线程 *attr:线程属性,一般用NULL 表示默认,修改属性可以用其他的函数接口去修改 *start_routine:函数指针,指定线程函数 *arg:传递给线程函数的参数 ,不传递 给NULL即可 *返回值: * 成功,返回0 * 失败,返回一个错误号,第一参数中的内容是未定义的 */
线程退出pthread_exit
//头文件 #include <pthread.h> void pthread_exit(void *retval); //retval:保存线程函数的返回值
取消其他线程pthread_cancel
//头文件 #include <pthread.h> int pthread_cancel(pthread_t thread); /** *thread:指定你要取消的那个线程 *返回值: * 成功,返回0 * 失败,返回非0的错误号 */
- 线程能否被取消要看被取消的线程是否具备被取消的属性
设置线程的可取消属性pthread_setcancelstate
SYNOPSIS #include <pthread.h> int pthread_setcancelstate(int state, int *oldstate); /** *state:指定要设置的属性 * PTHREAD_CANCEL_ENABLE 使能被取消 * PTHREAD_CANCEL_DISABLE 失能被取消 *oldstate:保存上一次的属性 *返回值: * 成功,返回0 * 失败,返回非0的错误号 */
线程退出资源释放
释放一个线程的资源可以自动释放和手动释放
如果设置了分离属性,线程结束之后,资源被自动释放掉
如果没有设置分离属性,线程结束之后,资源需要被手动释放
设置线程的分离属性pthread_detach
//头文件 #include <pthread.h> int pthread_detach(pthread_t thread); /** *thread:指定哪个线程的分离属性 *返回值: * 成功,返回0 * 失败,返回非0的错误号 */
手动释放
//头文件 #include <pthread.h> int pthread_join(pthread_t thread, void **retval); /** *thread:等待tid 指定的线程的退出 *retval:接收线程的返回值 *返回值: * 成功,返回0 * 失败,返回非0的错误号 */
4.线程间同步
4.1互斥锁
初始化一个互斥锁pthread_mutex_init
//头文件 #include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); /* *restrict mutex:要初始化的线程互斥锁指针 *restrict attr :线程互斥锁的属性,一般使用NULL ,默认属性 * 线程互斥锁的默认初始值为 1 *返回值: * 成功,返回0 * 失败,返回非0 */ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
p操作pthread_mutex_lock/pthread_mutex_trylock/pthread_mutex_timedlock
//头文件 #include <pthread.h> // p操作 如果不能获取 就等待 直到能够获得锁 int pthread_mutex_lock(pthread_mutex_t *mutex); // p操作 如果不能获取 就不等待 int pthread_mutex_trylock(pthread_mutex_t *mutex); #include <pthread.h> #include <time.h> // p操作 如果在等待时间内不能获取 就不等待 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
v操作pthread_mutex_unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
4.2条件变量
初始化/销毁一个条件变量pthread_cond_init
pthread_cond_destroy
//头文件 #include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond); /** *cond:要销毁的那个条件变量 *返回值: * 成功,返回0 * 失败,返回非0 */ int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); /** *restrict cond:保存初始化后的条件变量 *attr :属性,一般为NULL 采用默认属性 *返回值: * 成功,返回0 * 失败,返回非0 */ pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待一个条件变量pthread_cond_wait/pthread_cond_timedwait
//头文件 #include <pthread.h> int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); /** *cond:要等待的条件 *mutex:指向线程互斥锁,调用 pthread_cond_wait 之前,mutex是一个locked 状态 *返回值: * 成功,返回0 * 失败,返回非0 */
pthread_cond_wait
会使线程进入休眠,在休眠前会释放锁,在被唤醒时会申请锁
唤醒条件变量pthread_cond_signal/pthread_cond_broadcast
//头文件 #include <pthread.h> // 只会唤醒一个 int pthread_cond_signal(pthread_cond_t *cond); /** *cond:条件变量 唤醒在等待这个条件发生的线程 *返回值: * 成功,返回0 * 失败,返回非0 */ // 广播 唤醒 所有等待的线程 int pthread_cond_broadcast(pthread_cond_t *cond);
本文作者:乐情在水静气同山
本文链接:https://www.cnblogs.com/aalynsah/p/17763036.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步