文章目录

  • Linux多进程多线程编程
    • 一、多进程编程
      • 1、fork 函数
      • 2、exec 系列函数
      • 3、wait、waitpid 函数
      • 4、pipe 管道
      • 5、信号量
        • 5.1、semget
        • 5.2、semop
        • 5.3、semctl
        • 5.4、信号量同步父子进程
        • 5.5、信号量同步任意两个进程
        • 5.6、strace 查看进程运行状态
        • 5.7、ipcs 查看IPC信息
      • 6、POSIX信号量
        • 6.1、命令名信号量
          • 1、sem_open
          • 2、sem_close
          • 3、sem_unlink
          • 4、sem_wait
          • 5、sem_post
          • 6、POSIX命名信号量编程案列
        • 6.2、匿名信号量
          • 1、sem_init
          • 2、sem_destroy
          • 3、POSIX匿名信号编程案列
      • 7、共享内存
        • 7.1、shmget
        • 7.2、shmat 和 shmdt
        • 7.3、shmctl
        • 7.4、共享内存编程案列
      • 8、消息队列
    • 二、多线程编程
      • 1、NPTL线程库
      • 2、编译参数
      • 3、创建线程
      • 4、结束线程
        • 4.1、return
        • 4.2、pthread_exit
        • 4.3、pthread_join
        • 4.4、结束线程综合案列
        • 4.5、pthread_cancel
      • 5、互斥锁
        • 5.1 互斥锁API
        • 5.2 互斥锁案列
        • 5.3 互斥锁属性
        • 5.4 死锁案列
      • 6、读写锁
        • 6.1、读写锁原理介绍
        • 6.2、读写锁API
        • 6.3、原语读写锁和超时读写锁
        • 6.4、读写锁编程案列
      • 7、自旋锁
        • 7.1、自旋锁简介
        • 7.2、自旋锁API
      • 8、条件变量
      • 9、障碍
        • 9.1 障碍简介
        • 9.2 障碍API
        • 9.3 障碍编程案列
      • 10、多线程环境注意事件

Linux多进程多线程编程

一、多进程编程

1、fork 函数

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
// 返回值:成功情况下述;失败返回-1并设置相应的errno
  • fork创建的新进程称为子进程,此函数调用一次,但返回两次。 返回值的唯一区别是,子进程中的返回值是0,而父进程中的返回值是新子进程的进程ID。
  • 子进程ID返回给父进程的原因是,一个进程可以有多个子进程,并且没有函数允许进程获取其子进程ID。
  • fork返回0给子进程的原因是一个进程只能有一个父进程,并且子进程总是可以调用getppid来获取父进程的ID。 (进程ID 0是保留给内核使用的,所以0不可能是子进程ID。)

2、exec 系列函数

fork函数的一个用途是创建一个新进程(子程序),然后通过调用其中一个exec函数来执行另一个程序。当进程调用其中一个exec函数时,该进程将完全被新程序取代,新程序将开始在其主函数处执行。进程ID在执行过程中不会改变,因为没有创建新的进程;Exec只是用一个来自磁盘的全新程序替换当前进程(它的文本、数据、堆和堆栈段)。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execlp(const char *file, const char *arg0, ... /* (char *)0 */ );
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execv(const char *pathname, char *const argv[]);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// All seven return: −1 on error, no return on success
# 参数说明
pathname 指定可知行文件的完整路径,
file 中包含了1个或以上的'/',效果等同于pathname,否则在环境变量PATH中搜索
argv 接受参数数组,传递给被打开的新程序的main函数
envp参数用于设置新程序的环境变量,如果没设置,则使用由全局变量environ指定的环境变量

3、wait、waitpid 函数

这两个函数主要用于处理僵尸进程

僵尸进程介绍:多进程中,父进程一般需要跟踪子进程退出状态,因此当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程对子进程退出信息的查询,所以会造成两种形态的僵尸进程

  • 1、子进程结束运行之后,父进程读取该子进程退出状态之前,这个子进程处于僵尸状态
  • 2、父进程结束或者异常终止,而子进程继续运行,此时子进程的PPID将被操作系统设置为1,即init进程,init进程接管了它,等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸状态

僵尸进程会占据系统资源,这是高性能中不被允许的,所以用wait、waitpid 函数处理

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// Both return: process ID if OK, 0 (see later), or −1 on error

这两个函数的区别如下:

  • 1、wait函数可以阻塞调用者直到子进程终止,而waitpid有一个选项可以阻止它阻塞。
  • 2、waitpid函数不会等待第一个终止的子进程;它有许多选项来控制等待哪个进程

参数statloc是一个指向整数的指针。如果该参数不是空指针,则终止进程的终止状态存储在该参数所指向的位置中。如果我们不关心终止状态,只需传递一个空指针作为这个参数。
下面几个宏帮助解释子进程的退出状态信息

描述
WIFEXITED(status) 如果子进程正常终止,返回则为true。在这种情况下,我们可以执行WEXITSTATUS(status)获取子进程传递给exit、_exit或_exit的参数的低8位,也就是返回子进程的退出码
WIFSIGNALED(status) 如果一个异常终止的子进程的状态被返回,通过接收到它没有捕捉到的信号,返回非0值。在这种情况下,我们可以执行WTERMSIG(状态)获取导致终止的信号值
WIFSTOPPED(status) 如果子进程意外终止,返回一个非0值,此时可调用WSTOPSIG(status) 获取导致子进程停止的信号值
WIFCONTINUED(status) 如果在任务控制停止后继续执行子任务,返回状态为true (XSI中;只有waitpid)。

wait的阻塞特性不适合服务程序,用waitpid解决这个问题。

waitpid只等待由pid参数指定的子进程

  • 如果pid为-1,则和wait效果相同,等待任意一个子进程终止
  • statloc参数和wait相同
  • options参数可以控制waitpid函数行为,最常用的值是WNOHANG,当options是WNOHANG时,waitpit调用将是非阻塞的。如果pid指定子进程还没有结束或意外终止,waitpid立即返回0;如果目标子进程正常退出了,waitpid返回该子进程的PID。

对于waitpid函数而言,最好在某个子进程退出之后再调用它。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号,父进程可以捕获SIGCHLD信号得知某个子进程退出,并在信号处理函数调用waitpid函数以“彻底结束”一个子进程。以下是处理代码

