进程?
系统调用
系统调用提供了一组接口,允许用户程序请求操作系统内核提供的服务。
当进程想要访问内核或硬件资源时必须通过系统调用接口完成访问。
操作系统开启的时候,会将自己加载到内存中。进程在用户态转换成内核态的时候,只需要在自己的虚拟地址空间内进行跳转,转到内核空间,然后在执行系统调用的时候,系统调用函数会更改进程的运行级别,然后通过内核级页表映射到对应的内存中执行系统调用,系统调用完成后,进程从内核态切换回用户态。
父子进程:
1.创建子进程
#include <unistd.h>
pid_t fork();返回值:
父进程:返回子进程的进程ID(PID)。
子进程:返回0。
出错:返回-1,并设置错误码
errno
。2.等待子进程退出
a.wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULLb.waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
比特#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork");return 1;} else if (pid == 0) {// 子进程printf("Child process, PID: %d\n", getpid());sleep(3); // 模拟子进程工作return 0;} else {// 父进程int status;pid_t result = waitpid(pid, &status, 0);printf("Child exited with status %d\n", WEXITSTATUS(status)); }return 0; }
进程通信
进程之间是独立的,所以让多个进程通信一定要让他们看到同一份资源。
不同的通信种类本质就是是OS中哪一个模块提供的。
文件系统->管道(也叫管道文件,本质就是文件)
a.匿名管道
匿名管道文件是一种内存级别的文件,主要是用于进程之间的交流。它是由操作系统在内存中分配的一块缓冲区,用于临时存储数据,没有在文件系统中分配磁盘空间,也没有一个持久的、全局可访问的路径名。
用法:
1.创建管道:
#include <unistd.h>
int pipe(int pipefd[2]);int pipefd[2]:输出型参数,返回读端pipefd[0]和写端pipefd[1]的fd。
成功返回0,失败返回-1并设置错误码。
关闭文件描述符:close(fd);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {int fds[2]; // 文件描述符数组,用于存储管道的读端和写端// 1. 父进程创建管道if (pipe(fds) == -1) { // 创建管道perror("pipe");exit(EXIT_FAILURE);}printf("Pipe created, fd[0]=%d, fd[1]=%d\n", fds[0], fds[1]);pid_t pid = fork(); // fork出子进程if (pid == -1) { // fork失败perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子进程close(fds[1]); // 关闭管道的写端char buffer[100];int count = read(fds[0], buffer, sizeof(buffer)); // 从管道的读端读取数据if (count == -1) {perror("read");exit(EXIT_FAILURE);}buffer[count] = '\0'; // 确保字符串以空字符结尾printf("Child process read: %s\n", buffer);close(fds[0]); // 关闭管道的读端exit(EXIT_SUCCESS);}// 父进程const char *message = "Hello, child process!";write(fds[1], message, strlen(message) + 1); // 向管道的写端写入数据close(fds[0]); // 关闭管道的读端close(fds[1]); // 关闭管道的写端wait(NULL); // 等待子进程结束printf("Parent process sent: %s\n", message);return 0;
}
b.命名管道
命名管道(Named Pipe)在文件系统中有一个路径名,类似于一个普通文件,但它并不将数据存储在磁盘上。相反,命名管道在内存中维护一个缓冲区,用于临时存储数据。这种设计使得命名管道在数据传输时不需要进行磁盘 I/O 操作,从而提高了数据传输的效率,节省了磁盘 I/O 操作的时间。
1.创建命名管道
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int mkfifo(const char *pathname, mode_t mode);
pathname:命名管道的路径名。
mode:文件权限模式。
2.删除文件系统中的一个文件或链接。它通常用于删除普通文件、符号链接或命名管道。
命名管道在文件系统中有一个路径名,类似于一个普通文件。如果不删除,它会一直存在于文件系统中,占用磁盘空间和文件系统资源。
#include <unistd.h>
int unlink(const char *pathname);
pathname:要删除的文件或链接的路径名。
管道的特点:
- 匿名管道的生命周期随进程。命名管道需要显示删除。
- 匿名管道用来进行具有血缘关系的进程之间进行通信,常用于父子通信。命名管道可以用于没有血缘关系的进程通信。
- 管道是面向字节流的。
- 管道是一种半双工的单向通信方式。
- 管道具有内置的同步机制。读操作会阻塞直到有数据可读,写操作会阻塞直到缓冲区有空间。
System V IPC机制
POSIX IPC与System V IPC?
POSIX IPC与System V IPC在基本概念上几乎相同,主要区别在于接口方面。
POSIX相对于System V可以说是比较新的标准,语法相对简单。System-V IPC因为使用年代久远,因此有许多系统支持,使用得广泛,但由于没有固定的标准,所以不同操作系统System V存在一些差异。有小部分操作系统没有实现POSIX标准,但POSIX的可移植性,必然是后续的发展趋势
IPC(Inter-Process Communication,进程间通信)机制是指一组允许不同进程之间进行数据交换和同步的技术和方法。
IPC资源的组织方式:先描述,在组织。
如何查看IPC资源?
ipcs -m/-q/-s
System V消息队列接口调用包括:
msgget()
:创建或获取消息队列的标识符。
msgsnd()
:向消息队列发送消息。
msgrcv()
:从消息队列接收消息。
msgctl()
:对消息队列进行控制操作。
内存管理系统->共享内存
是和文件脱节的,不方便整合到网络通信当中,很少用。
概念:通过让不同的进程,看到同一个内存块。
原理:
1.申请一块空间
int shmget(key_t key, size_t size, int shmflg);
概念:用于创建共享内存段或获取一个已经存在的共享内存段标识符的函数
参数:
key
:用于识别共享内存段的键值,是什么不重要,重点是能进行唯一性标识,通常使用ftok
函数生成。key_t ftok(const char *pathname, int proj_id);pathname 是指定的文件名,这个文件必须是存在的而且可以访问的。proj_id 是子序号,它是一个 8bit 的整数。即范围是 0~255。
size
:共享内存段的大小,单位是字节。
shmflg
:打开标志,用于指定操作的方式,比如创建共享内存段、获取共享内存标识符等。
IPC_CREAT:如果共享内存不存在,就创建,存在就获取。
IPC_EXCL:不能单独使用,IPC_CREAT|IPC_EXCL表示如果不存在就创建,如果存在就出错返回。return value:
成功时返回共享内存段的标识符(非负整数),失败时返回-1。
2.将创建好的内存映射进进程的地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
概念:
shmat
函数是用于将共享内存段附加(attach)到进程地址空间的系统调用。通过shmat
函数,进程可以将一个已存在的共享内存段连接到自己的地址空间,从而可以读写这块内存。参数:
shmid:共享内存段的标识符,由
shmget
函数创建共享内存段时返回。shmaddr:指定共享内存附加到进程地址空间的位置。通常设置为
NULL
,表示由系统选择最佳位置。shmflg:控制附加操作的标志,0。
return value:
成功
shmat
返回附加到进程地址空间的共享内存段的指针;失败返回-1
,并设置errno
。
3.分离共享内存
int shmdt(const void *shmaddr);
概念:用于从当前进程的地址空间中分离(或解除映射)一个已附加(映射)的共享内存段。
参数:
shmaddr:共享内存段在进程地址空间中的地址,通常由
shmat
函数返回
4.删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
概念:用于对共享内存段进行控制操作的系统调用,包括获取共享内存段的信息、删除共享内存段以及更改共享内存段的权限等。
参数:
shmid:共享内存段的标识符,
shmget
函数返回值。cmd:指定要执行的操作命令,最常用的命令删除共享内存段:
IPC_RMID,
释放相关资源。buf:0,设置内存段的参数会用到。
return value:
成功返回0,出错返回-1,并设置errno
。
代码:
int getShmHelper(key_t k, int flags)
{int shmid = shmget(k, MAX_SIZE, flags);if (shmid < 0){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(2);}
}
int getShm(key_t k)
{return getShmHelper(k,IPC_CREAT);
}
int createShm(key_t k)
{return getShmHelper(k,IPC_CREAT|IPC_EXCL|0666);//共享内存的权限0666,不然会premission denied
}
void* attachShm(int shmid)
{void* mem = shmat(shmid,nullptr,0);if((long long)men == -1L){std::cerr<<"shmat:"<<errno<<":"<<strerror(errno)<<std::endl;exit(3);}return mem;
}
void detachShm(void* start)
{if(shmdt(start) == -1){std::cerr<<"shmdt:"<<errno<<":"<<strerror(errno)<<std::endl;}
}
void delShm(int shmid)
{if(shmtl(shmid,IPC_RMID,nullptr) == -1){std::cerr<<errno<<":"<<strrror(errno)<<std::endl;}
}
特点:
- 共享内存的生命周期是随OS的。
- 所有进程通信中速度最快的,能大大减少数据拷贝的次数。
- 共享内存没有同步互斥机制,没有对数据做任何保护。
对比管道和共享内存的拷贝次数?
管道至少需要4次
共享内存至少需要2次
消息队列
是和文件脱节的,不方便整合到网络通信当中,很少用。
双方可以互为读写,进程把数据作为节点传到里面
struct text
{int type;//类型,自定义的,用来表示属于哪个线程,确保每个线程可以读到别人的数据char buffer[];//数据
}
创建或获取一个消息队列的标识符。
int msgget(key_t key, int msgflg);
从消息队列接收消息。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
对消息队列进行控制操作。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
信号量
*在线程中了解
信号
信号的意义:信号的不同代表不同的事件,但是对事件发生之后的处理方式可以一样。
信号的产生方式:
1.键盘组合热键:
ctrl+c:(SIGINT)中断前台进程。
ctrl+z:(SIGTSTP)将当前前台进程挂起并放到后台。
2.系统调用向目标进程发送信号:
1.给任意进程发送任意信号
int kill(pid_t pid, int sig);
2.给自己发送任意的信号
int raise(int sig);
3.给自己发送指定的信号SIGABRT
void abort(void);
3.硬件异常产生信号:
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
除0异常(SIGFPE),越界访问(SIGSEGV)
4.软件中断:
时钟中断
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
信号的保存:
信号的处理并不是立即进行的。当操作系统给进程发送信号后,进程可能不会立即去处理信号,而是等到合适的时候去处理 。
信号的记录通常保存在进程的进程控制块(PCB)中,特别是其中的 pending 位图。当进程从内核态返回到用户态时,操作系统会检查是否有未处理的信号,如果有未处理的信号且满足信号处理条件,就会在此时处理信号。
信号的递达:
- 实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
- 进程对信号的处理是串行的。当我们递达某一个信号期间,系统会自动屏蔽该信号,直到该信号完成捕捉,系统会解除屏蔽,如果此时pending 信号为1,会继续重复该动作,没有就结束。
信号的阻塞:
sigset_t:信号集。未决和阻塞标志可以用相同的数据类型sigset_t来存储。
对信号集的操作:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
读取或更改block信号集(信号屏蔽字):
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);how:指定
sigprocmask
函数的行为。
SIG_BLOCK
:阻塞set
中指定的信号。将这些信号添加到当前的信号屏蔽字中。SIG_UNBLOCK
:解除阻塞set
中指定的信号。从当前的信号屏蔽字中移除这些信号。SIG_SETMASK
:设置当前的信号屏蔽字为set
指定的信号集。const sigset_t *set:输入型参数。
sigset_t *oset:输出型参数。保存未修改之前的信号屏蔽字,可方便未来做恢复。
return value:若成功则为0,若出错则为-1
获取进程的pending信号集:
#include <signal.h>
int sigpending(sigset_t *set);sigset_t *set:输出型参数,当前进程的pending 信号集。
return value:若成功则为0,若出错则为-1
信号的捕捉:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
- int signo:要操作的信号。
- const struct sigaction *act:输入型参数。
- struct sigaction *oact:输出型参数,用于存储信号的旧处理动作。
struct sigaction {
void (*sa_handler)(int);//自定义处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);//null
sigset_t sa_mask;//当我们正在处理信号时,也想顺便屏蔽其他信号,就可以加入到这个sa_mask中
int sa_flags;//0
void (*sa_restorer)(void);//null
};return value:若成功则为0,若出错则为-1
void handler(int signo) {cout<<"get a signo:"<<signo<<"正在处理中"<<endl; } int main() {struct sigaction act,oact;act.sa_handler = handler;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask,3);sigaction(SIGINT,&act,&oact);while(true) sleep(1);return 0; }
![](https://i-blog.csdnimg.cn/direct/6d7605953127499dada7942ec7087223.png)
进程收到信号的三种处理模式:
- 默认
- 自定义(修改默认的处理方式)
- 忽略(我们觉得不重要所以收到了也不去做)
自定义处理函数:
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
signum:信号的编号,指定了想要捕获和处理的信号。
sighandler_t handler:typedef void (*sighandler_t)(int);
套接字
*网络模块学习
什么是线程?
线程是进程的一个执行流。
为什么需要线程?
当我们启动一个软件时,可能需要他同时执行多个功能,比如音乐软件,可能需要边下载边播放音乐,这就需要多个执行流同时运行。
线程在进程内部运行,线程在进程的地址空间内运行,拥有该进程的一部分资源。
Windows VS Linux线程的区别:
Windows:
在Windows操作系统中,每个进程都有自己的PCB,线程有独立的TCB,Windows的开发者认为进程和线程在执行流层面是不同的东西。进程有自己的执行流,线程在进程内部也有自己的执行流。因此,Windows的PCB包含了所有线程的信息,每个线程也有自己的TCB,用于跟踪和管理线程的状态、调度信息以及其他相关信息
Linux:
Linux操作系统中并没有专门为线程创建真正的数据结构来管理,而是直接复用进程的PCB当作线程的描述结构体,用PCB来当作Linux系统内部的"线程"。Linux内核不区分进程和线程,它们在内核中都被视为轻量级进程(LWP),都有各自不同的PCB。在Linux中,线程可以看作是共享进程地址空间的轻量级进程,它们有自己的PCB,但没有独立的地址空间。优点:简单,维护成本大大降低,可靠高效。复杂的就需要大量的维护。这样是Linux经常作为服务器端的原因。
站在CPU的角度每一个PCB,都可以称之为叫做轻量级进程。
ps -aL查看轻量级进程
LWP(light weight process)轻量级进程ID
PID=LWP主线程
CPU调度的时候,是以LWP为标识符标定一个执行流的。(而不是PID)
进程:
- 承担分配系统资源的基本单位。系统资源包括:CPU,内存空间(代码和数据),I/O设备。
- 进程是系统调用的基本单位。
- 进程内部可以有一个或多个线程。
线程:
- 创建线程的时候创建一个task_struct就行了。
- CPU调度的基本单位。
- 线程共享大部分资源。(共享:虚拟地址空间,内存,页表,还有文件描述符表,对信号的处理方式,用户ID和组ID。私有:PCB属性私有,私有的上下文结构(寄存器),独立的栈空间,信号屏蔽字)
进程和线程的区别:
资源分配:进程是资源分配的基本单位,拥有独立的资源,创建和销毁的开销比较大。线程共享进程的大部分资源,拥有少量的独立资源(例如独立的栈结构)。
上下文切换:进程上下文切换开销较大,因为同一个进程中的线程共享大部分资源,所以线程相对来讲上下文切换开销较小。
通信方式:进程通信比较复杂,同一个进程内的线程可以通过共享资源进行通信。
独立性和健壮性:进程是系统调用的基本单位,进程之间是独立的。信号是以进程的形式接受的,所以一个线程异常会影响整个进程,线程的健壮性和独立性比较低。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少的多:
1.进程:切换页表&&虚拟地址空间&&切换PCB&&切换上下文
2.线程:切换PCB&&切换上下文
3.线程切换cache不用太更新,但是进程切换,全部更新。
![]()
数据从Cache加载到寄存器,然后CPU从寄存器中读取数据进行处理。
速度上:寄存器>缓存>内存由于CPU直接从内存读取数据太慢了,Cache被用来作为CPU和内存之间的缓冲。CPU首先检查Cache,如果所需数据在Cache中(Cache命中),则可以直接快速访问;如果不在(Cache未命中),则需要从内存中加载数据到Cache,这比直接从内存访问要快。由于局部性原理,程序倾向于访问临近的数据。Cache利用这一原理,通过预加载临近的数据项来提高效率。如果程序访问的数据符合局部性原理,Cache的效率会更高。对于线程切换来讲,他们用的都是同一个虚拟地址空间,热点数据几乎差不多,所以不用切换太多。但是对于进程来讲,就需要全部切换。
计算密集型应用(CPU,加密,解密,算法)vs I/O密集型应用(外设,访问磁盘,显示器,网络)
线程库:Linux操作系统中并没有专门为线程创建真正的数据结构来管理,而是直接复用进程的PCB当作线程的描述结构体,用PCB来当作Linux系统内部的"线程"。所以Linux无法直接提供创建线程的系统调用接口,只能给我们提供创建轻量级进程的接口,但是操作系统,程序员只认线程。所以在用户和系统之间提供了一个软件封装,提供了一个用户级线程库libpthread,任何Linux操作系统,都必须默认携带这个库(原生线程库)。
接口:
线程创建:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
pthread_t *thread
:输入输出型参数,创建成功返回线程ID。
const pthread_attr_t *attr
:线程属性,一般设置为NULL,表示使用默认的线程属性。
void *(*start_routine) (void *)
:传入线程执行流要执行的函数。
void *arg
:这是传递给start_routine
函数的参数。return_val:
成功返回0 ,失败返回错误编号。
线程终止:
(1)线程函数结束,return终止(建议)
(2)pthread_exit()函数(用在线程中)
void pthread_exit(void *retval);
(3)线程取消pthread_cancel()(用在主线程)线程取消的前提是线程已经跑起来了。
int pthread_cancel(pthread_t thread);
线程等待:
不等待,会造成内存泄漏。
- 回收新线程的退出信息。
- 回收新线程对应的PCB等内核资源,防止内存泄漏。
int pthread_join(pthread_t thread, void **retval);
pthread_t thread
:这是一个输入参数,指定了需要等待结束的线程的标识符,即线程ID。
void **retval
:这是一个输出参数,用于存储被等待线程的返回值。如果被等待的线程有返回值,那么这个值会被存储在retval
指向的内存地址中。如果不需要获取返回值,可以将这个参数设置为NULL
。
线程分离:
当你不需要从线程中获取返回值,或者不需要等待线程结束时,可以使用
pthread_detach
来自动回收线程资源。int pthread_detach(pthread_t tid);
了解:
线程安全
线程安全:多个线程并发执行同一段代码,一会出现不同的结果叫做线程安全,否则就是线程不安全。多线程通过共享资源进行通信,这就造成了线程安全问题。
线程不安全的情况:
- 不保护共享变量,静态变量,调用线程不安全的函数
线程安全的情况:
- 对全局变量只有读取而没有写入权限
- 局部变量是安全的,因为线程有独立栈结构
对一个全局变量进行多线程更改是安全的吗?
对变量进行++或者--,在C,C++上,看起来只有一条语句,但是汇编至少是三条语句:
例如在一个抢票系统中,抢票线程AB同时运行。
假设此时车票ticket=1。
线程A抢到CPU时间片,从内存中取出1放到寄存器,对寄存器中的1--;
这个时候突然CPU时间被线程B抢占,线程A只能带上下文先离开;
此时A的上下文ticket=0;内存中的ticket=1;
线程B执行操作,从内存中取出1放到寄存器,对寄存器中的1--,这个时候寄存器中的数据由1->0,然后将寄存器中数据写入到内存中。
此时内存中的ticket=0;
线程A继续抢到时间片,然后执行第三部,将寄存器中的数据0写入到内存中,此时内存中的数据本来就是0,但是这个A却又卖了一张票。
多个线程在交替执行造成的数据安全问题,我们叫做数据不一致问题。
解决方案:
锁
临界资源:多个执行流进行安全访问的共享资源
临界区:多个执行流中访问临界资源的代码
互斥:多线程串行访问共享资源
原子性:对一个资源进行访问,要么不做,要么做完(如果对一个资源进行操作只需要一条语句)
注意:
加锁的粒度一定要非常小,提高效率。
谁持有锁,谁进入临界区,即便切换线程了别的线程也进不去。
加锁的过程是原子的。
按实现方法分类:
互斥锁:
当一个线程尝试获取互斥锁时,如果锁已经被其他线程持有,它会进入睡眠状态,直到锁被释放。
适用场景:适用于锁的持有时间较长或锁竞争激烈的场景,例如复杂的资源同步
优点:
-
资源节省:线程在等待锁时不会占用 CPU 资源,适用于锁竞争激烈或锁持有时间较长的场景。
-
适用于长时间锁持有:互斥锁可以有效地管理长时间的锁持有,避免资源浪费。
初始化销毁:
如果这个锁是全局的或静态的:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
如果这个锁是局部的:
pthread_mutex_t mutex;//这把锁mutex叫做互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
临界资源;
int pthread_mutex_unlock(pthread_mutex_t *mutex);
自旋锁:
C++11 标准库中没有直接提供自旋锁的实现。
当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,它不会进入睡眠状态,而是不断循环检查锁的状态,直到锁被释放。
适用场景:适用于锁的持有时间非常短的场景,例如简单的原子操作或快速的数据访问。
优点:
- 低延迟:由于线程不会进入睡眠状态,自旋锁可以减少线程上下文切换的开销,适用于锁竞争较短的场景。
- 适用于短时间锁持有:当锁的持有时间很短时,自旋锁可以避免不必要的线程上下文切换。
初始化销毁:
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
加锁解锁:
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
读写锁:
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源.
优点:
- 提高了读取操作的并发性能
- 适用于读多写少的场景
初始化销毁:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁:
读锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
写锁:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
按实现策略分类:
*悲观锁:
互斥锁、自旋锁、读写锁,都是属于悲观锁。悲观锁认为并发访问共享资源时,冲突概率可能非常高,所以在访问共享资源前,都需要先加锁,当其他线程想要访问数据时,被阻塞挂起。
*乐观锁:
如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁。乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。
工作方式:访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
主要采用两种方式:版本号机制和CAS操作。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
死锁:
现象:在多把锁的情况下,我们持有自己锁不释放,还要对方的锁,对方也是不释放,此时就容易造成死锁。
死锁的4个必要条件:
- 互斥:锁的互斥性,只有拿到锁的线程才可以进入临界区
- 请求与保持:我请求其他锁,保持自己锁不释放
- 不剥夺:不可以抢占其他线程的锁
- 环路等待条件:形成环路等待
如何避免死锁?
- 破坏死锁的四个必要条件
设置优先级抢占——>破坏不剥夺- 加锁顺序一致——>破坏环路等待
- 避免锁未释放的场景——>破坏请求与保持
- 资源一次性分配
避免死锁算法:(了解)
- 死锁检测算法
- 银行家算法
条件变量
条件变量:条件变量是一种同步机制,它允许线程在某个条件不满足时挂起(阻塞),直到其他线程修改了条件并通知条件变量。
条件变量是一种类型,我们使用的时候定义对象。
初始化销毁:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);等待/申请资源(P):
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
该函数调用的时候,会以原子的方式将锁释放,并将自己挂起
该函数在被唤醒的时候,会重新自动获取你传入的锁
唤醒/释放资源(V):
int pthread_cond_signal(pthread_cond_t *cond);
信号量
什么是信号量?
信号量本质是一把衡量临界资源中资源数量多少的计数器
只要拥有信号量,就在未来一定能够拥有临界资源的一部分
申请信号量的本质:对临界资源中特定小块资源的预定机制
目的:在访问真正的临界资源之前,就提前知道临界资源的使用情况
#include <semaphore.h>
初始化:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁:
int sem_destroy(sem_t *sem);
申请资源(P):
申请成功,继续向下运行
申请失败,阻塞在申请处
等待信号量,信号量--
int sem_wait(sem_t *sem);
释放资源(V):
发布信号量,信号++
int sem_post(sem_t *sem);
可重入函数(了解)
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下不会出现任何不同或任何问题,我们叫他可重入函数,否则叫不可重入函数。
不可重入:
- 调用了malloc/free函数
- 使用全局变量或静态变量
- 调用不可重入函数
- 返回全局或静态数据
线程安全和可重入关系不大。重入是描述函数的特点,线程安全是描述线程。
可重入函数时线程安全函数的一种,线程安全不一定是可重入的,可重入的函数一定是线程安全的。
生产消费模型:
关键字:锁,条件变量
假设我们有一个餐厅,餐厅的厨房(生产者)负责制作食物,而顾客(消费者)负责享用这些食物。餐厅的服务员和传菜系统可以看作是缓冲区,负责将厨房制作的食物(数据)传递给顾客。
生产者和生产者的关系:两道菜(数据)不能放到同一个盘子(空间)里。
消费者和消费者的关系:两个顾客不能同时拿同一份食物(数据)。
生产者和消费者的关系:厨师(生产者)开始制作食物。当一道菜制作完成,厨师将其放在传菜区,并通知服务员有新的食物准备好了。服务员(缓冲区)检查传菜区是否有食物,如果有,就将其端给等待的顾客(消费者)。顾客享用食物,当传菜区空了,服务员会通知厨师可以继续制作新的食物。
![](https://i-blog.csdnimg.cn/direct/ab4ebd8512c04275943d51691c689765.png)
“321”原则:
3种关系:
生产者和生产者:互斥
消费者和消费者:互斥
生产者和消费者:互斥+同步
2种角色:
生产者线程,消费者线程
1个交易场所:
缓冲区
代码:
main.cc
#include<iostream>
#include<pthread.h>
#include<stdlib.h>
#include<memory>
#include"blockqueue.h"
using namespace std;
void* product(void* args)
{blockQueue<int>* bq = static_cast<blockQueue<int>*>(args);srand(time(NULL));while(1){bq->push(rand()%100);}
}
void* consumer(void* args)
{blockQueue<int>* bq = static_cast<blockQueue<int>*>(args);while(1){bq->pop();}
}
int main()
{blockQueue<int>* bq = new blockQueue<int>();pthread_t productid[5];pthread_t consumerid[5];for(int i = 0;i<5;i++){pthread_create(&productid[i],nullptr,product,(void*)bq);pthread_create(&consumerid[i],nullptr,consumer,(void*)bq);}for(int i = 0;i<5;i++){pthread_join(productid[i],nullptr);pthread_join(consumerid[i],nullptr);}delete bq;
}
blockqueue.h
#include <iostream>
#include <queue>
#include<unistd.h>
#include <pthread.h>
#define MAXSIZE 5
template<class T>
class blockQueue
{
public:blockQueue():_size(MAXSIZE){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_Pcond,nullptr);pthread_cond_init(&_Ccond,nullptr);}void push(const T& t){pthread_mutex_lock(&_mutex);while(full()){pthread_cond_wait(&_Pcond,&_mutex);}_bq.push(t);std::cout<<pthread_self()<<"push :"<<t<<std::endl;pthread_cond_signal(&_Ccond);pthread_mutex_unlock(&_mutex);}void pop(){pthread_mutex_lock(&_mutex);while(empty()){pthread_cond_wait(&_Ccond,&_mutex);}std::cout<<pthread_self()<<"pop :"<<_bq.front()<<std::endl;_bq.pop();pthread_cond_signal(&_Pcond);pthread_mutex_unlock(&_mutex);}bool empty(){return _bq.empty();}bool full(){return _bq.size()==_size?1:0;}~blockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_Pcond);pthread_cond_destroy(&_Ccond);}private:std::queue<T> _bq;int _size;pthread_mutex_t _mutex;pthread_cond_t _Pcond;pthread_cond_t _Ccond;
};
环形队列模型:
关键字:信号量
生产者和消费者的位置:队列中的下标,为空或者为满时下标相同。其他时候都是在访问不通的下标。
在环形队列中,大部分情况单生产和单消费时可以并发执行的,只有为空或为满时才有互斥和同步的问题。对于生产者而言看重的是space的数量,对于消费者而言看重的是data的数量。
代码:
main.cc
#include "ringqueue.hpp"
#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include<unistd.h>
#include <memory>
using namespace std;
void *product(void *args)
{ringqueue<int> *rq = static_cast<ringqueue<int> *>(args);while (1){int n = rand() % 100;rq->push(n);cout<<"生产了一个数据:"<<n<<endl;}
}
void *consumer(void *args)
{ringqueue<int> *rq = static_cast<ringqueue<int> *>(args);while (1){int n = rq->pop();cout<<"消耗了一个数据:"<<n<<endl;sleep(1);}
}
int main()
{srand(time(NULL));ringqueue<int> *rq = new ringqueue<int>();pthread_t productid;pthread_t consumerid;pthread_create(&productid, nullptr, product, (void *)rq);pthread_create(&consumerid, nullptr, consumer, (void *)rq);pthread_join(productid, nullptr);pthread_join(consumerid, nullptr);delete rq;
}
ringqueue.hpp
#pragma once
#include<iostream>
#include<vector>
#include <semaphore.h>
static const int MAXSIZE = 5;
template<class T>
class ringqueue{
public:ringqueue():_queue(MAXSIZE),_size(MAXSIZE){sem_init(&_ssem,0,_size);sem_init(&_dsem,0,0);_datastep = 0;_spacestep = 0;}void push(const T& t){sem_wait(&_ssem);_queue[_datastep++] = t;//先访问元素再++_datastep%=_size;sem_post(&_dsem);}int pop(){sem_wait(&_dsem);int n = _queue[_spacestep++];_spacestep%=_size;sem_post(&_ssem);return n;}~ringqueue(){sem_destroy(&_ssem);sem_destroy(&_dsem);}private:std::vector<T> _queue;int _size;sem_t _ssem;sem_t _dsem;int _datastep;int _spacestep;
};
读者写者模式:
关键字:读写锁
读者-写者模式是一种用于解决多线程环境中对共享资源访问问题的同步机制。它允许多个线程(读者)同时读取共享资源,但只允许一个线程(写者)在任何时候修改共享资源。
规则:
- 多个线程(读者)可以同时读取共享资源
- 只允许一个写者在任何时候修改共享资源,在写者写入数据期间,其他读者和写者都不能访问共享资源
- 避免写者饥饿,可以通过适当的调度策略来避免写者饥饿,例如设置写者优先.
321原则:
“3”种关系:
写者和写者:互斥
读者和写者:互斥且同步
读者和读者:没关系
“2”种角色:
读者线程写者线程
“1”个交易场所:
数据结构
线程池
关键字:并发,多线程
什么是线程池?以及线程池的作用?
线程池是一种并发编程技术,用于管理和优化多线程程序中的线程资源。它通过创建一个线程集合来处理多个任务,而不是为每个任务单独创建和销毁线程。
好处:
- 当任务到达时,线程池可以立即分配一个空闲线程来处理任务,而不需要等待线程的创建,从而提高了任务的响应速度.
- 频繁地创建和销毁线程会消耗大量的系统资源和时间。线程池通过重用一组线程来处理多个任务,从而减少了线程创建和销毁的开销.
应用:
-
Web服务器:处理客户端的并发请求.
-
数据库连接池:管理数据库连接的创建和释放.
-
文件下载:同时下载多个文件.
-
网络通信:处理多个网络连接的并发通信.
代码
threadpool.hpp
#pragma once
#include "thread.hpp"
#include <queue>
#include <vector>#define threadsize 5
template <class T>
class threadpool;template <class T>
class ThreadData
{
public:ThreadData(threadpool<T> *tp, char *threadname) : _tp(tp), _threadname(threadname) {}threadpool<T> *_tp;char *_threadname;
};template <class T>
class threadpool
{
public:threadpool() : _size(threadsize){pthread_mutex_init(&_mutex, 0);pthread_cond_init(&_cond, 0);// 创建线程for (int i = 0; i < _size; i++){_threads.push_back(new thread());}}void run(){std::cout << "启动线程" << _threads.size() << std::endl;// 启动线程for (thread *e : _threads){e->start(routine, (void *)new ThreadData<T>(this, e->getname()));}}void push(const T &t){pthread_mutex_lock(&_mutex);std::cout << "push:" << t << std::endl;_tasks.push(t);pthread_cond_signal(&_cond);pthread_mutex_unlock(&_mutex);}static void *routine(void *args){ThreadData<T> *td = static_cast<ThreadData<T> *>(args);while (1){pthread_mutex_lock(&td->_tp->_mutex);if (td->_tp->_tasks.empty()){pthread_cond_wait(&td->_tp->_cond, &td->_tp->_mutex);}T t = td->_tp->_tasks.front();td->_tp->_tasks.pop();pthread_mutex_unlock(&td->_tp->_mutex);std::cout << td->_threadname << "pop" << t << std::endl;delete td;}}~threadpool(){for (thread *e : _threads){e->join();//销毁线程}pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:std::queue<T> _tasks;std::vector<thread *> _threads;pthread_mutex_t _mutex;pthread_cond_t _cond;int _size;
};
thread.hpp
#pragma once
#include <pthread.h>
#include <iostream>
#include <stdio.h>
#include <functional>
#include <assert.h>
static int number = 0;
#define SIZE 100
typedef void *(*func_t)(void *);
class thread
{
public:thread(){snprintf(_threadname, sizeof(_threadname), "%d-thread", number++);}void start(func_t routine, void *args){int n = pthread_create(&_pid, 0, routine, (void *)args);assert(n == 0);std::cout << _threadname << "创建成功" << std::endl;}void join(){pthread_join(_pid, nullptr);}char* getname(){return _threadname;}~thread() {}private:char _threadname[SIZE];pthread_t _pid;
};
main.cc
#include<iostream>
#include<memory>
#include<ctime>
#include<unistd.h>
#include"threadpool.hpp"
using namespace std;
int main()
{unique_ptr<threadpool<int>> tp(new threadpool<int>());tp->run();srand(time(0));while(1){tp->push(rand()%100);sleep(1);}}
单例设计模式
什么是单例模式?单例模式的特点?
单例设计模式(Singleton Pattern) 是一种创建型设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
单例模式无法被回收,他的生命周期随进程,可以手动释放或者使用智能指针进行管理。
饿汉模式
饿汉模式:饿汉非常简单,静态方法和全局变量在加载到内存时就全部加载出来,缺陷是如果很大启动时就会花很长时间。
class Singleton {
private:static Singleton instance; // 静态实例Singleton() {} // 私有构造函数// 禁止拷贝构造函数和赋值操作符,防止拷贝Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:static Singleton* getInstance() {return &instance; // 直接返回静态实例的地址}
};// 在类外部初始化静态实例
Singleton Singleton::instance;
懒汉模式
懒汉模式:懒汉的核心思想是“延迟加载”,使用双重检查锁确保线程安全和同步开销。例如malloc和new申请空间时,操作系统会“延迟开辟”空间,如果当时就开始会导致申请的速度变慢,地址空间内存变少真正有需要的进程就没法开辟空间了。
#include <mutex>class Singleton {
private:static Singleton* instance; // 静态实例指针static std::mutex mutex; // 互斥锁,用于线程同步Singleton() {} // 私有构造函数// 禁止拷贝构造函数和赋值操作符,防止拷贝Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:static Singleton* getInstance() {if (instance == nullptr) { // 双重检查锁定(Double-Checked Locking)std::lock_guard<std::mutex> lock(mutex); // 加锁if (instance == nullptr) { // 再次检查实例是否已创建instance = new Singleton(); // 如果未创建,则创建实例}}return instance; // 返回实例的地址}
};// 在类外部初始化静态实例指针和互斥锁
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
Meyer's Singleton
使用静态局部变量实现的单例模式。
优点:
静态局部变量的初始化是线程安全的。编译器会确保静态局部变量只被初始化一次,即使多个线程同时调用 getInstance()
。同时避免了资源浪费。实现简单,是现代 C++ 中推荐的单例实现方式。
class Singleton {
private:Singleton() {} // 私有构造函数Singleton(const Singleton&) = delete; // 禁止拷贝构造Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作public:static Singleton& getInstance() {static Singleton instance; // 静态局部变量return instance;}void doSomething() {std::cout << "Singleton is doing something!" << std::endl;}
};