linux/C++ 进程线程

linux/C++ 进程线程

文章目录

  • linux/C++ 进程线程
    • 进程
      • 创建进程:
      • 跳转执行另一个程序
      • 僵尸进程
      • 命令与进程树
      • 孤儿进程
      • 进程间通信
        • 匿名管道(Pipe)
        • 有名管道(FIFO)
        • 共享内存
        • 消息队列
        • signal信号
          • 基本概念
          • 使用方法
          • 信号类型
          • 处理动作含义
          • 信号处理方法
          • 发送信号
      • 多进程和信号
      • 调用可执行程序
      • 进程终止
          • 5种正常终止进程的方法
          • 3种异常终止进程的方法
          • return和三种exit之间的区别
          • 进程的终止函数
    • 线程
      • 创建线程
      • 等待线程
      • 线程终止
      • 其他函数
      • Linux和C++两者的关联
        • 互斥锁
        • 读写锁
        • 自旋锁
          • 原子操作
        • 条件变量
        • 信号量
      • C++异步操作库

进程

进程是具有独⽴功能的程序在⼀个数据集合上运⾏的过程,是系统进⾏资源分配和调度的⼀个独⽴单位。

进程控制块(PCB):它是进程存在的唯⼀标识,系统通过PCB来描述进程的基本情况和运⾏状态,就进⽽控制和管理进程。其包括以下信息:

  1. 进程描述信息: 进程标识符、⽤户标识符

  2. 进程控制和管理信息: 进程当前状态,进程优先级

  3. 进程资源分配清单: 有关内存地址空间或虚拟地址空间的信息,所打开⽂件的列表和所使⽤的 I/O 设备信息。

  4. CPU相关信息: 当进程切换时,CPU寄存器的值都被保存在相应PCB中,以便CPU重新执⾏该进程时能从断点处继续执⾏。

创建进程:

pid_t fork(void);
/*
目的:
	创建一个子进程,会复制父进程的内存空间
返回值:
	父进程中返回子进程pid
	子进程中返回0
	失败返回-1
*/
pid_t getpid(void);
// 不会失败,必定返回当前进程的pid
pid_t getppid(void);
// 不会失败,必定返回当前进程父进程的pid
// 例:
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        // 创建进程失败
    } else if (pid == 0) {
        // 子进程代码
    } else {
        // 父进程代码
    }
    return 0;
}

跳转执行另一个程序