static void handle_child(int sig)
{pid_t pid;int stat;while (pid = waitpid(-1, &stat, WNOHANG) > 0){// 对结束的子进程进行善后处理}
}

4、pipe 管道

管道是父进程和子进程间常用的通信手段,管道传递通信是单方向的,所以是半双工。父子进程间必须有一个关闭fd[0],另外一个进程关闭fd[1]
FIFO命名管道可以全双工,但是在网络编程中使用得不多

基本使用方法:

int main() {int num = 0;int fd[2];pid_t pid;char data[20] = {};if (pipe(fd) < 0){printf("pipe error");}if ((pid = fork()) < 0){printf("fork error");}else if (pid > 0) // parent{printf("\n\n\nI am parent\n");close(fd[0]);write(fd[1], "hello world\n", 12);}else              // child{printf("\n\n\nI am child\n");close(fd[1]);num = read(fd[0], data, 20);write(STDOUT_FILENO, data, num);}return 0;
}

5、信号量

信号量优劣:

  • 1、信号量可以使线程进入休眠状态,会切换线程,信号量的开销要比自旋锁大,适用于占用资源比较久的场合。
  • 2、信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  • 3、如果共享资源的持有时间较短,不适合使用信号量,因为频繁的休眠、切换线程造成很大开销。

(2)如果共享资源的持有时间较短,不适合使用信号量,因为频繁的休眠、切换线程造成很大开销。

当多个进程访问某个资源时,比如同时写入一个文件时,就需要考虑同步问题,确保在同一时刻只有一个进程对该资源独占式访问。

信号量是一种特殊的变量, 信号量有两种操作,常用P、V两个字母表示(荷兰语单词首字母),P为传递,V为释放,用S表示一个信号量

  • P : 如果S的值大于0,就将S减1; 如果S的值为0,说明目标资源被占用中,则挂起当前进程
  • V : 如果有其他进程因为操作S后被挂起,就唤醒那个进程;如果没有进程被挂起,就将S的值加1

下面操作一段关键代码举例(只是最简单的情况,实际上操作会复杂一些),假设已经创建了信号量设置值为1(这里将信号量理解为跨进程的全局变量),有进程A和进程B,进程A先访问关键代码段,穿过了P位置,进行了P操作,S值减1为0,如果进程A还没有走到V这个位置时,而进程B又走到了P点,因为S值为0,所以进程B会被挂起,直到进程A执行了V操作,B才会被唤醒继续访问关键代码段



Linux的信号量主要由3个函数操作:semget、semop、semctl

5.1、semget

semget创建一个新的信号量集,或者获得一个已有的信号量集

#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
// Returns: semaphore ID if OK, −1 on error
  • key:标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的key值来创建/获取该信号量
  • nsems: 指定要创建/获取的信号量集中信号量的数目。如果是创建新的信号量,该值必须指定;如果获取已经存在的信号量,则可以把它设置为0
  • semflg : 指定一组标志,它低位的9个比特是该信号量的权限,它用来设置所有者、用户组、其他用户的权限。此外,它还可以和IPC_CREAT标志做按位“或”运算来创建新的信号量集。 还可以联合使用IPC_CREAT和IPC_EXCL标志确保创建一组新的唯一的信号量集。
    IPC_CREAT | IPC_EXCL和open函数的O_CREAT | O_EXCL作用相似,若不存在该信号量,就创建一个新的;若信号量集已存在,semget()返回错误设置errno为EEXIST

如果key的值为IPC_PRIVATE,或者没有现有的信号量集与key相关联,并且在semflg中指定了IPC_CREAT,则会创建一组新的nsems信号量

当创建新的信号量集后,与之关联的内核数据结构体semid_ds 将被初始化

sem_otime设置为0
sem_ctime设置为当前时间
sem_nsems设置为nsems
struct semid_ds {struct ipc_perm sem_perm; /* see below ipc_perm */unsigned short sem_nsems; /* # of semaphores in set */time_t sem_otime;         /* last-semop() time */time_t sem_ctime;         /* last-change time */...
};

IPC与每个IPC结构关联一个ipc_perm结构体,这个结构定义了权限和所有者,至少包括以下成员:

struct ipc_perm {uid_t uid;   /* owner’s effective user ID */gid_t gid;   /* owner’s effective group ID */uid_t cuid;  /* creator’s effective user ID */gid_t cgid;  /* creator’s effective group ID */mode_t mode; /* access modes */...
};

在创建IPC结构时初始化所有字段; 之后,我们可以通过调用msgctl、semctl或shmctl来修改uid、gid和mode字段。 要更改这些值,调用进程必须是IPC结构的创建者或超级用户; 更改这些字段类似于为文件调用chown或chmod

5.2、semop

semop函数改变信号量的值,即执行上面提及的P、V操作

信号量集中的每个信号量都有以下内核关联变量:

unsigned short  semval;   /* semaphore value */
unsigned short  semzcnt;  /* # waiting for zero */
unsigned short  semncnt;  /* # waiting for increase */
pid_t           sempid;   /* PID of process that last */

semop对信号量操作实际就是对以上的内核变量进行操作。

#include <sys/sem.h>
int semop(int semid, struct *sembuf sops, size_t nsops);
// Returns: 0 if OK, −1 on error
  • semid :由semget函数返回的信号量集ID,类似于操作句柄
  • sops:指向一个sembuf结构体类型的数组,该结构体下面会描述
  • nsops:指定要执行的操作个数,即sops数组中元素的个数。semop对数组sops中的每个成员按数组顺序依次操作,操作过程是原子操作
  • 返回值: 0 if OK, −1 on error,失败时sops指定一切都不会被执行
struct sembuf {unsigned short sem_num; /* member # in set (0, 1, ..., nsems-1) */short sem_op;           /* operation (negative, 0, or positive) */short sem_flg;          /* IPC_NOWAIT, SEM_UNDO */
};

1、sem_num表示信号集中信号量的编号,0表示信号集中的第一个信号量
2、sem_op指定操作类型负数、0、正数。而每种类型的操作又受到sem_flg的成员的影响
3、sem_op和sem_flg按照如下方式影响semop的操作

  • sem_op > 0 : 则semop将该信号量的值(semval)增加sem_op,调用进程对该信号量需要拥有写权限
  • sem_op = 0 : 则表示调用进程希望等待信号量的值变为0,调用进程对该信号量需要拥有读权限。
    如果此时信号量的值是0,则立即成功返回。
    如果此时信号量的值非0,情况如下:

    • 如果指定了IPC_NOWAIT,semop返回一个EAGAIN错误
    • 如果没有指定IPC_NOWAIT,这个信号量的semzcnt值就会增加1(因为调用者即将进入睡眠状态),并且调用进程将被挂起,直到发生以下三种情况之一:
      • 信号量的值(semval)变为0,此时系统将该信号量的semzcnt值减1(因为调用进程已经完成了等待)
      • 信号量从系统中移除,在这种情况下,semop函数返回一个EIDRM错误
      • 调用被捕获的信号中断,此时信号量的semzcnt值减1(因为调用进程不再等待),并且semop函数调用失败返回一个EINTR错误
  • sem_op < 0 :表示对信号量进行减操作,即希望获得该信号量控制的资源。
    • 如果信号量的值(semval)大于或等于sem_op的绝对值(资源可用),则从信号量的值(semval)中减去sem_op的绝对值,semop操作成功,调用进程立即获得信号量。 如果指定了SEM_UNDO标志,sem_op的绝对值也会被添加到该进程的信号量调整值中。
    • 如果信号量的值(semval)小于sem_op的绝对值,情况如下
      • 如果指定了IPC_NOWAIT,semop返回一个EAGAIN错误
      • 如果没有指定IPC_NOWAIT,这个信号量的semncnt值就会增加(因为调用者即将进入睡眠状态),并且调用进程被挂起,直到发生以下三种情况之一:
        • 信号量的值(semval)大于或等于sem_op的绝对值(即其他进程V释放过了资源),这个信号量的semncnt的值减1(因为调用进程已经完成了等待),并且信号量的值(semval)会减去sem_op的绝对值。 如果指定了SEM_UNDO标志,sem_op的绝对值也会被添加到该进程的信号量调整值中
        • 信号量从系统中移除,在这种情况下,semop函数返回一个EIDRM错误
        • 调用被捕获的信号中断,此时信号量的semncnt值减1(因为调用进程不再等待),并且semop函数调用失败返回一个EINTR错误

5.3、semctl

semctl函数可以对信号量进行直接控制

int semctl(int semid, int semnum, int cmd, ...);
  • semid :由semget函数返回的信号量集ID,类似于操作句柄
  • semnum :指定被操作的信号量在信号量集中的编号
  • cmd : 指定要执行的命令
  • semun : 第四个参数由用户自定义,可选参数是实际的联合体,而不是指向联合体的指针,sys/sem.h文件给出了推荐格式,如下
union semun {int val;                /* for SETVAL */struct semid_ds *buf;   /* for IPC_STAT and IPC_SET */unsigned short *array;  /* for GETALL and SETALL */struct seminfo  *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
struct  seminfo
{int semmap;     /* linux内核没有使用 */int semmni;     /* 系统最多可以拥有的信号量集数目 */int semmns;     /* 系统最多可以拥有的信号量数目 */int semmnu;     /* linux内核没有使用 */int semmsl;     /* 一个信号量集最多允许包含的信号了数目 */int semopm;     /* semop一次最多能执行的sem_op操作数目 */int semume;     /* linux内核没有使用 */int semusz;     /* sem_undo结构体的大小 */int semvmx;     /* 最大允许的信号量值 */int semaem;     /* 最多允许的UNDO次数(带SEM_UNDO标志的semop操作的次数) */
};

cmd 执行的参数和解释如下图:

5.4、信号量同步父子进程

这里使用IPC_PRIVATE信号量来同步父子进程。

假设现在有两个人,因为只有一张小床,所以同一时间只能有一个人在睡觉。下面代码实现了这个过程,但要注意的是最后这个信号量会被删除两次(父子进程各一次,最后一次就会报错)

#include <stdio.h>
#include <wait.h>
#include <sys/sem.h>
#include <unistd.h>union semun {int              val;    /* Value for SETVAL */struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */unsigned short  *array;  /* Array for GETALL, SETALL */struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */
};// op为-1时执行P操作,为1时执行V操作
void pv(int sem_id, int op) {struct sembuf sem_buf;sem_buf.sem_num = 0;sem_buf.sem_op = op;sem_buf.sem_flg = SEM_UNDO;// 第3个参数是要操作的元素个数,这里是1semop(sem_id, &sem_buf, 1);
}int main() {int sem_id = semget(IPC_PRIVATE, 1, 0666);union semun sem_un;sem_un.val = 1;// 设置信号量的值semval为sem_un.val=1semctl(sem_id, 0, SETVAL, sem_un);pid_t pid = fork();if (pid < 0) {return 1;}else if (pid == 0) { // 子进程printf("子进程尝试获取二进制信号量\n");// 传递信号量Ppv(sem_id, -1);printf("子进程睡觉中……\n");sleep(5);printf("子进程醒来了!\n");// 释放信号量Vpv(sem_id, 1);}else { // 父进程printf("*父进程尝试获取二进制信号量\n");// 传递信号量Ppv(sem_id, -1);printf("*父进程睡觉中……\n");sleep(5);printf("*父进程醒来了!\n");// 释放信号量Vpv(sem_id, 1);}// 以下代码也很关键,等待子进程终止waitpid(pid, NULL, 0);// 删除信号量,唤醒等待信号量的进程semctl(sem_id, 0, IPC_RMID, sem_un);return 0;
}

运行程序后就理解整个过程了,打印结果如下:

*父进程尝试获取二进制信号量
*父进程睡觉中……
子进程尝试获取二进制信号量
*父进程醒来了!
子进程睡觉中……
子进程醒来了!

5.5、信号量同步任意两个进程

为了方便将信号量操作的函数集成在一个sem_head.h头文件中,这种跨进程的信号量我们需要事先约定好key值

#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>// 信号量的key
#define SEM_KEY 0x993Aunion semun
{int              val;    /* Value for SETVAL */struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */unsigned short  *array;  /* Array for GETALL, SETALL */struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */
};// 创建信号量
int sem_create()
{int sem_id = semget(SEM_KEY, 1, 0666 | IPC_CREAT);if (-1 == sem_id){printf("semget err, errno = %d\n", errno);}return sem_id;
}// 设置信号量的值
void sem_set(int sem_id)
{// 设置信号量的值semval为sem_un.val=1union semun sem_un;sem_un.val = 1;if (-1 == semctl(sem_id, 0, SETVAL, sem_un)){printf("semctl SETVAL err, errno = %d\n", errno);}
}// op为-1时执行P操作,为1时执行V操作
void sem_pv(int sem_id, int op)
{struct sembuf sem_buf;sem_buf.sem_num = 0;sem_buf.sem_op = op;sem_buf.sem_flg = SEM_UNDO;// 第3个参数是要操作的元素个数,这里是1if (-1 == semop(sem_id, &sem_buf, 1)){printf("semop err, errno = %d\n", errno);}
}// 信号量执行传递P
void sem_p(int sem_id)
{sem_pv(sem_id, -1);
}// 信号量执行释放V
void sem_v(int sem_id)
{sem_pv(sem_id, 1);
}// 删除信号量,唤醒等待信号量的进程
void sem_del(int sem_id)
{union semun sem_un;if (-1 == semctl(sem_id, 0, IPC_RMID, sem_un)){printf("semctl IPC_RMID err, errno = %d\n", errno);}
}

同样,案列是同一时间只能有一个人在睡觉,但是要求子进程后睡觉。为了简便改编上一个列子写在同一个代码中。但这套代码实际上可以用于没有公共祖先的进程之间

#include <wait.h>
#include "sem_head.h"int main() {int sem_id = sem_create();pid_t pid = fork();if (pid < 0) {return 1;}else if (pid == 0) { // 子进程printf("子进程尝试获取二进制信号量\n");// 传递信号量Psem_p(sem_id);printf("子进程睡觉中……\n");sleep(2);printf("子进程醒来了!\n");// 释放信号量Vsem_v(sem_id);}else { // 父进程printf("*父进程先睡觉中……\n");sleep(2);printf("*父进程醒来了!\n");// 设置信号量的值sem_set(sem_id);}// 以下代码也很关键,等待子进程终止waitpid(pid, NULL, 0);// 子进程删除信号量,因为能确保子进程后睡觉if (pid == 0) {sem_del(sem_id);}return 0;
}

