并发

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. 进程是程序的一次执行活动 ; 一个程序可以对应多个进程
  3. 进程是一个独立的活动单位 进程是竞争系统资源(进程地址空间)的基本单位

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

进程退出的状态:

  1. 自杀

    • main函数退出

    • 调用exit / _exit

      //头文件
             #include <stdlib.h>
      //声明
             void exit(int status);
      //头文件
             #include <unistd.h>
      //声明
             void _exit(int status);
      /**
      status:退出码  退出状态  由程序员 自己指定含义
      exit:正常退出 -- 做一些清理工作 (将缓冲区数据同步...)  
      _exit:快速退出 -- 不做清理工作 
      */
      
  2. 他杀

    被 操作系统 杀死

    等待子进程状态改变: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 无名管道的特点

  1. pipe 有两端 一端用来读 一端用来写
  2. 按顺序读取 不能lseek
  3. 内容读完了就没有了,水流
  4. pipe 无名管道随内核持续
  5. pipe只能在亲缘(类似于父子)进程之间进行通信
  6. 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信号的处理

进程收到信号,处理方式有三种:

  1. 捕获。把一个信号 与 用户自定义的信号处理函数 关联起来。当收到这个信号的时候,就会自动调用 自定义的 信号处理函数

  2. 默认行为。收到信号时,采用操作系统默认的行为

  3. 忽略。ignore 收到信号相当于没有收到

2.2.2信号相关API

  1. 发送信号

    //头文件
    #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被设置
    */
    
  2. raise:发送信号给自己

    //头文件
    #include <signal.h>
    //声明
    int raise(int sig);
    
  3. alarm:设置一个发送信号的闹钟

    //头文件
    #include <unistd.h>
    //声明
    unsigned int alarm(unsigned int seconds);
    /**
    seconds:多少秒之后 发送一个闹钟信号
    返回值:
        返回上一个闹钟的剩余秒数
    */
    
    • 每个进程有且仅有一个闹钟
  4. 捕获信号 signal

    //头文件
    #include <signal.h>
    
    // 函数指针类型  指向的函数类型为 void (int)
    typedef void (*sighandler_t)(int); 
    //声明
    sighandler_t signal(int signum, sighandler_t handler);
    /**
    signum:你要捕获的信号
    handler:信号处理函数
    返回值:
        成功,返回 修改前的信号处理函数
        失败,返回SIG_ERR errno被设置
    */
    
  5. 等待信号pause

    //让进程/线程休眠直到有信号来临
    //头文件
    #include <unistd.h>
    //声明
    int pause(void);
    /**
    返回值:
        成功,返回信号值
        失败,返回-1, errno == EINTR
    */
    

2.3共享内存

进程间通信方式之一,多个进程可以共享一段内存 “共享内存”

这段内存 存在于 内核中,多个进程都可以访问,但是这种方式相较于其他 IPC 内存拷贝会少一次。

实现方式

在内核中开辟块内存,其他进程通过 映射的方式 获取这段共享内存的首地址

进程1可以映射这个内存,同时进程2也可以映射这块内存 ...

进程1 往共享内存中输入数据,相当于就是 往进程2的进程地址空间中写入数据。

2.3.1 System V 共享内存

共享内存操作流程

  1. ftok 创建/获取 System V IPC对象 的 key
  2. 创建一个共享内存
  3. 映射
  4. 读写 System V IPC 的 API 接口
  5. 解除映射关系 和 关闭 共享内存

生成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信号量如何实现

  1. 创建一个信号量
  2. 等待一个信号量(p操作)
  3. 释放一个信号量(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.线程

为了程序 能够实现并发,引入了进程的概念:存在某些问题

  1. 创建一个进程的开销比较大, 申请一个新的进程地址空间
  2. 进程间通信需要第三方:文件 内核 云端

于是,有人提出能不能再一个进程内部实现 并发? ---- 线程/LWP 轻量级进程

3.1线程

线程是比进程更小的活动单位,它是进程中的一个执行分支,线程也是并发的一种情形。进程内部可以有多个线程,他们之间并发执行,且线程之间共享整个进程的资源。

进程是系统资源分配的最小单位,线程是任务调度的最小单位

线程的特点:

  1. 创建一个线程的开销比较小
  2. 实现线程间通信比较方便
  3. 线程也是一个动态概念,线程本体是需要 被调度 (状态切换)
  4. 线程是任务调度的最小单位。

共享资源:

  1. 内存地址空间:同一进程中的多个线程共享相同的内存地址空间。这意味着它们可以访问相同的内存区域,例如全局变量和共享数据。
  2. 全局变量和共享数据:在进程范围内定义的变量和数据结构可以被多个线程访问和修改。
  3. 文件描述符:线程可以共享打开的文件描述符,以便它们可以访问相同的文件和 I/O 资源。
  4. 信号处理器:线程可以共享信号处理器,以便它们可以处理相同的信号。
  5. 当前目录和环境变量:线程在创建时继承了其父进程的当前目录和环境变量。
  6. 用户 ID 和组 ID:线程继承了其父进程的用户 ID 和组 ID,这有助于确定线程对系统资源的访问权限。

独立资源:

  1. 栈:每个线程都有自己的栈,用于存储局部变量、函数调用和返回地址等信息。线程之间不能直接访问彼此的栈。
  2. 线程 ID:每个线程都有唯一的线程 ID,用于在系统中标识和区分不同的线程。
  3. 进程 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);     
posted @ 2023-10-13 20:04  乐情在水静气同山  阅读(5)  评论(0编辑  收藏  举报