int execve (const char *__path, char *const __argv[], char *const __envp[]);
/*
目的:
	跳转执行另一个程序
path:
	需要执行程序的完整路径名
argv[]:
	指向字符串数组的指针 需要传入多个参数
    (1) 需要执行的程序命令(同path)
    (2) 执行程序需要传入的参数
    (3) 最后一个参数必须是NULL
envp[]:
	指向字符串数组的指针 需要传入多个环境变量参数
    (1) 环境变量参数 固定格式 key=value
    (2) 最后一个参数必须是NULL
返回值:
	成功不会返回,失败返回-1
*/
// 例:
int main() {
    char *name = "Ted";
    char *path = "test";
    char *args[] = {"/home/code_c++/test", name, NULL};
    char *envs[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games",NULL}
    int ret = execve(path, args, envs);
    if (ret == -1) {
        // 运行execve失败
    }
    return 0;
}

int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
/*
execl入参分别是需要执行的程序,需要执行的程序(第二遍),其他需要的参数,0(用于告知结束)
execv入参是需要执行的程序和一个char *数组,数组里面每一位存放需要执行的程序(只需要一次)或参数,最后一位存0
*/

僵尸进程

如果子进程比父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进程。

僵尸进程危害:

1. 内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。如果父进程没有处理子进程退出的信息,内核不会释放这个数据结构(危害较小)
1. 每个进程都有自己的进程编号,系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程(危害较大)

如何避免僵尸进程:

  1. 父进程忽略子进程退出时内核发送的信号SIGCHLD,即在代码里添加一行signal(SIGCHLD,SIG_IGN); (不推荐)

  2. 父进程通过wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将被阻塞

  3. 父进程捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()

pid_t wait(int *stat_loc);
/*
返回值是子进程编号
stat_loc也是出参,代表子进程的终止信息
	如果是正常终止,WIFEXITED(stat_loc)返回true,WEXITSTATUS(stat_loc)可获取终止状态
	如果是异常终止,WIFEXITED(stat_loc)返回false, WTERMSIG(stat_loc)可获取终止状态
*/
pid_t waitpid(pid_t pid, int *wstatus, int options);
/*
pid:
	等待模式
	(1) 小于-1 例如 -1 * pgid,则等待进程组ID等于pgid的所有进程终止
    (2) 等于-1 会等待任何子进程终止,并返回最先终止的那个子进程的进程ID -> 儿孙都算
    (3) 等于0 等待同一进程组中任何子进程终止(但不包括组领导进程) -> 只算儿子
    (4) 大于0 仅等待指定进程ID的子进程终止
wstatus: 
	整数指针,子进程返回的状态码会保存到该int
options: 
	选项的值是以下常量之一或多个的按位或(OR)运算的结果:
	(1) WNOHANG 如果没有子进程终止,也立即返回;用于查看子进程状态而非等待
    (2) WUNTRACED 收到子进程处于收到信号停止的状态,也返回。
    (3) WCONTINUED(自Linux 2.6.10起)如果通过发送SIGCONT信号恢复了一个已停止的子进程,则也返回。
返回值:
	成功等到子进程停止,返回pid
	没等到并且没有设置WNOHANG,一直等
	没等到设置WNOHANG,返回0
	出错返回-1
*/

// 方法1:
int main(int argc,char *argv[]) {
    signal(SIGCHLD,SIG_IGN) // 父进程不关心子进程是否退出,子进程退出后内核会释放数据结构和进程编号
    if (fork() > 0) {
        ...
    } else {
        ...
    }
}
// 方法2:
int main(int argc,char *argv[]) {
    if (fork() > 0) {
        int sts;
        pid_t pid = wait(&sts);
        if (WIFEXITED(sts)) {
            cout << "子进程" << pid << "正常终止,退出状态" << WEXITSTATUS(sts) << endl;
        } else {
            cout << "子进程" << pid << "异常终止,退出状态" << WTERMSIG(sts) << endl;
        }
    } else {
        ...
    }
}
// 方法3:
void func(int sig) {
    int sts;
    pid_t pid = wait(&sts);
    if (WIFEXITED(sts)) {
        cout << "子进程" << pid << "正常终止,退出状态" << WEXITSTATUS(sts) << endl;
    } else {
        cout << "子进程" << pid << "异常终止,退出状态" << WTERMSIG(sts) << endl;
    }
}
int main(int argc,char *argv[]) {
    signal(SIGCHLD,func);
    if (fork() > 0) {
        ...
    } else {
        ...
    }
}

命令与进程树

ps命令:

​ ps是"process status"的缩写,用于查看当前系统中进程状态的命令。它提供了有关正在运行的进程的各种信息,包括进程 ID(PID)、终端(TTY)、运行时间、命令名称等

​ -e:表示显示所有进程

​ -f:提供更完整的进程信息格式

ps -ef | grep 1234可以看到1234号进程和他的父进程以及子进程的信息

pstree命令:

会以树状图展示所有用户进程的依赖关系。

​ -p:显示进程号

孤儿进程

孤儿进程(Orphan Process)是指父进程已结束或终止,而子进程仍在运行的进程。

当父进程结束之前没有等待子进程结束,且父进程先于子进程结束时,那么子进程就会变成孤儿进程。

此时,子进程会被其祖先领养

进程间通信

匿名管道(Pipe)

匿名管道是位于内核的一块缓冲区,用于进程间通信。

int pipe(int pipefd[2]);
/*
目的:
	在内核空间创建管道,用于父子进程或者其他相关联的进程之间通过管道进行双向的数据传输
pipefd:
	用于返回指向管道两端的两个文件描述符
	pipefd[0]指向读端
	pipefd[1]指向写端
返回值:
	成功返回0,失败返回-1,且pipefd不会改变
*/
// 例:
int main(int argc, char const *argv[]) {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        // 创建管道失败
    }
    pid_t childPid = fork();
    if (childPid < 0) {
        // fork失败
    } else if (childPid == 0) {
        // 父进程
        close(pipefd[1]); // 关闭写端,只留读端
        char buf;
        while (read(pipefd[0], &buf, 1) > 0) {
            // 一直单个字节读取读端的数据,直到数据结束或出错
        }
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else {
        // 子进程
        close(pipefd[0]); // 关闭读端,只留写端
        char *buf = "lalala";
        write(pipefd[1], buf, strlen(buf));
        close(pipefd[1]);  // 写完后关闭写端
        waitpid(childPid, NULL, 0); // 等待子进程结束   
        exit(EXIT_SUCCESS);
    }
    return 0;
}

使用管道的限制:

  1. 两个进程通过一个管道只能实现单向通信,即一方只能写,另一方只能读。如果想实现双向通信就需要开另一个管道
  2. 管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。简而言之,只能在有父子关系的进程间使用。
有名管道(FIFO)

和匿名管道的共同点:同一条管道只应用于单向通信

和匿名管道的区别:有名管道可以用于任何进程之间的通信

int mkfifo(const char *pathname, mode_t mode);
/*
目的:
	创建有名管道FIFO的专用文件
pathname:
	有名管道绑定的文件路径
mode:
	有名管道绑定的文件权限
返回值:
	成功返回0,失败返回-1
*/
int unlink(const char *pathname);
/*
目的:
	从文件系统中清除一个名称及其链接的文件
pathname:
	文件路径
返回值:
	成功返回0,失败返回-1
*/
// 写端为例:
int main() {
    char *pipe_path = "/tmp/myfifo";
    // 创建有名管道,权限设置为 0664
    if (mkfifo(pipe_path, 0664) != 0) {
        // 创建管道失败
    }
    int fd = open(pipe_path, O_WRONLY);
    if (fd == -1) {
        // open失败处理
    }
    char write_buf[100];
    ssize_t read_num;
    // 从标准输入读取并写入到管道中
	while ((read_num = read(STDIN_FILENO, write_buf, 100)) > 0) {
        write(fd, write_buf, read_num);
    }
	close(fd);
    int ret = unlink(pipe_path); // 把有名管道创建的绑定文件清除掉
    if (ret == -1) {
        // unlink处理失败
    }
    return 0;
}
共享内存

前面提到的管道,是使用内核中的一处空间来进行进程通信。这就会有个问题:内核的空间是很宝贵的。所以管道能传输的消息字节也很少。

为了能传输更多的数据,可以使用共享内存。映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。通过对这⽚共享空间进⾏写/读操作实现进程之间的信息交换。

int shm_open(const char *name, int oflag, mode_t mode);
/*
目的:
	开启一块内存共享对象,我们可以像使用一般文件描述符一般使用这块内存对象
name:
	共享内存对象的名称,共享内存本身会保存在/dev/shm。
	名称必须是唯一的,必须是以正斜杠/开头,以\0结尾的字符串,中间可以包含若干字符,但不能有正斜杠
oflag:
	打开模式,用|拼接:
	O_CREAT:如果不存在则创建新的共享内存对象
	O_EXCL:当与O_CREAT一起使用时,如果共享内存对象已经存在,则返回错误(避免覆盖现有对象)
	O_RDONLY:以只读方式打开
	O_RDWR:以读写方式打开
	O_TRUNC 用于截断现有对象至0长度(只有在打开模式中包含 O_RDWR 时才有效)。
mode:
	当创建新共享内存对象时使用的权限位,类似于文件的权限模式,一般0644即可
返回值:
	成功返回共享内存对象的文件描述符,失败返回-1
*/
int shm_unlink(const char *name);
/*
目的:
	删除一个先前由 shm_open() 创建的命名共享内存对象
	并没有真正删除共享内存段本身,只是移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存
	当所有已打开该共享内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源
name:
	共享内存对象的名称
返回值:
	成功返回0,失败返回-1
*/
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
/*
目的:
	将指定文件扩展或截取到指定大小
	如果文件被缩小,截断部分的数据丢失,如果文件空间被放大,扩展的部分均为\0字符。
	缩放前后文件的偏移量不会更改
path:
	文件名/文件路径,不需要打开文件
fd:
	文件描述符,需要打开文件并且有写权限
length:
	指定长度,单位字节
返回值:
	成功返回0,失败返回-1
*/
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
目的:
	将文件映射到内存区域
	进程可以直接对内存区域进行读写操作,就像操作普通内存一样,但实际上是对文件或设备进行读写,从而实现高效的I/O操作
addr:
	指向期望映射的内存起始地址的指针,通常设为NULL,让系统选择合适的地址
length:
	要映射的内存区域的长度,单位字节
prot:
	内存映射区域的保护标志,可以是以下标志的组合
	PROT_READ: 允许读取映射区域
	PROT_WRITE: 允许写入映射区域
	PROT_EXEC: 允许执行映射区域
	PROT_NONE: 页面不可访问
flags:
	映射选项标志
	MAP_SHARED: 映射区域是共享的,对映射区域的修改会影响文件和其他映射到同一区域的进程(一般使用共享)
	MAP_PRIVATE: 映射区域是私有的,对映射区域的修改不会影响原始文件,对文件的修改会被暂时保存在一个私有副本中
	MAP_ANONYMOUS: 创建一个匿名映射,不与任何文件关联
	MAP_FIXED: 强制映射到指定的地址,如果不允许映射,将返回错误
fd: 
	文件描述符,用于指定要映射的文件或设备
	如果是匿名映射,则传入无效的文件描述符(例如-1)
offset:
	从文件开头的偏移量,映射开始的位置
返回值:
	成功返回映射区域的起始地址,失败返回(void *)-1,并设置errno
*/
int munmap(void *addr, size_t length);
/*
目的:
	取消之前通过 mmap() 函数建立的内存映射关系
addr:
	映射的内存区域的起始地址的指针
length:
	内存区域的大小,单位字节
返回值:
	成功返回0,失败返回-1
*/
// 以父子进程使用共享内存为例:
int main() {
    char shmName = { 0 };
    sprintf(shmName, "/letter%d", getpid()); // 使用getpid确保文件名唯一
    int fd = shm_open(shmName, O_CREAT | O_RDWR, 0644); // 创建一个共享内存对象
    if (fd < 0) {
        // 处理shm_open失败
    }
    ftruncate(fd, 100); // 将该区域扩充为100字节长度
    char *share = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // 以读写方式映射该区域到内存,并开启父子共享标签
    if (share == MAP_FAILED) {
        // 处理mmap失败,注意判断时不能和NULL判断,因为失败返回的是(void *)-1,用专有宏判断
    }
    close(fd); // 映射区建立完毕,关闭读取连接,注意这里不是删除
    pid_t childPid = fork();
    if (childPid < 0) {
        // 处理fork失败
    } else if (childPid == 0) {
        // 子进程写入数据
        strcpy(share, "你是个好人!\n");
    } else {
        // 父进程读取数据
        sleep(1);
        printf("老学员%d看到新学员%d回信的内容: %s", getpid(), pid, share);
        wait(NULL); // 等待子进程运行结束
        int ret = munmap(share, 100); // 释放映射关系
        if (ret == -1) {
            // 处理munmap失败
        }
    }
    shm_unlink(shmName); // 删除共享内存对象
    return 0;
}
消息队列

把进程交互的数据划分成一条条消息。用于从一个进程向另一个进程发送数据。但仅把数据发送到一个“队列”中,而不指定由哪个进程来接受。

消息队列独立于发送消息的进程和接收消息的进程。

typedef int mqd_t;
// 消息队列描述符,属于int别名
struct mq_attr {
    long mq_flags;   /* 对于mq_open()忽略 */
    long mq_maxmsg;  /* Max. # of messages on queue */
    long mq_msgsize; /* Max. message size (bytes) */
    long mq_curmsgs; /* 当前队列中的消息数量,对于mq_open,忽略它 */
	...
};
struct timespec {
    time_t tv_sec;        /* 秒 */
    long   tv_nsec;       /* 纳秒 */
};
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
mqd_t mq_open(const char *name, int oflag);
/*
目的:
	创建或打开一个已存在的POSIX消息队列
name:
	消息队列名称
	必须是以正斜杠/开头,以\0结尾的字符串,中间可以包含若干字符,但不能有正斜杠
oflag:
	指定消息队列的控制权限,必须也只能包含以下三者之一
        O_RDONLY:打开的消息队列只用于接收消息
        O_WRONLY:打开的消息队列只用于发送消息
        O_RDWR:打开的消息队列可以用于收发消息
	可以与以下选项中的0至多个或操作之后作为oflag
		O_CLOEXEC:设置close-on-exec标记,这个标记表示执行exec时关闭文件描述符
		O_CREAT:当文件描述符不存在时创建它,如果指定了这一标记,需要额外提供mode和attr参数
		O_EXCL:创建一个当前进程独占的消息队列,要同时指定O_CREAT,要求创建的消息队列不存在,否则将会失败,并提示错误EEXIST
		O_NONBLOCK:以非阻塞模式打开消息队列,如果设置了这个选项,在默认情况下收发消息发生阻塞时,会转而失败,并提示错误EAGAIN
mode:
	每个消息队列在mqueue文件系统对应一个文件,mode指定消息队列对应文件的权限
attr:
	属性信息,如果为NULL,则队列以默认属性创建
返回值:
	成功返回消息队列描述符,失败返回(mqd_t)-1,并设置errno
*/
int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout);
/*
目的:
	将msg_ptr指向的消息追加到消息队列描述符mqdes指向的消息队列的尾部,也就是往消息队列里写消息
mqdes:
	消息队列描述符
msg_ptr:
	消息的指针
msg_len:
	消息的长度,不能超过队列的mq_msgsize属性指定的队列最大容量
	长度为0的消息是被允许的
msg_prio:
	消息的优先级,非负整数
	消息队列中的数据是按照优先级降序排列的,如果新旧消息的优先级相同,则新的消息排在后面
abs_timeout:
	阻塞等待超时时间。
	如果消息队列已满,且abs_timeout指定的时间节点已过期,则调用立即返回
返回值:
	成功返回0,失败返回-1,并设置errno
*/
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout);
/*
目的:
	从消息队列中取走最早入队且权限最高的消息,把它放到msg_ptr里。
mqdes:
	消息队列描述符
msg_ptr:
	接收消息的缓存
msg_len:
	消息缓冲区的大小,必须大于等于mq_msgsize属性指定的队列单条消息最大字节数
msg_prio:
	如果不为NULL,则用于接收接收到的消息的优先级
abs_timeout:
	阻塞等待超时时间。
	如果消息队列中没有消息,且abs_timeout指定的时间节点已过期,则调用立即返回
返回值:
	成功返回接收到的消息字节数,失败返回-1,并设置errno
*/
int mq_unlink(const char *name);
/*
目的:
	清除name对应的消息队列,mqueue文件系统中的对应文件被立即清除。
	消息队列本身的清除必须等待所有指向该消息队列的描述符全部关闭之后才会发生。
name:
	消息队列名称
返回值:
	成功返回0,失败返回-1,并设置errno
*/
int clock_gettime(clockid_t clockid, struct timespec *tp);
/*
目的:
	获取clockid指定的时钟
clockid:
	特定时钟的标识符,常用的是CLOCK_REALTIME,表示当前真实时间的时钟
tp:
	接收时间信息的缓存
返回值:
	成功返回0,失败返回-1,并设置errno
*/
// 以父子进程间使用消息队列为例:
int main() {
    struct mq_attr attr;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 100;
	// 被忽略的消息 在创建消息队列的时候用不到
    attr.mq_flags = 0;
    attr.mq_curmsgs = 0;
	char *mq_name = "/father_son_mq";
    mqd_t mqdes =  mq_open(mq_name, O_RDWR | O_CREAT, 0664, &attr);
    if (mqdes == (mqd_t)-1) {
        // 处理mq_open失败
    }
    pid_t pid = fork();
    if (pid < 0) {
        // 处理fork失败
    } else if (pid == 0) {
        // 子进程等待接收消息队列中的信息
        char read_buf[100];
        struct timespec time_info;
		for (size_t i = 0; i < 10; i++) {
            memset(read_buf, 0, 100);
            clock_gettime(0, &time_info); // 设置接收数据的等待时间
            time_info.tv_sec += 15;
            int fd = mq_timedreceive(mqdes, read_buf, 100, NULL, &time_info;
            if (fd == -1) {
                // 处理读消息队列失败
            }
            printf("子进程接收到数据:%s\n",read_buf);
        }
    } else {
        // 父进程发送消息到消息队列中
        char send_buf[100];
        struct timespec time_info;
		for (size_t i = 0; i < 10; i++) {
            memset(send_buf, 0, 100);
            sprintf(send_buf, "父进程的第%d次发送消息\n", (int)(i+1));
            // 获取当前的具体时间
            clock_gettime(0, &time_info);
            time_info.tv_sec += 5;
			ssize_t bytecount = mq_timedsend(mqdes, send_buf, strlen(send_buf), 0, &time_info);
            if (bytecount == -1) {
                // 处理写消息队列失败
            }
        }
    }
    close(mqdes); // 不管是父进程还是子进程都需要释放消息队列的引用
    // 清除消息队列只需要执行一次
    if (pid > 0) {
        int ret = mq_unlink(mq_name);
        if (ret == -1) {
            // 处理mq_unlink失败
        }
    }
    return 0;
}

从技术上讲,单条消息队列可以用于双向通信,但是这会导致消息混乱,无法确定队列中的数据是本进程写入的还是读取的,因此不会这么做。

通常单条消息队列只用于单向通信。为了实现全双工通信,我们可以使用两条消息队列,分别负责两个方向的通信。

signal信号
基本概念

用于通知进程发生了事件,但不能给进程传递任何数据

使用方法

​ - kill -信号的类型 进程编号

​ - killall -信号的类型 进程名

信号类型
信号名 信号值 默认处理动作 发出信号的原因
/ 0 / 检测程序是否存活,如果没有返回说明信号存活,否则显示no process found
SIGHUP 1 A 终端挂起或者控制进程终止
SIGINT 2 A 键盘中断Ctrl+c
SIGKILL 9 AEF 采用kill -9 进程编号 强制杀死程序。
SIGSEGV 11 CEF 无效的内存引用(数组越界、操作空指针和野指针等)。
SIGALRM 14 A 由闹钟alarm()函数发出的信号。
SIGTERM 15 A 采用"kill 进程编号"或"killall 程序名"通知程序。
SIGCHLD 17 B 子进程结束信号

linux一共64个信号,上面是比较关键的几个

处理动作含义

A 缺省的动作是终止进程。

B 缺省的动作是忽略此信号,将该信号丢弃,不做处理。

C 缺省的动作是终止进程并进行内核映像转储(core dump)。

D 缺省的动作是停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。

E 信号不能被捕获。

F 信号不能被忽略。

信号处理方法
  1. 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。

  2. 设置信号的处理函数,收到信号后,由该函数来处理。

  3. 忽略某个信号,对该信号不做任何处理,就像未发生过一样。

#include 
sighandler_t signal(int signum, sighandler_t handler);
/*
目的:
	设置捕获特定信号对应的处理方式
signum:
	待捕获的信号值
handler:
	处理方式:
		SIG_IGN:忽略此信号
		自定义函数名:捕获信号后进入此函数,会传入信号值
		SIG_DEL:恢复此信号的默认行为
返回值:
	忽略
*/

// 用法:
#include 
void func(int signum) {
    cout << "捕获了信号: " << signum << endl;
    return;
}

void func1(int signum) {
    cout << "只改变第一次捕获此信号的行为,之后恢复原状" << endl;
    signal(signum, SIG_DEL);
}

int main() {
    signal(1, func); // 改变信号1的行为,从原本的挂起变成了进入func打印
    signal(SIGINT, func); // 改变了信号2的行为,同上
    signal(SIGALRM, func1); // 改变的第一次捕获信号14的行为,第一次进入func1打印,第二次及之后恢复原状
    signal(8, SIG_IGN); // 忽略信号8的行为
}
发送信号

linux命令行可以使用kill或killall,代码中使用kill

int kill(pid_t pid, int sig);
/*
目的:
	向其他进程发送信号
pid:
	三种情况:
	pid > 0:向指定进程编号 = pid的进程发送信号
	pid = 0:向当前进程组里的全部进程发送信号,通常用于父进程给所有子进程发送信号,需要注意的是发送着也会收到这个信号
	pid < 0:向所有进程广播信号,基本不用
sig:
	信号值
返回值:
	成功返回0,失败返回-1,并设置errno记录具体原因
*/

多进程和信号

在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出

void fathEXIT(int sig) {
    // 防止信号处理函数在执行的过程中再次被信号中断,忽略这两个信号
    signal(SIGTERM, SIG_IGN);
    signal(SIGINT, SIG_IGN);
    kill(0, SIGTERM); // 参照信号章节,pid = 0会向当前进程组里的全部进程发送信号
    // 释放全局资源
    exit(0);
}

void childExit(int sig) {
    // 防止信号处理函数在执行的过程中再次被信号中断,忽略这两个信号
    signal(SIGTERM, SIG_IGN);
    signal(SIGINT, SIG_IGN);
    // 释放子进程资源
    exit(0);
}

int main(int argc,char *argv[]) {
	signal(SIGTERM, fathEXIT); // kill和killall发送的信号,除了kill -9以外
    signal(SIGINT, fathEXIT); // ctrl + c中断
    if (fork() > 0) {
        ...
    } else {
        signal(SIGTERM, childExit); // 子进程退出函数和父进程的有所区分
        signal(SIGINT, SIG_IGN); // 子进程运行在后台,不需要捕获中断信号
    }
}

调用可执行程序

int system(const char * string);
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
/*
system和exec都能执行其他程序
    system入参是一个字符串,里面是需要执行的程序和参数,中间用空格隔开
    execl入参分别是需要执行的程序,需要执行的程序(第二遍),其他需要的参数,0(用于告知结束)
    execv入参是需要执行的程序和一个char *数组,数组里面每一位存放需要执行的程序(只需要一次)或参数,最后一位存0
两者最大的不同在于调用完其他程序后能否回到原程序继续往下调用
	system可以
	exec不行,exec调用的新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈,相当于把原程序直接覆盖了
*/
// 例:
int main(int argc,char *argv[])
{
    int ret = system("/bin/ls -lt /tmp");
    cout << "ret=" << ret << endl; // 可以执行到
    perror("system");
}
int main(int argc,char *argv[])
{
  int ret=execl("/bin/ls","/bin/ls","-lt","/tmp",0);
  cout << "ret=" << ret << endl; // 不会执行到
  perror("execl");
}
int main(int argc,char *argv[])
  char *args[10];
  args[0]="/bin/ls";
  args[1]="-lt";
  args[2]="/tmp";
  args[3]=0;

  int ret=execv("/bin/ls",args);
  cout << "ret=" << ret << endl; // 不会执行到
  perror("execv");
}