5.6、strace 查看进程运行状态

会附加进程查看状态,当停在某行代码时,直接显示该行代码。信号量挂起进程时,会显示相应的代码

jydr@jie:~$ sudo strace -p 1181
strace: Process 1181 attached
gettid() = 1181

5.7、ipcs 查看IPC信息

jydr@jie:~$ ipcs -a--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      ------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态
0x00000000 7          jydr       600        524288     2          目标
0x00000000 1179692    jydr       600        524288     2          目标       --------- 信号量数组 -----------
键        semid      拥有者  权限     nsems
0x00000000 5          jydr       666        1
0x00000000 6          jydr       666        1
0x00000000 15         jydr       666        1

6、POSIX信号量

优点:

  • 与XSI信号量相比,POSIX信号量接口允许更高性能的实现。
  • POSIX信号量接口使用起来更简单:没有信号量集,而且有几个接口是模仿熟悉的文件系统操作的。虽然不要求在文件系统中实现它们,但有些系统确实采用这种方法。
  • 当POSIX信号量被移除时,它的行为更加优雅。当XSI信号量被删除时,使用相同信号量标识符的操作会失败,而errno设置为EIDRM。使用POSIX信号量,操作将继续正常工作,直到最后一个对该信号量的引用被释放。

POSIX信号量有两种,命名和匿名,匿名通常在同一个进程的不同线程间使用,也可以在不同进程,但必须共享了内存的情况下使用。

在使用POSIX信号量时,gcc编译需要加入-pthread,clion编译需要加入

find_package(Threads REQUIRED)
target_link_libraries(obj_name Threads::Threads)

6.1、命令名信号量

1、sem_open

要创建一个新的命名信号量或使用一个现有信号量,我们调用sem_open函数。

#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);
// Link with -pthread.
// Returns: Pointer to semaphore if OK, SEM_FAILED on error
  • 当使用现有的已命名信号量时,我们只指定两个参数:信号量的名称和oflag参数的值指定为零。
  • name
    • 名字的首字符必须是斜杠(/)。
    • 除首字符外,名字中不能再包含其他斜杠(/)。
    • 名字的最长长度由实现定义,不应超过_POSIX_NAME_MAX(14)个字符。
  • oflag
    • oflag参数设置了O_CREAT标志时,若信号量还不存在,则创建一个新的命名信号量。若它已经存在,则打开使用,但不会进行额外的初始化。
    • 当指定O_CREAT标志时,需要提供两个附加参数modevalue
    • 如果希望确保正在创建信号量,可以设置为O_CREAT|O_EXCL。如果信号量已经存在,这将导致sem_open失败。
  • mode :设置权限,指定谁可以访问信号量。和open(2)相同,man手册查询。
  • value :用于在创建信号量时指定信号量的初始值。它可以取从0到SEM_VALUE_MAX的任何值。
2、sem_close

sem_open函数返回一个信号量指针,可以调用sem_close函数来释放与这个信号量相关的任何资源。

#include <semaphore.h>
int sem_close(sem_t *sem);
// Returns: 0 if OK, −1 on error

如果我们的进程没有首先调用sem_close就退出了,内核将自动关闭所有打开的信号量。注意,这并不影响信号量值的状态——如果我们增加了它的值,这并不会因为退出而改变。
类似地,如果调用sem_close,则信号量值不受影响。没有与XSI信号量中的SEM_UNDO标志等效的机制。

3、sem_unlink

要销毁一个命名的信号量,可以使用sem_unlink函数。

#include <semaphore.h>
int sem_unlink(const char *name);
// Returns: 0 if OK, −1 on error

sem_unlink函数删除信号量的名称。如果没有对该信号量的开放引用,则该信号量被销毁。否则,销毁将延迟到最后一个打开的引用被关闭。

4、sem_wait
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
// Both return: 0 if OK, −1 on error
  • sem_wait函数,如果信号量计数为0,就会阻塞。在成功减少信号量计数或被信号中断之前,不会返回。
  • 可以使用sem_trywait函数来避免阻塞。当我们调用sem_trywait时,如果信号量计数为0,它将返回−1,errno设置为EAGAIN,而不是阻塞。

5、sem_post

要增加信号量的值,调用sem_post函数,可理解为释放或解锁函数

#include <semaphore.h>
int sem_post(sem_t *sem);
// Returns: 0 if OK, −1 on error

当调用sem_post时,如果进程在调用sem_wait(或sem_timedwait)时被阻塞,进程就会被唤醒,刚才以sem_post递增的信号量计数会以sem_wait(或sem_timedwait)递减。

6、POSIX命名信号量编程案列

定义一个POSIX命名信号量的头文件。

#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdlib.h>
#include <values.h>
#include <fcntl.h>
#include <errno.h>struct semlock
{sem_t *p_sem;char name[_POSIX_NAME_MAX];
};// 创建或打开信号量
struct semlock *sem_create()
{struct semlock *psl;if ((psl = malloc(sizeof(struct semlock))) == NULL){printf("malloc semlock err, errno = %d", errno);return NULL;}// 定义信号名snprintf(psl->name, sizeof(psl->name), "/mytest_sem");psl->p_sem = sem_open(psl->name, O_CREAT, S_IRWXU, 1);if (SEM_FAILED == psl->p_sem){printf("sem_open err, errno = %d", errno);free(psl);return NULL;}return psl;
}// 释放资源
void sem_free(struct semlock **psl)
{if (-1 == sem_unlink((*psl)->name)){printf("sem_unlink err, errno = %d", errno);}if (-1 == sem_close((*psl)->p_sem)){printf("sem_close err, errno = %d", errno);}free(*psl);*psl = NULL;
}// 信号量加锁
void sem_lock(struct semlock *psl)
{if (-1 == sem_wait(psl->p_sem)){printf("sem_wait err, errno = %d", errno);}
}// 信号量解锁
void sem_unlock(struct semlock *psl)
{if (-1 == sem_post(psl->p_sem)){printf("sem_post err, errno = %d", errno);}
}

继续沿用XSI信号量的睡觉列子。

#include <stdio.h>
#include <wait.h>
#include "posix_sem_head.h"int main()
{// 创建POSIX命名信号量struct semlock *p_sem = sem_create();pid_t pid = fork();if (pid < 0) {return 1;}else if (pid == 0) { // 子进程printf("子进程尝试获取二进制信号量\n");sem_lock(p_sem);printf("子进程睡觉中……\n");sleep(2);printf("子进程醒来了!\n");sem_unlock(p_sem);}else { // 父进程printf("*父进程尝试获取二进制信号量\n");sem_lock(p_sem);printf("*父进程睡觉中……\n");sleep(2);printf("*父进程醒来了!\n");sem_unlock(p_sem);}// 等待子进程终止waitpid(pid, NULL, 0);// 释放资源sem_free(&p_sem);
}

6.2、匿名信号量

当希望在单个进程中使用POSIX信号量时,使用匿名信号量更简单。创建和销毁信号量的方式会改变。

1、sem_init

为了创建一个匿名的信号量,调用sem_init函数。

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
// Returns: 0 if OK, −1 on error
  • sem :需要声明一个类型为sem_t的变量,并将其地址传递给sem_init进行初始化。如果计划在两个进程之间使用这个信号量,需要确保sem参数指向进程之间共享的内存范围。
  • pshared :参数表示是否计划在多个进程中使用该信号量。如果是,则将其设置为非零值。
  • value :参数指定信号量的初始值(通常为1或0)。
2、sem_destroy

当使用完匿名信号量后,可以通过调用sem_destroy函数来销毁它。

#include <semaphore.h>
int sem_destroy(sem_t *sem);
// Returns: 0 if OK, −1 on error

3、POSIX匿名信号编程案列
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <pthread.h>// 初始化
sem_t sem1;void *child(void *arg)
{printf("子线程尝试获取POSOX信号量\n");// 传递信号量Psem_wait(&sem1);printf("子线程睡觉中……\n");sleep(2);printf("子线程醒来了!\n");
}int main()
{sem_init(&sem1, 0, 0);void *ret;pthread_t tid_chi;pthread_create(&tid_chi, NULL, child, NULL);printf("*父线程睡觉中……\n");sleep(2);printf("*父线程醒来了!\n");// 释放信号量Vsem_post(&sem1);// 等待子线程pthread_join(tid_chi, &ret);// 销毁信号量sem_destroy(&sem1);
}

7、共享内存

共享内存是最高效的IPC机制,因为它不涉及进程间数据传输。但是要特别注意的是进程同步问题,共享内存通常和其他IPC方式一起使用

共享内存主要有四个函数:shmget、shmat、shmdt、shmctl

7.1、shmget

shmget函数创建一段新的共享内存,或者获取一段已经存在的共享内存。
当一个新的段被创建时,该段的内容被初始化为0。

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflag);
// Returns: shared memory ID if OK, −1 on error
  • key : 一个键值,用来标识一段全局唯一的共享内存
  • size : 指定共享内存的大小,单位是字节。如果是创建新的共享内存,则size必须被指定。如果是获取已存在的共享内存,可以把size设置为0
  • shmflag : 权限参数,与semget的semflag参数相同,不过还支持两个额外的标志SHM_HUGETLB 和 SHM_NORESERVE,含义如下:
    • SHM_HUGETLB : 类似mmap的MAP_HUGETLB标志,系统将使用大页面来为共享内存分配空间
    • SHM_NORESERVE: 类似mmap的MAP_NORESERVE,不为共享内存保留交换分区(swap空间)。当物理内存 不足的时候,对该共享内存知行写操作将触发SIGSEGV信号

shmget函数创建一段新的共享内存,内核为每个共享内存段维护一个至少包含以下成员的结构:

struct shmid_ds {struct ipc_perm shm_perm; /* see Section 15.6.2 */size_t shm_segsz;         /* size of segment in bytes */pid_t shm_lpid;           /* pid of last shmop() */pid_t shm_cpid;           /* pid of creator */shmatt_t shm_nattch;      /* number of current attaches */time_t shm_atime;         /* last-attach time */time_t shm_dtime;         /* last-detach time */time_t shm_ctime;         /* last-change time */...
};

7.2、shmat 和 shmdt

创建/获取共享内存段之后,不能立即访问。需要调用shmat将其连接到自己的地址空间。使用完后共享内存后,也需要将其从进程地址空间中分离,分别由如下两个系统调用实现。

#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
// Returns: pointer to shared memory segment if OK, −1 on error
#include <sys/shm.h>
int shmdt(const void *shmaddr);
// Returns: 0 if OK, −1 on error
  • shmid : 共享内存标识符。
  • shmaddr : 指定共享内存关联的地址空间,推荐为NULL,让操作系统选择地址,确保代码的可移植性。
  • shmflg :通常用作权限标志,具体查看man手册

7.3、shmctl

shmctl控制共享内存的某些属性。

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf );
// Returns: 0 if OK, −1 on error
  • shmid :共享内存标识符
  • cmd :指定在由shmid指定的内存段上执行以下五个命令之一
    • IPC_STAT 获取这个段的shmid_ds结构体复制到buf指向的结构体中。
    • IPC_SET 在与这个共享内存段关联的shmid_ds结构中,从buf指向的结构中设置以下三个字段:shm_perm.uid、shm_perm.gid, shm_perm.mode。该命令只能由有效用户和超级用户知行。
    • IPC_RMID 从系统中删除共享内存段集。因为要为共享内存段维护一个附件计数(shmid_ds结构中的shm_nattch字段),所以直到最后一个使用该段的进程终止或卸载它时,该段才被删除。不管这个段是否仍然在使用中,段的标识符都会被立即删除,这样shmat就不能再连接这个段了。
    • SHM_LOCK 锁定内存中的共享内存段,使其不能移动至swap交换分区。
    • SHM_UNLOCK 解锁内存中的共享内存段。

7.4、共享内存编程案列

假设一对夫妻往家里小金库存一年的钱,丈夫每月工资5000,妻子工资4000。小金库就可以看做一个共享内存。

首先写好头文件。

#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>/* 约定一个共享内存键指 */
#define SHM_MY_KEY 0x389A/* 家庭金库 */
typedef struct
{int all_money;   // 家庭收入int hubby_money; // 丈夫收入int wife_money;  // 妻子收入
}EXCHEQUER;/* 共享内存标识符 */
int g_shmid = 0;/* 创建-获取共享内存 */
int create_shared_memory(EXCHEQUER **p_exchequer)
{void *p_shmaddr = NULL;g_shmid = shmget(SHM_MY_KEY, sizeof(EXCHEQUER), 0666 | IPC_CREAT);if (-1 == g_shmid){printf("shmget error, errno = %d\n", errno);return 0;}/* 连接共享内存的地址空间 */p_shmaddr = shmat(g_shmid, NULL, 0);if ((void*)-1 == p_shmaddr){printf("shmat error, errno = %d\n", errno);return 0;}*p_exchequer = p_shmaddr;return 1;
}/* 删除共享内存 */
void delete_shared_memory(EXCHEQUER **p_exchequer)
{if(-1 == shmdt(*p_exchequer)){printf("shmdt error, errno = %d\n", errno);return;}if (-1 == shmctl(g_shmid, IPC_RMID, NULL)){printf("shmctl error, errno = %d\n", errno);return;}*p_exchequer = NULL;
}

下面是丈夫进程,运行的时候还需要一个妻子进程,妻子进程只需修改一个变量和金额就行,同时使用POSIX信号量进行进程同步,POSIX信号量利用的是上一章节的案列代码。

#include "shared_memory.h"
#include "posix_sem.h"// hubby 丈夫进程
int main() {EXCHEQUER *p_exchequer = NULL;// 创建共享内存create_shared_memory(&p_exchequer);// 创建POSIX命名信号量struct semlock *p_sem = sem_create();// 增加小金库for (int i = 0; i < 12; ++i) {sem_lock(p_sem);p_exchequer->all_money += 5000;p_exchequer->hubby_money += 5000;usleep(100000); // 睡眠0.1秒sem_unlock(p_sem);}printf("\n\n丈夫进程打印如下:\n");printf("家庭收入:%d元\n丈夫收入:%d元\n妻子收入:%d元\n",p_exchequer->all_money, p_exchequer->hubby_money, p_exchequer->wife_money);sleep(2);delete_shared_memory(&p_exchequer);
}

以下是同时运行两个进程后的一组输出,可以看出进程同步成功,共享内存完成。

debug$
妻子进程打印如下:
家庭收入:53000元
丈夫收入:5000元
妻子收入:48000元丈夫进程打印如下:
家庭收入:108000元
丈夫收入:60000元
妻子收入:48000元
shmctl error, errno = 22

8、消息队列

直接上案列