进程终止

8种方式终止进程

5种正常终止进程的方法
  1. 在main()函数用return返回

  2. 在任意函数中调用exit()函数

  3. 在任意函数中调用_exit()或_Exit()函数

  4. 最后一个线程从其启动例程(线程主函数)用return返回

  5. 在最后一个线程中调用pthread_exit()返回

3种异常终止进程的方法
  1. 调用abort()函数中止

  2. 接收到一个信号

  3. 最后一个线程对取消请求做出响应

return和三种exit之间的区别
/*
retun表示函数返回,会调用局部对象的析构函数,main函数中的return还会调用全局对象的析构函数
exit表示终止进程,只调用全局对象的析构函数,不调用局部对象的析构函数,会进行清理工作(例如把缓冲区数据写入磁盘,关闭文件等)并退出
_exit和_Exit直接退出,不进行任何清理工作
*/
// 例:
struct tmp {
    tmp(){};
    ~tmp(){ cout << "lalala" << endl; }
};
tmp t1;
int main(int argc,char *argv[]) {
    tmp t2;
    return 0; // 会打印t2销毁的lalala和t1销毁的lalala
    exit(0); // 只会打印t1销毁的lalala
    _exit(0); // 不会打印任何lalala
    Exit(0); // 不会打印任何lalala
}
进程的终止函数
int atexit(void (*function)(void));
/*
进程可以用atexit()函数登记终止函数(最多32个),这些函数将由exit()自动调用
exit()调用终止函数的顺序与登记时相反
*/

线程

线程是进程中的⼀个实体,是程序执⾏的最⼩单位。

同⼀个进程内多个线程之间可以共享代码段、数据段、打开的⽂件等资源,但每个线程各⾃都有⼀套独⽴的寄存器和栈。

共享资源意味着

  1. 线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多,所以多线程比多进程更适合高并发。

  2. 同一进程的多线程之间进行数据交换比进程间通信方便很多,但也由此带来线程同步问题。

下面介绍线程时会分别介绍linux中的线程和C++11的线程库,两者存在差异,都需要了解

创建线程

linux:

#include 
typedef unsigned long int pthread_t;
// pthread_t是unsigned long的别名

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);
/*
目的:
	创建一个线程
thread:
	指向线程标识符(即线程ID)的指针,用于保存成功创建的线程标识符
attr:
	设置线程的属性,不需要直接输入NULL
start_routine:
	函数指针,线程创建后开始执行的入口
	入参必须是void *,出参必须是void *
arg:
	函数的参数,可以是一个指向任意类型数据的指针
返回值:
	成功返回0,失败返回非0错误码
*/
// 例:
void *lalala(void *argv) {
    cout << "lalala" << endl;
}
pthread_t pid;
int ret = pthread_create(&pid, NULL, lalala, NULL);

C++:

#include 
// 三种构造方法

// 1.无参构造,创建出来的线程对象没有关联任何线程函数,也就是t1没有没有对应任何OS中实际的线程
thread t1;