#include <stdio.h>
#include <sys/msg.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>// MSG的key
#define DZ_MSG_KEY 0x993Atypedef struct{long type;char data[512];
}MSG_BUF;// 发送消息队列消息
void send_msg_queue(MSG_BUF *msg)
{int     msg_id = 0;int     res = 0;// 创建或者获取消息队列msg_id = msgget(DZ_MSG_KEY, IPC_CREAT | 0777);if (msg_id < 0){printf("msgget error");return;}printf("msg_id=%d\n", msg_id);// 发送消息res = msgsnd(msg_id, (const void*)msg, sizeof(msg->data), IPC_NOWAIT);if (res < 0){printf("msgsnd error");return;}
}// 接收消息队列数据线程
void* thread_fun(void* arg)
{int     msg_id = 0;int     res = 0;int     type = 0;   // 0为读取消息队列第一个消息// 创建或者获取消息队列msg_id = msgget(DZ_MSG_KEY, IPC_CREAT | 0777);if (msg_id < 0){printf("msgget error");return (void*)0;}MSG_BUF msg = {};// 接收消息while (1){memset(&msg, 0, sizeof(msg));res = msgrcv(msg_id, (void*)&msg, sizeof(msg.data), type, 0);if (res < 0){printf("msgsnd error");continue;}switch (msg.type) {case 1:printf("001 data=%s\n", msg.data);break;case 2:printf("002 data=%s\n", msg.data);break;case 3:printf("003 data=%s\n", msg.data);break;default:break;}printf("xunhuan msg queue\n");}
}int main()
{pthread_t phandle;int err = pthread_create(&phandle, NULL, thread_fun, NULL);if (err != 0){printf("can't create thread!");}MSG_BUF msg = {};msg.type = 1;memcpy(msg.data, "wo shi ni baba", sizeof("wo shi ni baba"));while (1){//printf("main main\n\n");sleep(5);send_msg_queue(&msg);msg.type = 2;send_msg_queue(&msg);msg.type = 3;send_msg_queue(&msg);}return 0;
}

二、多线程编程

多线程涉及POSIX,新系统可能没有相应的man手册,安装一下就好。

sudo apt install -y manpages-posix-dev

1、NPTL线程库

查看NPTL线程库。

debug$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.32

相比于2.6内核版本之前的LinuxThreads线程库(实现方式是模拟进程),NPTL线程库主要优势在于:

2、编译参数

# 在CMakeLists.txt中需加入:
cpp的添加
SET(CMAKE_CXX_FLAGS -pthread)
C语言的添加
SET(CMAKE_C_FLAGS -pthread)# gcc 或者 cc 编译
gcc 添加 -lpthread

3、创建线程

可以通过调用pthread_create函数来创建其他线程。

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, // 输出线程句柄const pthread_attr_t *restrict attr,     // 通常NULL默认属性void *(*start_rtn)(void *),              // 回调函数void *restrict arg);                     // 传入回调函数的参数
// Returns: 0 if OK, error number on failure

系统上所有用户创建的线程总数不能超过cat /proc/sys/kernel/threads-max

以下是创建线程后,查看进程和线程ID的小案列。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 打印进程和线程ID
void print_tids(const char* s){pid_t pid;pthread_t tid;pid = getpid();tid = pthread_self(); // 获取线程IDprintf("%s pid-> %lu, tid-> %lu(0x%x)\n", s,(unsigned long)pid, (unsigned long)tid, (unsigned long)tid);
}// 线程1
void* thread_fun(){print_tids("new thread: ");return (void*)2;
}int main(){pthread_t phandle;int err = pthread_create(&phandle, NULL, thread_fun, NULL);if (err != 0){printf("can't create thread!");}print_tids("main thread: ");sleep(1);return 0;
}
# 输出结果
main thread:  pid-> 40276, tid-> 139824549762880(0x709e9740)
new thread:  pid-> 40276, tid-> 139824549758528(0x709e8640)

4、结束线程

4.1、return

return 可以主动结束某个线程。

return (void*)0

4.2、pthread_exit

线程结束是最好调用pthread_exit以确保安全,干净的退出。

#include <pthread.h>
void pthread_exit(void *retval);
// This function does not return to the caller.

retval参数是一个无类型指针,类似于传递给start例程的单个参数。该指针可通过调用pthread_join函数用于进程中的其他线程。

4.3、pthread_join

一个进程中所有线程都可以调用pthread_join来回收其他线程,即等待其他线程结束。

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// Returns: 0 if OK, error number on failure

4.4、结束线程综合案列

此案列打印相应的退出码,这点在某些情况可能会有用。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* thread1(){printf("thread 1 returning\n");return (void*)1;
}void* thread2(){printf("thread 2 exiting\n");pthread_exit((void*)2);
}void err_print(int err, char* str){if (err != 0){printf("%s\n", str);}
}int main(){int err = 0;pthread_t tid1, tid2;void* pret = NULL;// 创建线程err = pthread_create(&tid1, NULL, thread1, NULL);err_print(err,"can't create thread 1");err = pthread_create(&tid2, NULL, thread2, NULL);err_print(err,"can't create thread 2");// 等待线程并打印相应线程的退出码err = pthread_join(tid1, &pret);err_print(err,"can't join with thread 1");printf("thread 1 exit code %ld\n", (long)pret);err = pthread_join(tid2, &pret);err_print(err,"can't join with thread 2");printf("thread 2 exit code %ld\n", (long)pret);return 0;
}
# 运行结果
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2

4.5、pthread_cancel

有时候需要异常终止一个线程,使用pthread_cancel。调用pthread_cancel函数来请求同一进程中的另一个线程被取消,这只是一个请求,它不会等待该线程终止。

#include <pthread.h>
int pthread_cancel(pthread_t tid);
// Returns: 0 if OK, error number on failure

pthread_cancel 函数只是发送取消请求,接到取消请求的目标线程可以决定是否被取消以及如何取消,由以下两个函数决定。

  • pthread_setcancelstate
  • pthread_setcanceltype

一个线程可以通过调用pthread_setcancelstate来改变它的取消状态。

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
// Returns: 0 if OK, error number on failure
state:
PTHREAD_CANCEL_ENABLE,允许线程被取消,创建线程时的默认取消状态。
PTHREAD_CANCEL_DISABLE,禁止线程被取消,如果一个线程收到取消请求,它会将请求挂起,直到该线程允许被取消oldstate : 旧的的取消状态


在调用pthread_cancel之后,实际的取消直到线程到达一个取消点才会发生。可以通过调用pthread_setcanceltype来改变取消类型

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
// Returns: 0 if OK, error number on failure
type :
PTHREAD_CANCEL_ASYNCHRONOUS,线程随时都可以被取消,这种情况下,接到取消请求的目标线程立即执行。
PTHREAD_CANCEL_DEFERRED,允许目标线程延迟行动。oldtype : 返回oldtype所指向的前一个整数类型

5、互斥锁

互斥体优劣与二值信号量一样,但多线程访问共享资源互斥体更专业一些。

5.1 互斥锁API

互斥体由pthread_mutex_t数据类型表示。在使用互斥体之前,必须先将其初始化为常数PTHREAD_MUTEX_INITIALIZER(仅用于静态分配互斥)或调用pthread_mutex_init。如果动态分配互斥锁(例如通过调用malloc),那么在释放内存之前需要调用pthread_mutex_destroy。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// Both return: 0 if OK, error number on failure


要锁定互斥锁,调用pthread_mutex_lock。如果互斥锁已经被锁定,调用该互斥锁的线程将阻塞,直到互斥锁被解锁。要解锁互斥锁,调用pthread_mutex_unlock

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// All return: 0 if OK, error number on failure

5.2 互斥锁案列

当两个线程同时修改一个变量的时候,会出现竞争状态,导致未知的修改结果,上锁后就能确保结果。

#include <stdio.h>
#include <pthread.h>int g_num = 0;
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;// 改变全局变量
void add_num()
{for (int i = 0; i < 1000000; ++i) {pthread_mutex_lock(&g_mutex);++g_num;pthread_mutex_unlock(&g_mutex);}printf("g_num = %d\n", g_num);
}void *thread1()
{// 第二个线程修改全局变量add_num();pthread_exit((void*)1);
}int main() {pthread_t tid1;pthread_create(&tid1, NULL, thread1, NULL);// 主线程修改全局变量add_num();pthread_join(tid1, NULL);
}
# 运行结果
g_num = 1547848
g_num = 2000000

5.3 互斥锁属性

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
// 获取和设置互斥锁共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
// 获取和设置互斥锁类型
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

psharedtype常用。

5.4 死锁案列

主线程先获取了锁1,子线程先获取了锁2。
这种情况下,主线程想要获取锁2而不得,进入休眠。同理,子线程想要获取锁1而不得,进入休眠。程序就死锁了,编程中要尽量避免这种情况。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>int g_num = 0;
int g_multi = 10;pthread_mutex_t g_mutex1;
pthread_mutex_t g_mutex2;// 操作两把锁
void two_lock(pthread_mutex_t *mutex1, pthread_mutex_t *mutex2)
{pthread_mutex_lock(mutex1);++g_num;sleep(3);pthread_mutex_lock(mutex2);--g_multi;pthread_mutex_unlock(mutex1);pthread_mutex_unlock(mutex2);
}void *thread1()
{// 子线程先获得锁2two_lock(&g_mutex2, &g_mutex1);pthread_exit((void*)1);
}int main() {pthread_mutex_init(&g_mutex1, NULL);pthread_mutex_init(&g_mutex2, NULL);pthread_t tid1;pthread_create(&tid1, NULL, thread1, NULL);// 主线程先获得锁1two_lock(&g_mutex1, &g_mutex2);pthread_join(tid1, NULL);printf("执行完毕\n");pthread_mutex_destroy(&g_mutex1);pthread_mutex_destroy(&g_mutex2);
}

实际情况通常是没有sleep函数的,这样的代码有时会死锁,有时不会,排查出BUG难度就比较大了。

避免死锁的一种方式是使用pthread_mutex_timedlock超时锁,超过设定的时间就自动释放。
pthread_mutex_timedlock函数等价于pthread_mutex_lock,但是如果达到超时值,pthread_mutex_timedlock将返回错误码ETIMEDOUT,而不会锁定互斥对象。

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);
// Returns: 0 if OK, error number on failure

6、读写锁

6.1、读写锁原理介绍

读写锁非常适合读取数据结构多于修改数据结构的情况。

当读写锁以写模式持有时,它保护的数据结构可以被安全地修改,因为一次只有一个线程可以以写模式持有锁。当读写锁以读模式持有时,它所保护的数据结构可以被多个线程读取,只要这些线程首先以读模式获取锁。

使用互斥锁,状态要么是锁定的,要么是解锁的,并且一次只能有一个线程锁定它。读写锁类似于互斥锁,它允许更高程度的并行性。

读写锁有三种状态:读模式下的锁定、写模式下的锁定、解锁。

  • 当读写锁被写锁定时,所有试图锁定它的线程都会阻塞,直到它被解锁。
  • 当读写锁被读锁定时,所有试图以读模式锁定它的线程都被授予访问权限,但任何试图以写模式锁定它的线程都会阻塞,直到所有线程都释放了它们的读锁。

6.2、读写锁API

与互斥锁一样,读写锁必须初始化,可以静态初始化为PTHREAD_RWLOCK_INITIALIZER。或者在使用前初始化,并在释放其底层内存之前销毁。

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// Both return: 0 if OK, error number on failure


读写锁读锁定,调用pthread_rwlock_rdlock
读写锁写锁定,调用pthread_rwlock_wrlock
无论是读还是写锁定,都通过调用pthread_rwlock_unlock来解锁。

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// All return: 0 if OK, error number on failure

实际使用中可能会限制在共享模式下读写锁的锁定次数,因此我们需要检查pthread_rwlock_rdlock的返回值。另外两个函数只要设计合理时无需检查返回值。

6.3、原语读写锁和超时读写锁

UNIX单一规范还定义了读写锁定原语的条件版本。

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// Both return: 0 if OK, error number on failure

当可以获取锁时,这些函数返回0。否则,返回错误EBUSY。这两个函数可以用于在难以遵守锁层次结构的情况下避免死锁。


与互斥锁一样,读写锁也有超时读写锁,提供一个超时时间,以使应用程序在试图获取读取-写入锁时避免无限期阻塞。这些函数是pthread_rwlock_timedrdlockpthread_rwlock_timedwrlock

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,const struct timespec *restrict tsptr);
// Both return: 0 if OK, error number on failure

如果它们不能获得锁,以上函数将在超时时返回ETIMEDOUT错误。与pthread_mutex_timedlock函数一样,超时指定了绝对时间,而不是相对时间。

6.4、读写锁编程案列

参考互斥锁案列,使用情况一模一样。

7、自旋锁

7.1、自旋锁简介

自旋锁类似于互斥锁,不同的是,该进程不是通过休眠来阻塞进程,而是通过忙碌等待(旋转)来阻塞进程,直到可以获得该锁。

自旋锁可以用于以下情况:锁被持有的时间很短,并且线程不希望产生被取消调度的成本。

自旋锁很高效,但可能会导致CPU资源的浪费:当线程正在旋转并等待锁变得可用时,CPU不能做其他任何事情。这就是为什么自旋锁应该只持有很短的一段时间。

在非抢占式内核中使用自旋锁非常有用:除了提供互斥机制外,它还会阻塞中断,这样中断处理程序就不会通过尝试获取已经锁定的锁而导致系统死锁(把中断看作是另一种类型的抢占)。在这些类型的内核中,中断处理程序不能休眠,所以它们唯一可以使用的同步原语是自旋锁。

在用户级别中,除非运行在不允许抢占的实时调度类中,否则自旋锁没有那么有用。用户级别的互斥锁和自旋锁能达到差不多的效率,很多互斥锁都底层都借鉴了自旋锁的原理,有一个自旋超时机制,所以说自旋锁通常用作低级原语来实现其他类型的锁。根据系统架构的不同,可以使用测试集指令高效地实现它们。

7.2、自旋锁API

API与互斥锁类似。

#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
// Both return: 0 if OK, error number on failure

pshared参数表示进程共享属性,该属性指示如何获取自旋锁。如果设置为
PTHREAD_PROCESS_SHARED,那么可以由访问锁的底层内存的线程获得自旋锁,即使这些线程来自不同的进程。否则,pshared参数被设置为PTHREAD_PROCESS_PRIVATE,并且自旋锁只能被初始化它的进程中的线程访问。


要锁定自旋锁,可以调用pthread_spin_lock或pthread_spin_trylock。
pthread_spin_trylock如果不能立即获得锁,会返回EBUSY错误。注意,pthread_spin_trylock不旋转。
无论旋转锁是如何被锁定的,都可以通过调用pthread_spin_unlock来解锁它。

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
// All return: 0 if OK, error number on failure

若自旋锁锁定成功,需要小心不要调用任何在持有自旋锁时可能处于睡眠状态的函数。如果这样做,就会浪费CPU资源,因为如果其他线程试图获取CPU,就会延长它旋转的时间。

8、条件变量

用法不明,遇见时参考UNIX环境高级编程第三版。

9、障碍

9.1 障碍简介

障碍(barriers)是可以用来协调多个线程并行工作的异步机制。障碍允许每个线程等待,直到所有合作的线程到达同一点,然后从那里继续执行。

类似于障碍的的一种形式是pthread_join函数,允许一个线程等待另一个线程退出。
障碍对象比这更普遍。它们允许任意数量的线程等待,直到所有线程都完成处理,但这些线程并不一定要退出。它们可以在所有线程到达障碍后继续工作。

9.2 障碍API

初始化和释放函数于互斥锁类似。
count参数来指定在所有线程被允许继续之前必须到达barrier的线程数。

#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,
unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
// Both return: 0 if OK, error number on failure


pthread_barrier_wait函数指示一个线程已经完成了它的工作,并准备等待所有其他线程赶上来。

#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
// Returns: 0 or PTHREAD_BARRIER_SERIAL_THREAD if OK, error number on failure

如果barrier计数(在pthread_barrier_init调用中设置)还没有满足,那么调用pthread_barrier_wait的线程就会进入睡眠状态。如果线程是最后一个调用pthread_barrier_wait的线程,从而满足barrier计数,那么所有的线程都将被唤醒。
对于任意一个线程来说,pthread_barrier_wait函数返回了PTHREAD_BARRIER_SERIAL_THREAD的值。其余的线程看到的返回值为0。这允许一个线程继续作为主线程处理所有其他线程所做的工作的结果。
一旦达到barrier计数并且线程被解除阻塞,barrier就可以再次使用。但是,barrier计数不能更改,除非调用pthread_barrier_destroy函数,然后再调用pthread_barrier_init函数,并使用不同的计数。

9.3 障碍编程案列

案列情况是对8000万个数字进行快速排序,我的电脑性能还可以,一个线程只花费了13.5秒时间。

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>#define NUMNUM 80000000L     /* 需要排序的数字的个数 */long nums[NUMNUM];// 快速排序辅助函数
int compare_long(const long *num1, const long *num2)
{if (*(long*)num1 == *(long*)num2)return 0;else if (*(long*)num1 > *(long*)num2)return 1;elsereturn -1;
}// 显示排序耗费的时间
void show_time(struct timeval *start, struct timeval *end)
{long long startusec = start->tv_sec * 1000000 + start->tv_usec;long long endusec = end->tv_sec * 1000000 + end->tv_usec;double elapsed = (double)(endusec - startusec) / 1000000.0;printf("经过 %.4f 秒完成快速排序\n", elapsed);
}int main()
{struct timeval start, end;// 生成8000万个固定的伪随机数srandom(1);for (int i = 0; i < NUMNUM; ++i) {nums[i] = random();}// 获取开始时间gettimeofday(&start, NULL);// 快速排序qsort(&nums, NUMNUM, sizeof(long), (__compar_fn_t)compare_long);// 获取结束时间gettimeofday(&end, NULL);// 显示排序耗费时间show_time(&start, &end);
}