/* 2.带可变参数的构造
fn:
	可调用对象,例如函数指针,仿函数,lambda表达式等
args:
	调用对象入参
*/
template 
explicit thread (Fn&& fn, Args&&... args);
// 例:
thread t2(func1, 1, 10);
thread t2([](string &s) {cout << s << endl;}, "lalala");

// 3.移动构造,用一个右值线程对象来构造一个线程对象
thread t3 = thread(func2, 90);
thread t4(move(thread(func3, 100)));

// 注意:thread类进制拷贝!不允许拷贝构造和拷贝赋值!
thread::id get_id() const noexcept;
// 获取当前线程的id,如果没有现成,则返回默认构造的thread::id
// 例:
thread(t1, foo);
thread_id t1ID = t1.get(id); // 会是一串数字
t1.join();
cout << t1.get_id() << endl; // 会打印thread::id

等待线程

linux:

int pthread_join(pthread_t thread, void **retval);
/*
目的:
	阻塞当前线程,直至某个线程终止。获得其返回值,并回收其资源
thread:
	要等待的线程ID
retval:
	如果非空,且pthread_exit有返回值,则接收线程结束后传递的返回值
返回值:
	成功返回0,失败返回非0错误码
*/
int pthread_detach(pthread_t thread);
/*
目的:
	不等待线程终止,但是通知操作系统,当此线程终止时回收其资源
thread:
	不等待的线程ID
返回值:
	成功返回0,失败返回非0错误码
*/

C++:

bool joinable() const noexcept;
/*
目的:
	检查当前线程是否活跃,也就是是否能被join或detach。
	一个thread有以下三种情况属于不能被join或detach的:
        1.thread刚被创建,并没有关联任何线程函数,即没有对应任何OS中实际的线程
        2.thread被移动走了(被其他thread调用移动构造函数或移动赋值运算符)
        3.thread已经被join或detach了
    这三种情况下thread对象都不会有自己的ID,所以joinable函数的实现方法就是当get_id() != thread::id()时返回true,否则返回false
*/
void join();
/*
目的:
	阻塞当前线程,直至调用join的线程终止运行
	在掉用前,需要确保joinable返回true,否则会导致未定义行为
*/
void detach();
/*
目的:
	把此线程从当前线程分割出去,允许它独立地持续执行。当此线程退出时将释放其分配的任何资源
	在掉用前,需要确保joinable返回true,否则会导致未定义行为
*/
// 例:
void foo() {
    thread::sleep_for(chrono::seconds(1));
}
thread t1(foo);
thread t2(foo);
t1.join(); // 阻塞直至foo运行完毕
t2.detach(); // 分割t2,不再关注
t1.join(); // 未定义行为
t2.join(); // 未定义行为

线程终止

linux:

void pthread_exit(void *retval);
/*
目的:
	终止调用线程
retval:
	返回给当前进程中调用了pthread_join的其它线程的数据
	指向的区域不能放在线程函数的栈中
*/
int pthread_setcancelstate(int state, int *oldstate);
/*
目的:
	设置本线程的取消状态
state:
	PTHREAD_CANCEL_ENABLE(默认):线程可以被取消
	PTHREAD_CANCEL_DISABLE:线程不可以取消,如果收到取消请求,会阻塞此线程直至取消类型修改为可以取消
oldstate:
	返回之前的取消状态
*/
int pthread_setcanceltype(int type, int *oldtype);
/*
目的:
	设置本线程的取消类型
type:
	PTHREAD_CANCEL_DEFERRED(默认):线程会继续运行,到下一个取消点时被取消
	PTHREAD_CANCEL_ASYNCHRONOUS:线程任何时刻都可能被取消(一般是立刻取消,但是操作系统不保证这一点)
oldtype:
	返回之前的取消类型
*/
void pthread_testcancel(void);
/*
目的:
	给当前线程设置一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求
*/
int pthread_cancel(pthread_t thread);
/*
目的:
	向线程发起取消信号,线程是否会被取消取决于它的取消状态和取消类型
thread:
	要取消的线程号
返回值:
	成功返回0,失败返回非0错误码
	取消操作和此函数是异步的,返回值只能代表此函数的取消请求是否发送成功,不能代表目标线程是否被取消
*/

pthreads标准定义了几个取消点:

  1. 通过pthread_testcancel调用以编程方式建立线程取消点
  2. 线程等待pthread_cond_wait或pthread_cond_timewait()中的特定条件
  3. 被sigwait(2)阻塞的函数
  4. 一些标准的库调用。通常,这些调用包括线程可基于阻塞的函数。如

根据POSIX标准,很多函数都属于取消点,例如connect(),accept(),read(),write(),open(),poll(),pthread_join(),sigwait(),sleep(),mkfifo(),perror(),printf(),scanf()等。具体可以查看manpage中的Cancelation points。不过可以肯定的是,基本上所有能阻塞当前进程的函数都属于取消点。

C++:

不需要,thread对象在运行完以后就会自动终止

其他函数

linux:

pthread_t pthread_self(void);
/*
目的:
	返回当前线程的ID
返回值:
	当前线程的ID
*/
struct sched_param {
   int sched_priority;     /* 调度优先级 */
};
int pthread_getschedparam(pthread_t thread, int *restrict policy, struct sched_param *restrict param);
/*
目的:
	返回thread的调度策略及优先级
thread:
	要检查的线程ID
policy:
	调度策略
param:
	优先级
返回值:
	成功返回0,失败返回非0错误码
*/
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param);
/*
目的:
	改变thread的调度策略及优先级
thread:
	要改变的线程ID
policy:
	SCHED_FIFO(先进先出):具有相同优先级的线程按照先来先服务的原则执行。一旦一个高优先级的线程准备好运行,它会一直执行直到它自己阻塞或者放弃CPU。
	SCHED_RR(时间片轮转):线程会按照优先级分配时间片,当时间片用完后,系统会将CPU分配给下一个同优先级的线程。
	SCHED_OTHER(默认):系统会根据线程的优先级和其他因素(如等待时间等)来动态分配CPU时间。
param:
	优先级
	SCHED_FIFO和SCHED_RR使用优先级,从低到高是1到99
	SCHED_OTHER不适用优先级,需要设置成0
返回值:
	成功返回0,失败返回非0错误码,并且不会修改优先级和策略
*/

C++:

void swap( std::thread& other ) noexcept;
// 交换两个thread对象的底层句柄
// 例:
thread t1(foo); // id 1
thread t2(foo); // id 2
t1.swap(t2);
cout << t1.get_id(); // id 2
cout << t2.get_id(); // id 1

void yield() noexcept;
// 一种线程让步的方法,它告诉操作系统当前线程愿意放弃剩余的时间片,以便其他线程有机会执行。当然,操作系统是否接受这个建议取决于具体的调度策略。
// 例:当尝试获取原子锁时,如果没有获取到就调用yield让出CPU,使持有锁的线程有机会运行,从而更快的释放锁
std::atomic lock(false);
void thread_func()
{
    while (lock.exchange(true, std::memory_order_acquire)) {
        std::this_thread::yield();
    }
    // 临界区
    lock.store(false, std::memory_order_release);
}

void sleep_for(const std::chrono::duration& sleep_duration);
void sleep_until(const std::chrono::time_point& sleep_time);
// 阻塞当前线程一段时间。两者的区别是sleep_for是睡眠一个时间段,sleep_until是睡眠到某一个时间点
// 相比POSIX中的sleep,这两者支持多种时间单位(秒、毫秒、微秒等),精度更高
// 例:
this_thread::sleep_for(chrono::seconds(2)); // 暂停当前线程2秒
auto next_time = chrono::system_clock::now() + chrono::seconds(2); // 获取当前时间并加上2秒
this_thread::sleep_until(next_time); // 暂停当前线程直到next_time

Linux和C++两者的关联

C++:

native_handle_type native_handle();
/*
目的:
	访问并操作线程的底层实现,进而利用操作系统提供的特定功能
	在POSIX中,返回的是一个pthread_t类型句柄
	这样就可以和pthread库进行互动了
*/
// 例:设置底层线程的调度策略为FIFO,优先级为10
thread t(foo);
pthread_t nativeHandle = t.native_handle();
sched_param para;
para.sched_priority = 10;
if (pthread_setschedparam(nativeHandle, SCHED_FIFO, ¶) != 0) {
    std::cerr << "Failed to set thread priority" << std::endl;
}

互斥锁

linux:

typedef union {
  struct __pthread_mutex_s __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;
typedef union
{
  char __size[__SIZEOF_PTHREAD_MUTEXATTR_T];
  int __align;
} pthread_mutexattr_t;
// pthread_mutex_t和pthread_mutexattr_t是两个联合体类型的别名
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
/*
目的:
	创建并初始化一个互斥锁
mutex:
	指向要被初始化的互斥锁
attr:
	锁的属性,如果想用默认的就填NULL
返回值:
	成功返回0,失败返回非0错误码
*/
/*
一共有两种初始化方式,一种是动态的,就是调用上面的pthread_mutex_init。而另一种是静态的,直接让一个锁 = PTHREAD_MUTEX_INITIALIZER
动态的需要调用销毁函数进行销毁,静态的不需要。但是动态的使用场景更多,例如锁是动态申请内存出来的,或者有一个锁数组,里面每个索引都要初始化
这些场景下只能用动态初始化,而不能静态初始化
*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 同pthread_mutex_init,用来销毁互斥锁
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
// 初始化和销毁互斥锁的属性
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
/*
目的:
	获取/设置属性中的互斥锁类型
attr:
	属性指针
type:
	PTHREAD_MUTEX_TIMED_NP(默认):一个线程加锁后,其余请求锁的线程形成等待队列。当锁被释放时按照队列顺序获取锁
	PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许一个现成多次获取同一个锁,并通过多次unlock解锁
	PTHREAD_MUTEX_ERRORCHECK_NP:当一个线程多次获取同一个锁时,报错而非死锁,其余和默认类型相同
	PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,当锁被释放时所有线程进行抢占,而非按排队顺序获取
*/
int pthread_mutex_lock(pthread_mutex_t *mutex);
/*
目的:
	尝试获取互斥锁
	如果没获取到(当前锁被别的线程持有),则会阻塞当前线程,直到锁变成可占有状态
	注:如果当前线程在持有锁的情况下再次尝试获取锁,不同的类型会有不同的行为:
		TIMED_NP,ADAPTIVE_NP:死锁
		RECURSIVE_NP:成功
		ERRORCHECK_NP:报错
返回值:
	成功返回0,失败返回非0错误码
*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/*
目的:
	尝试获取互斥锁,但是如果没有获取到,不阻塞当前线程,直接报错返回
	如果是RECURSIVE_NP,或者获取到了这个锁,则返回成功
返回值:
	成功返回0,失败返回非0错误码
*/
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*
目的:
	释放互斥锁
	注:如果没有线程持有锁的情况下进行释放,可能报错,也可能有未定义行为
返回值:
	成功返回0,失败返回非0错误码
*/
// 例:
pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t m2;
pthread_mutexattr_t attr;
int count = 0;

void *lalala(void *argv) {
    pthread_mutex_lock(&m2);
    cout << "thread " << pthread_self() << " lock" << endl;
    count += 1;
    sleep(1);
    cout << "thread " << pthread_self() << " unlock" << endl;
    pthread_mutex_unlock(&m2);
    return nullptr;
}

int main(int argc, char *argv[]) {
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE_NP);
    pthread_mutex_init(&m2, &attr);
    pthread_t pid1, pid2;
    pthread_create(&pid1, NULL, lalala, NULL);
    pthread_create(&pid2, NULL, lalala, NULL);
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    cout << count << endl;
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_destroy(&m2);
}

C++:

#include 
// mutex:
void lock();
void unlock();
bool try_lock();
// 行为和linux中的一致
// 例:
mutex a;
int count = 0;
void lalala() {
    a.lock()
    count += 1;
    a.unlock();
}

// timed_mutex:
// 相比mutex,增加了下面两个函数
bool try_lock_for(const chrono::duration& timeout_duration);
bool try_lock_until(const chrono::time_point& timeout_time);
// 这两个函数的含义是尝试获取锁,最多阻塞(至)传入的时间。如果成功获取到了锁返回true,否则返回false
// 所以这两个传入的时间是阻塞线程的时间,不是持有锁的时间,不能搞混

// recursive_mutex:
// 同linux种的RECURSIVE_NP,允许一个线程多次获取同一个锁,当相同数量的unlock被调用后,此线程会释放锁
// 成员函数和mutex一致

// recursive_timed_mutex:
// recursive_mutex和timed_mutex相结合,不再赘述

// lock_guard:
// mutex的包容器,提供RAII风格机制。当创建lock_guard对象时,它尝试获取互斥锁。当离开lock_guard对象的作用域时,会自动销毁lock_guard并释放锁
// 注:一旦创建,则在作用域结束前无法销毁,即无法释放锁
// 例:
mutex a;
int count = 0;
void lalala() {
    lock_guard lock(a);
    count += 1;
    // 退出后会自动销毁lock并释放a
}

// unique_lock:
// 相比lock_guard更加灵活,可手动控制加锁解锁,可以延迟加锁,以及转移所有权
// 构造方式1:
unique_lock ul; // 创建一个unique_lock对象,但不关联任何互斥量,处于未锁定状态
ul = unique_lock(a); // 后续可以关联互斥锁
// 构造方式2:
unique_lock ul(a); // 创建一个unique_lock对象并关联互斥锁
// 构造方式3:
unique_lock ul(a, defer_lock); // 创建一个unique_lock对象,但不锁定互斥锁
// 构造方式4:
unique_lock ul(a, try_lock); // 创建一个unique_lock对象,尝试锁定互斥锁,如果不能锁定也不阻塞,而是继续执行
if(ul.owns_lock()) ... else ...;
// 构造方式5:
unique_lock ul(a, adopt_lock); // 创建一个unique_lock对象,假定当前线程已经获取到互斥锁,把锁交给ul来管理
// 和timed_mutex一样,有lock,try_lock,unlock,try_lock_for,try_lock_until
bool owns_lock() const noexcept;
// 如果当前unique_lock持有互斥锁,返回true,否则返回false
void swap( unique_lock& other ) noexcept;
// 交换锁对象的内部状态,例如u1.swap(u2),会把u2里的mutex转给u1


void call_once(once_flag& flag, Callable&& f, Args&&... args);
// 确保只调用f一次,即使多线程中每个线程都调用了call_once,主要用于初始化共享资源
// 需要创建一个once_flag用于此函数,当调用call_once时,如果flag指明f已经被调用过了,call_once会立即返回
// 例:
once_flag flag;
int shared_resource = 0;
void init() {
    shared_resource = 24;
    cout << "init" << endl;
}
void thread_init() {
    call_once(flag, init);
}
int main() {
    thread t1(thread_init); // 会把shared_resource初始化为24并打印
    thread t2(thread_init); // 不会再调用init
    thread t3(thread_init); // 不会再调用init
    t1.join();
    t2.join();
    t3.join();
    return 0;
}


// C++17:scoped_lock:
// 相比unique_lock,scoped_lock一次可以锁定多个互斥锁
mutex a, b;
scoped_lock lock(a, b);

linux也有和call_once类似的函数:

pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
读写锁

需要提前明确的概念是:如果没有任何线程持有读写锁,则即可获取写锁又可获取读锁。如果有线程获取了读锁,那么可以继续获取读锁,但是不能获取写锁。如果有线程获取了写锁,那么读锁和写锁都不能获取。

扩展一下,读写锁能实现并发和互斥功能。需要并发的就去获取读锁,而需要互斥的就去获取写锁。

linux:

typedef union
{
  struct __pthread_rwlock_arch_t __data;
  char __size[__SIZEOF_PTHREAD_RWLOCK_T];
  long int __align;
} pthread_rwlock_t;

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 和互斥锁一样两种创建方法,函数是动态创建,initializer是静态创建,差别不再赘述
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 销毁函数
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
// 初始化和销毁读写锁属性的函数
int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t *restrict attr, int *restrict pref);
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
目的:
	获取/设置读写锁选择读写优先策略
attr:
	要获取/设置的读写锁属性结构体
pref:
	PTHREAD_RWLOCK_PREFER_READER_NP(默认):偏向读取,即如果有读锁请求时,写锁请求将被阻塞
	PTHREAD_RWLOCK_PREFER_WRITER_NP:期望效果是偏向写入,但是有bug,可能导致死锁,不要用这个宏
	PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:偏向写入,即如果有写锁请求时,读锁请求将被阻塞
返回值:
	成功返回0,失败返回非0错误码
*/
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime);
/*
目的都是获取读锁,rdlock当获取不到读锁时会阻塞,直到获取读锁。而tryrdlock则会立刻返回,不会阻塞。timedrdlock会等待abstime时间,如果还不能获取锁则报错返回。
*/
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime);
// 目的都是获取写锁,过程和上面读锁的一致,不再赘述
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 释放当前持有的读写锁,不管是读锁还是写锁

C++:

#include 
// C++17:
// shared_mutex:
// 用于实现读写锁机制,允许同时进行多个读操作或一个写操作
void lock();
bool try_lock();
void unlock();
// 获取独占锁,阻塞调用线程,直到获取锁成功。和之前mutex的函数没有区别
void lock_shared();
bool try_lock_shared();
void unlock_shared();
// 这块是shared_mutex和mutex所区分的地方。允许多个线程同时获取共享锁


// C++14:
// shared_timed_mutex:
/* 包含shared_mutex的全部成员函数,比shared_mutex多了超时功能,而且比shared_mutex更早一版本发布。
根据stackoverflow上的问题40207171解释,是一开始把shared_timed_mutex当做shared_mutex开发,但是当要发布C++14时,发现可以有不包含超时的更"轻量级"版本。但是这会已经来不及去实现了。所以在发布版本前把原定的"shared_mutex"改名成了“shared_timed_mutex”,并在下一个版本(C++17)实现了轻量级的shared_mutex
*/
bool try_lock_for(const chrono::duration& timeout_duration);
bool try_lock_until(const chrono::time_point& timeout_time);
bool try_lock_shared_for(const chrono::duration& timeout_duration);
bool try_lock_shared_until(const chrono::time_point& timeout_time);
// 这就是相比shared_mutex多出的四个超时函数,原理和之前的timed_mutex一样,不再赘述


// shared_lock:
// 共享锁,允许多个线程一起持有一个锁,和unique_lock这种独占锁正好相反
// 构造方式和unique_lock类似
shared_mutex a;
shared_lock ul;
ul = shared_lock(a);
shared_lock ul(a);
shared_lock ul(a, deferd_lock);
shared_lock ul(a, try_to_lock);
shared_lock ul(a, adopt_lock);
// 同样有try_lock_until,try_lock_for,和owns_lock
bool owns_lock() const noexcept;
bool try_lock_for(const chrono::duration& timeout_duration);
bool try_lock_until(const chrono::time_point& timeout_time);
// 以及unlock和swap
void unlock();
void swap(shared_lock& other) noexcept;

// 那么现在就可以通过shared_lock和unique_lock,对shared_mutex实现读写锁了,下面是例子:
shared_mutex m;
int count = 0;
void read() {
    shared_lock l1(m); // shared_lock和shared_mutex实现读锁,多个线程可以一起持有读锁
    cout << count << endl;
}
void write() {
    unique_lock l2(m); // unique_lock和shared_mutex实现写锁,只有一个线程可以持有写锁
    count += 1;
}
int main() {
    thread t1(write);
    thread t2(read);
    t1.join();
    t2.join();
    return 0;
}
自旋锁