接下来使用8个线程对8000万个数字进行快速排序,结果是1.9秒就完成,相比之前快了6、7倍。

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <pthread.h>
#include <limits.h>#define NPROC 8              /* number of threads */
#define NUMNUM 80000000L     /* number of numbers to sort */
#define TNUM (NUMNUM/NPROC)  /* number to sort per thread */long nums[NUMNUM];
long snums[NUMNUM];pthread_barrier_t g_barrier;// 快速排序辅助函数
int compare_long(const long *num1, const long *num2)
{if (*(long*)num1 == *(long*)num2)return 0;else if (*(long*)num1 > *(long*)num2)return 1;elsereturn -1;
}// 显示排序耗费的时间
void show_time(struct timeval *start, struct timeval *end)
{long long startusec = start->tv_sec * 1000000 + start->tv_usec;long long endusec = end->tv_sec * 1000000 + end->tv_usec;double elapsed = (double)(endusec - startusec) / 1000000.0;printf("经过 %.4f 秒完成快速排序\n", elapsed);
}// 线程回调
void *thread_callback(void *arg)
{int num = (int)arg;printf("线程%d 快速排序中……\n", num + 1);qsort(&nums[num * TNUM], TNUM, sizeof(long), (__compar_fn_t)compare_long);// 等待所有线程执行完毕pthread_barrier_wait(&g_barrier);printf("线程%d pthread_barrier_wait 结束\n", num + 1);pthread_exit((void*)arg);
}// 合并
void merge_num() {long idx[NPROC];long i, min_idx, s_idx, num;for (i = 0; i < NPROC; ++i) {idx[i] = i * TNUM;}for (s_idx = 0; s_idx < NUMNUM; ++s_idx){num = LONG_MAX;for (i = 0; i < NPROC; ++i) {if ((idx[i] < (i + 1) * TNUM) && (nums[idx[i]] < num)) {num = nums[idx[i]];min_idx = i;}}snums[s_idx] = nums[idx[min_idx]];++idx[min_idx];}
}int main()
{pthread_t tid;struct timeval start, end;// 生成8000万个固定的伪随机数srandom(1);for (int i = 0; i < NUMNUM; ++i) {nums[i] = random();}// 获取开始时间gettimeofday(&start, NULL);// 初始化障碍pthread_barrier_init(&g_barrier, NULL, NPROC + 1);// 创建8个线程for (int i = 0; i < NPROC; ++i) {if(0 != pthread_create(&tid, NULL, thread_callback, (void*)i)){printf("创建线程失败,退出!\n");exit(0);}}// 合并数组merge_num();printf("主线程开始等待\n");// 等待所有线程执行完毕pthread_barrier_wait(&g_barrier);printf("主线程 pthread_barrier_wait 结束\n");// 获取结束时间gettimeofday(&end, NULL);// 显示排序耗费时间show_time(&start, &end);// 销毁障碍pthread_barrier_destroy(&g_barrier);// 打印最前和最后的20个数看是否排序成功for (int i = 0; i < 20; ++i) {printf("%d ", nums[i]);}printf("\n");for (int i = NUMNUM - 1; i > NUMNUM - 21; --i) {printf("%d ", nums[i]);}
}

10、多线程环境注意事件

Linux多进程多线程编程笔记相关推荐

  1. linux c多进程多线程,linux下的C\C++多进程多线程编程实例详解

    linux下的C\C++多进程多线程编程实例详解 1.多进程编程 #include #include #include int main() { pid_t child_pid; /* 创建一个子进程 ...

  2. [转]Linux 的多线程编程的高效开发经验

    Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别.不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断.本文中我们 ...

  3. Linux环境多线程编程基础设施

    Linux环境多线程编程基础设施 来源:Yebangyu 本文介绍多线程环境下并行编程的基础设施.主要包括: Volatile __thread Memory Barrier __sync_synch ...

  4. Linux 的多线程编程的高效开发经验

    背景 Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别.不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断.本文 ...

  5. linux线程多参数传递参数,Linux中多线程编程并传递多个参数

    解析Linux中多线程编程并传递多个参数 Linux中多线程编程并传递多个参数实例是本文讲解的内容,不多说,先来看内容. Linux下的多线程编程,并将多个参数传递给线程要执行的函数. 以下是实验程序 ...

  6. 对linux中多线程编程中pthread_join的理解

    对linux中多线程编程中pthread_join的理解 分类: 程序员面试 linux学习2013-08-04 21:32 234人阅读 评论(0) 收藏 举报 多线程linuxpthread_jo ...

  7. linux 多线程 semaphore ,Linux下多线程编程-Pthread和Semaphore使用.doc

    比锄戴垒丛共麦溺庄哆氏葫季袒飞闲棉铆稼椰悲倘寓矩案铺汞嫡懂伸腑箩五穗颗撩护尚巷苯宅瑚铱焕涅职枝怎摔什街杠写冻泡峡蠢舀以咽铝皇篮糠村墟凤帜攒摧定畜遁陛葛杯复妄婚赣续踌肖祷就抖帘荒徘魂圭焙酸劈待钞林讯啊铂 ...

  8. [原创]手把手教你Linux下的多线程设计--Linux下多线程编程详解(一)

    本文可任意转载,但必须注明作者和出处. [原创]手把手教你Linux下的多线程设计(一)                                       --Linux下多线程编程详解 原 ...

  9. linux 多线程编程笔记

    一, 线程基础知识 1,线程的概念 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行 中必不可少的资源(如程序计 ...

最新文章

  1. java写出文本文档乱码_对象流如何写出到文件以及为什么乱码
  2. 2019年计算机一级考试pdf,2019年计算机一级考试试题与答案.pdf
  3. 耳朵经济在生活中的应用
  4. 小程序当中的文件类型,组织结构,配置,知识点等
  5. 二维碰撞检测matlab,二维平面内的碰撞检测【二】
  6. WDK中出现的特殊代码
  7. 适用于各种列表操作的Python程序
  8. TechEd 2007 HOL分享
  9. sql server 游标和with as使用
  10. 富集分析:(一)概述
  11. 用Regedit命令控制注册表
  12. 批量生成条形码并写入到excel文件
  13. 服务器winsxs文件夹怎么清理工具,win10系统winsxs文件夹清理的操作方法
  14. 用PowerPoint巧做特效字幕(转)
  15. 数字式高精度可调电流源电路设计
  16. 微信小程序关于wx:key的警告
  17. 初学编程遇到的问题总结
  18. 【Android-Broadcast】广播的权限
  19. python 3 爬虫小白PyCharm爬取简单网页信息控制台错误
  20. 看英文数据手册必备之——Copy Translator

热门文章

  1. 烤仔万花筒 | “人类群星闪耀时”系列NFT现已在Tspace开售
  2. CURL 发送请求示例
  3. 中小企业及个人如何3步做出完美关键词
  4. 2022年学习机器人和人工智能的一些期待
  5. java毕业设计扶贫助农与产品合作系统mybatis+源码+调试部署+系统+数据库+lw
  6. erlang与java构建的节点通讯
  7. github电脑壁纸_40行代码+奇技淫巧搞定专属电脑壁纸库【附壁纸】
  8. G1技术细节之记忆集和卡表解决跨代引用问题
  9. 剖析抖音爆火的美食探店大佬,揭秘他们的运营秘诀
  10. Python的扩展阅读