linux:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
/*
目的:
	初始化一个自旋锁
lock:
	指向自旋锁的指针
pshared:
	PTHREAD_PROCESS_PRIVATE:只有和初始化自旋锁线程所在同一进程的现成可以使用此锁,其他进程的线程不能使用
	PTHREAD_PROCESS_SHARED:任何进程中的线程都可以使用此锁
返回值:
	成功返回0,失败返回非0错误码
*/
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 销毁自旋锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
/*
lock和之前的互斥锁以及读写锁不同,如果自旋锁当前已被占用,则会一直"自旋",即不断尝试获取此锁,而非阻塞当前线程
trylock以及unlock和之前的互斥锁以及读写锁没有区别
*/
// 例:
pthread_spinlock_t sl;
int count = 0;
void fun() {
    pthread_spin_lock(&sl); // 如果t1先进来,t2进来的时候t1还没有释放此锁,t2就会一直尝试获取锁,而非阻塞线程让出cpu
    count += 1;
    pthread_spin_unlock(&s1);
}
int main() {
    pthread_spin_init(&s1, PTHREAD_PROCESS_PRIVATE); // 自旋锁没有静态创建方式,只能通过init函数动态初始化
    pthread_t t1;
    pthread_t t2;
    pthread_create(&t1, NULL, fun, NULL);
    pthread_create(&t2, NULL, fun, NULL);
    ...
}

C++:

C++中并没有一个自旋锁的库,所以是没有关于自旋锁的实现的。但是我们可以用atomic库里的函数来实现自旋锁。在介绍如何实现之前,需要先了解下什么是原子操作。

原子操作

原子操作是一种不可分割的操作,它在执行过程中不会被其他线程或中断打断,确保操作的完整性和一致性。在多线程环境中,原子操作保证了数据的读取、修改或写入操作在一个单一的、不可中断的步骤中完成,避免了多个线程同时访问同一数据时可能产生的数据竞争问题。

在C++中,原子操作通过库atomic实现,下面列出原子操作和锁的区别:

原子操作
使用场景 适用于简单的数据操作,如对单个变量的读、写、修改 适用于保护复杂的代码块或数据结构
复杂性 对于复杂的操作(如复杂的计算或多个操作的组合),实现原子性非常困难甚至不可能 可以通过 lock_guardunique_lock 等方便地保护一段代码,更适合复杂的逻辑和数据结构
公平性和饥饿问题 不涉及线程的等待和唤醒,因此不存在公平性问题 可能存在公平性问题,如某些线程可能会长期等待锁,导致饥饿。但可以使用一些高级锁(如 timed_mutex)来缓解

下面介绍下atomic库中的类型:

**atomic**类型:

atomic
/*
主要模板类,允许创建原子对象,T可以是:
- 整数类型(如 int, unsigned int, long, unsigned long, long long, unsigned long long, short, unsigned short, char, unsigned char, signed char)
- 指针类型(如 T*,其中 T 可以是任何对象类型)
- 布尔类型bool
*/
// 同时每个类型都有对应的别名,以int为例, 下面两个是等价的:
atomic a;
atomic_int a;
  • 下面介绍下atomic类型的库函数:
// 默认构造函数,可以创建一个未初始化的原子对象
atomic a;
// 初始化构造函数
atomic a(24);
// 赋值运算符
atomic_int a = 24;

// 原子修改当前的值到desired,按照order的值影响内存
// 如果order是memory_order_consume,memory_order_acquire,或memory_order_acq_rel,则属于未定义行为
void store(T desired, memory_order order = memory_order_seq_cst) noexcept;
// 原子读取当前存放的值并返回,按照order的值影响内存
// 如果order是memory_order_release或memory_order_acq_rel,则属于未定义行为
T load(memory_order order = memory_order_seq_cst) const noexcept;
// 原子交换当前的值为desired,并返回原有的值。也就是原子操作读->改->返回
T exchange(T desired, memory_order order = memory_order_seq_cst) noexcept;
/*
这三个函数分别代表写,读,改
因为拷贝、赋值操作在atomic模板中被删除,所以模板也提供了更安全的函数实现方式来实现数据的存储(写)、加载(读)、交换(改)的操作
*/

// CAS(compare and swap)操作,如果当前值等于expected,那就用desired替换它,按照order的值影响内存
bool compare_exchange_weak(T& expected, T desired, memory_order order = memory_order_seq_cst) noexcept;
bool compare_exchange_strong(T& expected, T desired, memory_order order = memory_order_seq_cst) noexcept;
/*
weak和strong的区别:
weak可能存在"伪失败"的情况,即虽然对象值和expected一致,但是在更新至desired的过程中,行为被打断,导致返回false
strong不会存在"伪失败"的情况
那为啥要用weak不用strong呢?因为在一些循环算法中可以接受这种伪失败,并且weak比起strong有更高的性能。
*/

// 原子算法
T fetch_add(T arg, memory_order order = memory_order_seq_cst) noexcept;
T fetch_sub(T arg, memory_order order = memory_order_seq_cst) noexcept;
T fetch_and(T arg, memory_order order = memory_order_seq_cst) noexcept;
T fetch_or(T arg, memory_order order = memory_order_seq_cst) noexcept;
T fetch_xor(T arg, memory_order order = memory_order_seq_cst) noexcept;
// 原子把当前的值替换为和arg的加/减/与/或/异或操作,然后返回原有的值
// 这五个有对应的运算符
T operator+=( T arg ) noexcept;
T operator-=( T arg ) noexcept;
T operator&=( T arg ) noexcept;
T operator|=( T arg ) noexcept;
T operator^=( T arg ) noexcept;
// C++20又增加了max,min,自增和自减
T fetch_max(T arg, memory_order order = memory_order_seq_cst) noexcept;
T fetch_min(T arg, memory_order order = memory_order_seq_cst) noexcept;
T operator++() noexcept; // ++atomic, 等同于fetch_add(1) + 1
T operator--() noexcept; // --atomic, 等同于fetch_sub(1) - 1
T operator++(int) noexcept; // atomic++,等同于fetch_add(1)
T operator--(int) noexcept; // atomic--,等同于fetch_sub(1)
// max和min是原子的把当前值替换为原有值和arg通过max/min获取到的值,然后返回原有的值

// 原子等待
void wait(T old, memory_order order = memory_order::seq_cst) const noexcept;
void notify_one() noexcept;
void notify_all() noexcept;
/*
C++20引入的新特性,在调用wait时使用原子等待,也就是不断循环下面的操作:
	如果load读取出的当前值和old相等,则阻塞此进程,直至被notify_one或notify_all唤醒。
	如果load读取出的当前值和old不相等,直接返回。
notify_one会至少唤醒一个wait阻塞的线程(不是固定唤醒一个)
notify_all会唤醒所有wait阻塞的线程
*/
  • 特殊的库函数:
bool is_lock_free() const noexcept;
bool atomic_is_lock_free(const atomic* obj) noexcept;
static constexpr bool is_always_lock_free = /* implementation-defined */; // C++17
// 如果当前atomic类型底层实现没有用到锁,返回true,否则返回false

这块有些难以理解,之前不是说过,原子操作和锁不一样,甚至有个对比的表格吗,为什么这里又会有一个专门的函数来看是否用到了锁呢?

答案是最简单的解决原子操作问题的方案就是使用锁。底层硬件只支持一些特定类型的原子操作,例如布尔类型。而且不同硬件支持的还不一样,一些甚至一个都不支持。C++官方文档中的说法是,除了atomic_flag以外(这个下面会说到),所有其他原子类型都可能是通过互斥锁或者其他操作来实现的。

总体来讲这几个函数基本上没什么用,毕竟能用就行,不需要考虑底层实现。只有当一些特定的情况才需要查看(例如有锁则意味着线程可能阻塞,如果不希望有任何阻塞则需要考虑使用try_lock或者用后面的atomic_flag实现自旋锁)。

  • 变量memory_order:

上面很多的库函数中都有一个memory_order的参数,这个指定了内存访问(包括常规的非原子内存访问)如何围绕原子操作排序。我们从宽到严的讲解。

  1. memory_order_relaxed:

不对执行顺序做保证,编译器和处理器可以对各种语句进行重新排序,但是同一线程内对同一原子变量的访问不可以被重排

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

使用memory_order_relaxed只保证A,B,C,D每一个操作本身不会被重排,但是不保证会按照先A后B,或者先C后D的顺序执行。

所以一种可能得执行顺序是DABC,这样执行完之后r1 == r2 == 42。而另一种可能得执行顺序是ABCD,这样执行完之后r1 == 0, r2 == 42。

  1. memory_order_acquire和memory_order_release:

acquire保证本线程中,所有后续的读操作必须在本条原子操作完成后执行。即在acquire之后的所有load操作绝对不会重排到此acquire对应的操作之前。

release保证本线程中,所有之前的写操作完成后才能执行本条原子操作。即在release之前的所有store操作绝不会重排到此release对应的操作之后。

std::atomic x;
int i;

void producer() {
  int* p = new int(42); // 01
  i = 42; // 02
  x.store(p, std::memory_order_release);  // 1
}

void consumer() {
  int* q;
  while (!(q = x.load(std::memory_order_acquire))) { // 2
      continue;
  }
  assert(*q == 42); // 03
  assert(i == 42); // 04
}

int main() {
  std::thread t1(producer);
  std::thread t2(consumer);
  t1.join();
  t2.join();
}

由于1使用了release的写操作,所以可以确定只有在执行完01和02后才会执行1(但是不保证01和02的先后顺序)。

由于2对同一个原子变量进行了acquire的读操作,所以当2读到q不是空并退出循环后,说明1一定执行完毕了,否则x仍然会是空。

此时下面的两个assert一定不会出错。因为acquire和release的同步保证,在consumer中看到x的新值时,必然能看到producer中x.store之前的所有操作。01和1执行完毕意味着03必定是true,而02执行完毕意味着04必定为true。

  1. memory_order_acq_rel:

结合了acquire和release。

所有线程中,所有之前的写操作完成后才能执行本条原子操作,并且所有后续的读操作必须在本条原子操作完成后执行。

需要不同线程都是用同一个原子变量

  1. memory_order_seq_cst:

所有原子操作的默认选项。

如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义。

此外还附加一个单独的 total ordering,即所有线程对同一操作看到的顺序也是相同的。

这是最简单直观的顺序,但由于要求全局的线程同步,因此也是开销最大的。

**atomic_flag**类型:

atomic_flag() noexcept = default;
// atomic_flag 是一种原子布尔类型。和其他atomic类型不同,atomic_flag保证是lock-free(无锁)的。和atomic相比,atomic_flag不支持load或store
  • 下面介绍下它的类函数:
// 默认构造函数
atomic_flag f = ATOMIC_FLAG_INIT;
// 把atomic_flag里的值设成clear(false)
void clear(memory_order order = memory_order_seq_cst) noexcept;
/*
这里需要注意下,一般来讲atomic_flag有两种初始化方式。一种是前面提到的,静态初始化,令f = ATOMIC_FLAG_INIT,这个代表把f设为clear状态值
所以和它等同的第二种构造方法就是
atomic_flag f;
f.clear();
这个的好处是可以在类中正确的初始化atomic_flag,而坏处则是在调用clear之前f处于未定义状态,不能被使用
*/

// 原子的将atomic_flag设置成true并返回原本的值,也就是原子的读->改->写操作(RMW)
bool test_and_set(memory_order order = memory_order_seq_cst) noexcept;
// 原子的读出atomic_flag的值
bool test(memory_order order = memory_order_seq_cst) noexcept;

// 原子等待
void wait(bool old, memory_order order = memory_order::seq_cst) const noexcept;
void notify_one() noexcept;
void notify_all() noexcept;
/*
C++20引入的新特性,在调用wait时使用原子等待,也就是不断循环下面的操作:
	如果test读取出的当前值和old相等,则阻塞此进程,直至被notify_one或notify_all唤醒。
	如果test读取出的当前值和old不相等,直接返回。
notify_one会至少唤醒一个wait阻塞的线程(不是固定唤醒一个)
notify_all会唤醒所有wait阻塞的线程
*/

那么现在有了atomic_flag,我们终于可以自己来造一个自旋锁了

class spinlock {
private:
    atomic_flag f;
public:
    spinlock() {
        f.clear();
    }
    spinlock(const spinlock&) = delete;
    ~spinlock() = default;

    void lock() {
        while(f.test_and_set());
    }
    bool try_lock() {
        return !f.test_and_set();
    }
    void unlock() {
        f.clear();
    }
};

int count = 0;
spinlock sl;

void aaa() {
    lock_guard l1(sl);
    for (int i = 0; i < 100; i++) {
        count += 1;
    }
    cout << "aaa end, count " << count << endl;
}

int main() {
    thread t1(aaa);
    thread t2(aaa);
    t1.join();
    t2.join();
    cout << count << endl;
    return 0;
}

上面的程序保证了每次打印aaa end, count的时候,后面都会跟着100的倍数,最后count一定会是线程数*100

最后还剩下**atomic_ref**类型。在介绍此类型之前,需要先看下atomic类型的问题:

int main() {
	int val = 10086;
	int& ref = val;
	atomic atomicRef(ref);
	++atomicRef;
	cout << "val = " << val << " ref = " << ref << " atomicRef = " << atomicRef.load() << endl;
}

显然,通过把ref传递给一个atomic类型,我们希望在进行自增后,val,ref,和atomicRef都是10087

但是实际的结果是只有atomicRef是10087,val和ref都是10086

这里的问题在于我们以为是把ref传给atomicRef,其实是用atomic创建了一个副本,这个副本不会影响到原本的ref或者val

所以除非一开始就把val定义为atomic_int,后续把它传给一个atomic类型不会使其具有原子性。

那么如果一开始没有定义成atomic类型,后续又希望它有原子性应该怎么办呢?C++20引入的atomic_ref类型就解决了这个问题:

atomic_ref( T& obj );

atomic_ref允许对其引用的对象进行原子操作

所以如果上面的函数变成

int main() {
	int val = 10086;
	int& ref = val;
	atomic_ref atomicRef(ref);
	++atomicRef;
	cout << "val = " << val << " ref = " << ref << " atomicRef = " << atomicRef.load() << endl;
}

这样val,ref,和atomicRef就都会是10087了。

atomic_ref的类函数和atomic的完全一致,这里不再进行赘述

条件变量

linux:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
// 仍然是两种初始化方式
int pthread_cond_destroy(pthread_cond_t *cond);
// 销毁函数
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/*
目的:
	释放mutex并阻塞当前线程,直到被其他线程唤醒。被唤醒后会重新获取互斥锁
cond:
	条件变量指针,用于等待某个条件的发生
mutex:
	配合的互斥锁指针,在调用wait函数时此线程必须已经获取了这个互斥锁
返回值:
	成功时返回0,失败返回非0错误码
*/
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
/*
wait的超时版本,如果超过了abstime仍然没被唤醒,则会返回超时错误码ETIMEDOUT
*/
int pthread_cond_signal(pthread_cond_t *cond);
// 随机唤醒一个被cond阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒所有正在等待条件变量cond的线程
// 例:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int flag = 0;

void* thread_func(void* arg) {
    pthread_mutex_lock(&mutex);
    while (flag == 0) {
        pthread_cond_wait(&cond, &mutex);
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    sleep(2);
    pthread_mutex_lock(&mutex);
    flag = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex); // 注意这里的顺序,先通知再解锁
    pthread_join(tid, NULL);
    return 0;
}

C++:

#include 

一共有三种类型,condition_variable,condition_variable_any,cv_status。

condition_variable只能和mutex一起用

condition_variable_any可以和任何类型的锁一起用,用法以及类函数和condition_variable一样,不做赘述

cv_status为前两个类里面的wait函数使用,用来判断是否因等待超时而返回

下面看下类函数:

// 阻塞当前线程直至条件变量通知,或虚假唤醒发生。和linux一样,调用wait的前提是已经持有了这个mutex
void wait(unique_lock& lock);

template
void wait(unique_lock& lock, Predicate pred);
// wait的超时版本,wait_for是指定等待的时间,wait_until是指定等待到的时间
cv_status wait_for(unique_lock& lock, const chrono::duration& rel_time);
bool wait_for(unique_lock& lock, const chrono::duration& rel_time, Predicate pred);

cv_status wait_until(unique_lock& lock, const chrono::time_point& abs_time);
bool wait_until(unique_lock& lock, const chrono::time_point& abs_time, Predicate pred);

这里和linux有个不同点,就是虚假唤醒。虚假唤醒指的是因为操作系统的原因,导致在还没有满足wait条件(并且没有达到超时时间)的时候就返回。

下面两个语句是等价的:

wait(lock, pred);

while (!pred()) {
    wait(lock);
}

那对于wait_for和wait_until,怎么区分是超时返回还是虚假唤醒返回呢?答案是用cv_status这个枚举值,定义如下:

enum class cv_status {
    no_timeout,
    timeout  
};

如果返回的是cv_status::timeout,那么说明是超时返回。如果返回的是cv_status::no_timeout,那么说明要么是真正的唤醒,要么是虚假唤醒返回。这时就得检查pred是否为true进行判断了。

唤醒函数和linux的也基本上一样:

void notify_one() noexcept;
void notify_all() noexcept;

不过这里notify_one只会唤醒一个被阻塞的线程。

信号量

信号量本质就是一个容量为N的锁,跟一般的锁不一样,它可以放个多个线程访问临界资源,但是达到上限就不会让线程进入了,而让他们阻塞等待

linux:

在Linux中,根据是否具有唯一的名称,分为有名信号量和无名信号量。

无名信号量不是通过名称标识,而是直接通过sem_t结构的内存位置标识。无名信号量在使用前需要初始化,在不再需要时应该销毁。它们不需要像有名信号量那样进行创建和链接,因此设置起来更快,运行效率也更高,适合线程之间使用。

有名信号量在系统范围内是可见的,可以在任意进程之间进行通信。它们通过名字唯一标识,适合进程之间使用。

无名信号量相关函数:

int sem_init(sem_t *sem, int pshared, unsigned int value);
/*
目的:
	初始化sem指向的地址为一个无名信号量
pshared:
	0:信号量是线程间共享的,应该被置于所有线程均可见的地址
	非0:信号量是进程间共享的,应该被置于共享内存区域
value:
	信号量的初始值
返回值:
	成功返回0,失败返回-1,并设置errno
*/
int sem_destroy(sem_t *sem);
// 销毁sem指向的无名信号量

有名信号量相关函数:

sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
/*
目的:
	创建或打开一个已存在的POSIX有名信号量
name:
	信号量的名称,格式为/somename,是一个以斜线(/)打头,\0字符结尾的字符串
	打头的斜线之后可以有若干字符但不能再出现斜线,长度上限为251
oflag:
	标记位,控制调用函数的行为
	通常是O_CREAT:如果信号量不存在则创建,指定了这个标记,必须提供mode和value
mode:
	有名信号量在临时文件系统中对应文件的权限
value:
	信号量的初始值
返回值:
	成功返回创建的有名信号量的地址,失败返回SEM_FAILED,并设置errno
*/
int sem_close(sem_t *sem);
// 关闭对于sem指向的有名信号量的引用,每个打开了有名信号量的进程在结束时都应该关闭引用
int sem_unlink(const char *name);
// 移除内存中的有名信号量对象,/dev/shm下的有名信号量文件会被清除。当没有任何进程引用该对象时才会执行清除操作。只应该执行一次。

公共操作函数:

int sem_post(sem_t *sem);
// 将sem指向的信号量加1。如果信号量从0变为1,且其他进程或线程因信号量而阻塞,则阻塞的进程或线程会被唤醒并获取信号量,然后继续执行。
int sem_wait(sem_t *sem);
// 将sem指向的信号量减1。如果信号量的值大于0,函数执行减1后立即返回,线程继续执行。如果信号量的值是0,则阻塞直至信号量的值大于0,或信号处理函数打断当前调用。

无名信号量例子:

sem_t unnamed_sem;
int shard_num = 0;
void *plusOne(void *argv) {
    sem_wait(&unnamed_sem);
    int tmp = shard_num + 1;
    shard_num = tmp;
    sem_post(&unnamed_sem);
}
int main() {
    sem_init(&unnamed_sem, 0, 1);
    pthread_t tid[10000];
    for (int i = 0; i < 10000; i++) {
        pthread_create(tid + i, NULL, plusOne, NULL);
    }
    for (int i = 0; i < 10000; i++) {
        pthread_join(tid[i], NULL);
    }
    printf("shard_num is %d\n", shard_num);
    sem_destroy(&unnamed_sem);
    return 0;
}

有名信号量例子:

int main()
{
    char *sem_name = "/named_sem";
    char *shm_name = "/named_sem_shm";
    sem_t *sem = sem_open(sem_name, O_CREAT, 0666, 1);
    // 初始化内存共享对象
    int fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, sizeof(int));
    int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    *value = 0;
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
    }
    sem_wait(sem);
    int tmp = *value + 1;
    sleep(1);
    *value = tmp;
    sem_post(sem);
    // 每个进程都应该在使用完毕后关闭对信号量的连接
    sem_close(sem);
    if (pid > 0)
    {
        waitpid(pid, NULL, 0);
        // 有名信号量的取消链接只能执行一次
        sem_unlink(sem_name);
    }
    munmap(value, sizeof(int));
    close(fd);
    // 只有父进程应该释放内存共享对象
    if (pid > 0)
    {
        if (shm_unlink(shm_name) == -1)
        {
            perror("shm_unlink");
        }
    }
    return 0;
}

C++:

C++20引入了semaphore库,实现了两种信号量。其中counting_semaphore实现非负资源计数的信号量,binary_semaphore实现仅拥有二个状态的信号量。

两者的类函数都一样,下面只介绍counting_semaphore的:

counting_semaphore sm(2);
using binary_semaphore = std::counting_semaphore<1>;
// 构造函数,可以看到binary_semaphore就是counting_semaphore为1的别名
void acquire();
// 当信号量大于0时原子减一,否则阻塞当前线程直至信号量大于0并且可以正常减一的时候
bool try_acquire() noexcept;
// 当信号量大于0时原子减一并返回true,否则返回false
bool try_acquire_for(const chrono::duration& rel_time);
bool try_acquire_until(const chrono::time_point& abs_time);
// 增加阻塞超时时间
void release(ptrdiff_t update = 1);
// 原子的给信号量增加update的值,并唤醒所有被acquire阻塞的线程。要求update >= 0。
ptrdiff_t max();
// 返回内部计数器的最大可能值

例子:

counting_semaphore sm(0); //初始化信号量为0
void threadproc()
{
	sm.acquire();
	this_thread::sleep_for(chrono::microseconds(100));
	sm.release(1);
}
int main()
{
	thread t1(threadproc);
	sm.release(1);
	this_thread::sleep_for(chrono::milliseconds(200));
	sm.acquire();
	t1.join();
	return 0;
}

C++异步操作库

c++11中引入了future库来实现异步操作。

首先来看future类:

future;

提供了访问异步操作结果的返回值。通过async, packaged_task, promise创建的异步操作可以给创建者提供一个future对象。之后创建者可以查询/等待/从future中提取返回值。如果当前异步操作还没有返回值,那么上面的三种操作会阻塞当前线程。

这种机制也被称为共享状态。future关联了一个共享状态,会从中读取执行结果。而async, packaged_task和promise也关联了这个共享状态,会往里面写执行结果。所以如果只是单独创建一个future对象,它是没有共享状态的。于是有了下面这个类函数:

bool valid();
// 判断共享状态是否有效,有效返回true,无效返回false

那么知道了这个概念后,就有了获取结果的函数:

T get();
// 调用wait()等待,直至共享状态准备就绪,然后从共享状态中获取存储的值
// 调用后valid()会变成false。调用前valid()如果是false则属于未定义行为

上面说get会调用wait等待,wait的定义如下:

void wait() const;
// 阻塞当前线程,直至结果变得可用
// 调用后valid()会变成true。调用前valid()如果是false则属于未定义行为
future_status wait_for(const chrono::duration& timeout_duration) const;
future_status wait_until(const chrono::time_point& timeout_time) const;
// 有了阻塞超时选项的两个函数,future_status这个枚举定义如下:
enum class future_status {
    ready,
    timeout,
    deferred
};
/*
future_status::deferred:共享状态持有的函数正在延迟运行,可以理解为还未启动
future_status::ready:共享状态就绪
future_status::timeout:共享状态在经过指定的等待时间内仍未就绪
*/

以上就是future的类函数了。我们可以发现,future和共享状态是一对一的关系,那如果有多个线程想获取同一个共享状态怎么办呢?答案就是用shared_future类。它的类函数和future完全一致。future提供了一个share类函数用来把转移共享状态至shared_future对象。

// 例:
future fut = async(do_get_value);
shared_future shared_fut = fut.share();

看完了共享状态的接收端,我们来看发送端。根据cppreference,三个发送端大体的作用如下:

async 异步运行一个函数,并返回将保有它的结果的future
promise 存储一个值以进行异步获取
packaged_task 打包一个函数,存储其返回值以进行异步获取

先看**async**函数

async(F&& f, Args&&... args);
// 异步地运行函数f,并返回保有该函数调用结果的future。执行策略是launch::async | launch::deferred
async(launch policy, F&& f, Args&&... args);
// 执行执行策略。policy标志位会影响async实现方式,此枚举定义如下:
enum class launch : /* unspecified */ {
    async,
    deferred,
};
/*
launch::async:在不同的线程上执行此任务(可能会创建并启动一个新线程)
launch::deferred:惰性等待,不会立即启动,只有在接收端/调用方的future上调用非定时等待函数时才会执行
launch::async | launch::deferred:由实现选择一个策略执行(二选一执行)
*/

看一个例子:

int longTask(int x) {
    this_thread::sleep_for(chrono::seconds(3));
    return x * x;
}
 
int main() {
    future result = async(launch::async, longTask, 5);
    // 主线程可以继续执行其他任务
    cout << "Processing in main thread..." << endl;
    // 获取异步任务的结果(阻塞,直到任务完成)
    int value = result.get();
    cout << "Result from async task: " << value << endl;
    return 0;
}

然后来看下**promise**类:

promise;

promise提供一种设施用以存储一个值/指针/异常,之后通过promise对象所创建的future对象异步获得。promise应当使用一次。

future get_future();
// 返回与promise关联同一个共享状态的future对象
// 只能调用一次,多次调用会抛出异常
void set_value(R& value);
void set_value(R&& value);
void set_value(const R& value);
void set_value();
/*
1-3原子地存储value到共享状态,并使状态就绪
4只使状态就绪
*/

前三条都好理解,看到第四条可能有疑惑,你这个promise不就为了存储值的吗,怎么不存储直接使状态就绪了呢?这个其实类似于前面的async,只不过async需要函数返回一个具体的值,而promise可以用来看监控某一块内存信息。

例如我们有一个数组,经过一系列操作后(时间可能很长)会准备完毕,然后我们需要在主线程中获取这个数组里面的数据。我们可以创建一个promise对象,当数组准备完毕时,promise对象调用set_value()使状态就绪。主线程中对应的future获取到这个信息后就会直接去查看数组。虽然promise在这个例子里没有动态的绑定到这个数组上,但它的作用是通知数组准备完毕。

void set_value_at_thread_exit(R& value);
void set_value_at_thread_exit(R&& value);
void set_value_at_thread_exit(const R& value);
void set_value_at_thread_exit();
// 1-3原子地存储value到共享状态,而不立即令状态就绪。在当前线程退出时,销毁所有拥有线程局域存储期的对象后,再令状态就绪。
// 4在线程退出后再令状态就绪
void set_exception(exception_ptr p);
// 自动存储异常指针p到共享状态中,并令状态就绪。
void set_exception_at_thread_exit(exception_ptr p);
// 存储异常指针p到共享状态中,但不立即使状态就绪。在当前线程退出时,销毁所有拥有线程局域存储期的变量后,再令状态就绪。

看一个例子:

void compute(promise&& p) {
    // 假设这里有一个复杂的计算
    int result = 42; // 计算结果
    p.set_value(result);
}

int main() {
    promise p;
    future f = p.get_future();
    thread t(compute, move(p));
    int result = f.get(); // 在主线程中等待结果。这里会阻塞,直到compute函数设置了promise的值
    cout << "Result is: " << result <

最后看下**packaged_task**类:

packaged_task;

packaged_task是一个模板类,用于打包任务(如可调用对象、函数、lambda表达式、bind表达式或者其他函数对象),以便异步地执行它,并获取其结果。

bool valid() const noexcept;
// 检查任务对象是否拥有共享状态
future get_future();
// 返回与packaged_task关联同一个共享状态的future对象
void operator()(ArgTypes... args);
// 执行可调用对象,对象返回值或抛出的异常会被记录在共享状态,并令共享状态就绪
void make_ready_at_thread_exit(ArgTypes... args);
// 执行可调用对象,对象返回值或抛出的异常会被记录在共享状态。只有在此线程结束并且所有线程局部对象被销毁后,共享状态才会就绪
void reset();
// 重置状态,抛弃先前执行的结果。构造新的共享状态。

看个例子:

int f(int x, int y) { return pow(x,y); }
void task_lambda()
{
    packaged_task task([](int a, int b)
    {
        return std::pow(a, b); 
    });
    future result = task.get_future();
    task(2, 9);
    std::cout << "task_lambda: " << result.get() << endl;
}
 
void task_bind()
{
    packaged_task task(bind(f, 2, 11));
    future result = task.get_future();
    task();
    cout << "task_bind: " << result.get() << endl;
}
 
void task_thread()
{
    packaged_task task(f);
    future result = task.get_future();
    thread task_td(move(task), 2, 10);
    task_td.join();
    cout << "task_thread: " << result.get() << endl;
}
 
int main()
{
    task_lambda();
    task_bind();
    task_thread();
}

总而言之,future就像一个点菜单。当我们去餐厅吃饭时,点完菜后餐厅会给一个点菜单,指明我们点了哪些菜。但是具体的菜需要厨师做出来后才能端过来。

future也一样。当我们有一个绑定了共享状态的future对象时,它也指明了之后会有一个可以获取的"状态/数据"。但是具体的值需要另一端运行完成后才会写入到共享状态里,然后future才可以获取。

end

你可能感兴趣的:(linux,c++,运维)