文章目录
- C++和C的区别
- extern“C”
- C++11中的可变参数模板、右值引用和lambda这几个新特性
- C++源文件从文本到可执行文件经历的过程?
- include头文件的顺序以及双引号””和尖括号<>的区别?
- new和malloc的区别
- malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?
- C++函数栈空间的最大值
- C语言是怎么进行函数调用的?
- C++如何处理返回值?
- C语言参数压栈顺序?
- C++的内存管理是怎样的?
- 什么是内存泄露?如何判断内存泄漏?
- 什么时候会发生段错误
- 共享内存相关api
- reactor模型组成
- 设计一下如何采用单线程的方式处理高并发
- strcpy和strlen
- 字符串拷贝strcpy函数
- 检查代码题常出的错误
- 不同类型if与零值比较
- sizeof
- 宏
- string类的编写
- C++中拷贝赋值函数的形参能否进行值传递?
- C++中类成员的访问权限
- C++中struct和class的区别
- C++中析构函数的作用
- 为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数
- 虚函数和多态
- 重载和重写
- static关键字作用:
- 虚函数表具体是怎样实现运行时多态的?
- 请你来说一下静态函数和虚函数的区别
- C++里是怎么定义常量的?常量存放在内存的哪个位置?
- const修饰成员函数的目的是什么?
- 两个函数,一个带const,一个不带,会有问题吗?
- const关键字作用:
- 隐式类型转换
- c++中四种cast转换
- RTTI
- 指针和引用的区别
- c++中的smart pointer四个智能指针
- fork函数的作用
- fork,wait,exec函数
- map和set有什么区别,分别又是怎么实现的?
- STL有什么基本组成
- STL中map与Multimap
- vector和list的区别,应用,越详细越好
- STL中迭代器的作用,有指针为何还要迭代器
- STL的allocaotr
- STL里resize和reserve的区别
- 写个函数在main函数执行前先运行
- STL迭代器删除元素
- epoll原理
C++和C的区别
设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言
语法上:
C++具有封装、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等
extern“C”
C++调用C函数需要extern C,因为C语言没有函数重载。
作用:
1.使用本文件之外的文件中定义的全局变量
2.在c++中使用c语言定义的函数
C++11中的可变参数模板、右值引用和lambda这几个新特性
可变参数模板:
C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。
例如:
Template<class ... T>
void func(T ... args)
{
cout<<”num is”<<sizeof ...(args)<<endl;
}
func();//args不含任何参数
func(1);//args包含一个int类型的实参
func(1,2.0)//args包含一个int一个double类型的实参
其中T叫做模板参数包,args叫做函数参数包
省略号作用如下:
1)声明一个包含0到任意个模板参数的参数包
2)在模板定义得右边,可以将参数包展成一个个独立的参数
C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。例如:
#include using namespace std;
// 最终递归函数
void print()
{
cout << "empty" << endl;
}
// 展开函数
template void print(T head, Args... args)
{
cout << head << ","; print(args...);
}
int main()
{
print(1, 2, 3, 4); return 0;
}
参数包Args …在展开的过程中递归调用自己,没调用一次参数包中的参数就会少一个,直到所有参数都展开为止。当没有参数时就会调用非模板函数printf终止递归过程。
右值引用:
C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,没有名字的就是右值。而在指C++11中,右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。
C++11中,右值引用就是对一个右值进行引用的类型。由于右值通常不具有名字,所以我们一般只能通过右值表达式获得其引用,比如:
T && a=ReturnRvale();
假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
基于右值引用可以实现转移语义和完美转发新特性。
移动语义:
对于一个包含指针成员变量的类,由于编译器默认的拷贝构造函数都是浅拷贝,所有我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。
但是如下列代码所示:
当类HasPtrMem包含一个成员函数GetTemp,其返回值类型是HasPtrMem,如果我们定义了深拷贝的拷贝构造函数,那么在调用该函数时需要调用两次拷贝构造函数。第一次是生成GetTemp函数返回时的临时变量,第二次是将该返回值赋值给main函数中的变量a。与此对应需要调用三次析构函数来释放内存。
而在上述过程中,使用临时变量构造a时会调用拷贝构造函数分配对内存,而临时对象在语句结束后会释放它所使用的堆内存。这样重复申请和释放内存,在申请内存较大时会严重影响性能。因此C++使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。
如下列代码所示,移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量。
其原理就是使用在构造对象a时,使用h.d来初始化a,然后将临时对象h的成员变量d指向nullptr,从而保证临时变量析构时不会释放对内存。
完美转发:
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数,即传入转发函数的是左值对象,目标函数就能获得左值对象,转发函数是右值对象,目标函数就能获得右值对象,而不产生额外的开销。
因此转发函数和目标函数参数一般采用引用类型,从而避免拷贝的开销。其次,由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型。
C++11采用引用折叠的规则,结合新的模板推导规则实现完美转发。其引用折叠规则如下:
因此,我们将转发函数和目标函数的参数都设置为右值引用类型,
当传入一个X类型的左值引用时,转发函数将被实例为:
经过引用折叠,变为:
当传入一个X类型的右值引用时,转发函数将被实例为:
经过引用折叠,变为:
除此之外,还可以使用forward()函数来完成左值引用到右值引用的转换:
Lambda表达式:
Lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量,其定义如下:
capturemutable->return-type{statement}
其中,
[capture]:捕获列表,捕获上下文变量以供lambda使用。同时[]是lambda寅初复,编译器根据该符号来判断接下来代码是否是lambda函数。
(Params):参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。
mutable是修饰符,默认情况下lambda函数总是一个const函数,Mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略。
->return-type:返回类型是返回值类型
{statement}:函数体,内容与普通函数一样,除了可以使用参数之外,还可以使用所捕获的变量。
Lambda表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。其形式如下:
Lambda的类型被定义为“闭包”的类,其通常用于STL库中,在某些场景下可用于简化仿函数的使用,同时Lambda作为局部函数,也会提高复杂代码的开发加速,轻松在函数内重用代码,无须费心设计接口。
C++源文件从文本到可执行文件经历的过程?
对于C++源文件,从文本到可执行文件一般需要四个过程:
**预处理阶段:**对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
**编译阶段:**将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
**链接阶段:**将多个目标文件及所需要的库连接成最终的可执行目标文件
一.四个步骤
对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:
1).预处理,产生.ii文件
2).编译,产生汇编文件(.s文件)
3).汇编,产生目标文件(.o或.obj文件)
4).链接,产生可执行文件(.out或.exe文件)
以hello.c为例,这个过程可以用下面的图来表示
二.预处理
预处理主要包含下面的内容:
a.对所有的“#define”进行宏展开;
b.处理所有的条件编译指令,比如“#if”,“#ifdef”,“#elif”,“#else”,“#endif”
c.处理**“#include”指令,这个过程是递归的,也就是说被包含的文件可能还包含其他文件
d.删除所有的注释“//”和“//”
e.添加行号和文件标识
f.保留所有的“#pragma”编译器指令
经过预处理后的.ii文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.ii文件中。
三.编译
编译的过程就是将预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件(.s文件)
四.汇编
汇编器是将汇编代码转变成机器可以执行的代码,每一个汇编语句几乎都对应一条机器指令。最终产生目标文件(.o或.obj文件)。
五.链接
链接的过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)
原文链接:https://blog.youkuaiyun.com/sheng_ai/article/details/47860403
include头文件的顺序以及双引号””和尖括号<>的区别?
Include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
当前头文件目录
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
#include <>格式:引用标准库头文件,编译器从标准库目录开始搜索
#incluce ""格式:引用非标准库的头文件,编译器从用户的工作目录开始搜索
预处理器发现 #include 指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的#include指令,就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。
#include 指令有两种使用形式
#include <stdio.h> 文件名放在尖括号中
#include “mystuff.h” 文件名放在双引号中
尖括号< 和> 括起来表明这个文件是一个工程或标准头文件。查找过程会检查预定义的目录,我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录。
如果文件名用一对引号括起来则表明该文件是用户提供的头文件,查找该
文件时将从当前文件目录(或文件名指定的其他目录)中寻找文件,然后再在标准位置寻找文件。
原文链接:https://blog.youkuaiyun.com/finded/article/details/50478885
new和malloc的区别
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,**malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。**当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
C++函数栈空间的最大值
默认是1M,不过可以调整
C语言是怎么进行函数调用的?
每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
C++如何处理返回值?
生成一个临时变量,把它的引用作为函数参数传入函数内。
C语言参数压栈顺序?
从右到左
C++的内存管理是怎样的?
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:
各个段说明如下:
3G用户空间和1G内核空间
静态区域:
text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
data segment(数据段):存储程序中已初始化的全局变量和静态变量
**bss segment:**存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0
动态区域:
heap(堆): 当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。
memory mapping segment(映射区):存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)
stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。
什么是内存泄露?如何判断内存泄漏?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的分类:
1. 堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
- 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3. 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
什么时候会发生段错误
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
使用野指针
试图修改字符串常量的内容
产生段错误就是访问了错误的内存段,一般是你没有权限,或者根本就不存在对应的物理内存,尤其常见的是访问0地址。
一般来说,段错误就是指访问的内存超出了系统所给这个程序的内存空间,通常这个值是由gdtr来保存的,它是一个48位的寄存器,其中的32位是保 存由它指向的gdt表,后13位保存相应于gdt的下标,最后3位包括了程序是否在内存中以及程序在cpu中的运行级别,指向的gdt是以64位为一个单 位的表,在这张表中就保存着程序运行的代码段、数据段的起始地址、与此相应的段限和页面交换、程序运行级别还有内存粒度等等的信息。一旦一个程序发生了越 界访问,cpu就会产生相应的异常保护,于是segmentation fault就出现了.
在编程中以下几类做法容易导致段错误,基本是是错误地使用指针引起的:
1)访问系统数据区,尤其是往系统保护的内存地址写数据,最常见的就是给一个指针以0地址;
2)内存越界(数组越界,变量类型不一致等) 访问到不属于你的内存区域。
另外,缓存溢出也可能引起“段错误”,对于这种while(1) {do}的程序,这个问题最容易发生,多此sprintf或着strcat有可能将某个buff填满,溢出,所以每次使用前,最好memset一下,不过要是一开始就是段错误,而不是运行了一会儿出现的,缓存溢出的可能性就比较小。
共享内存相关api
Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。
1)新建共享内存shmget
int shmget(key_t key,size_t size,int shmflg);
key:共享内存键值,可以理解为共享内存的唯一性标记。
size:共享内存大小
shmflag:创建进程和其他进程的读写权限标识。
返回值:相应的共享内存标识符,失败返回-1
2)连接共享内存到当前进程的地址空间shmat
void *shmat(int shm_id,const void *shm_addr,int shmflg);
shm_id:共享内存标识符
shm_addr:指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。
shmflg:标志位
返回值:指向共享内存第一个字节的指针,失败返回-1
3)当前进程分离共享内存shmdt
int shmdt(const void *shmaddr);
4)控制共享内存shmctl
和信号量的semctl函数类似,控制共享内存
int shmctl(int shm_id,int command,struct shmid_ds *buf);
shm_id:共享内存标识符
command: 有三个值
IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。
IPC_RMID:删除共享内存
buf:共享内存管理结构体。
共享内存区是最快的IPC(进程间通信)形式。
用共享内存从服务器拷贝文件数据到客户端:
共享内存基本API:
#include<sys/ipc.h>
#include<sys/shm.h>
1. int shmget(key_t key,size_t size,int shmflg);
功能:用来创建共享内存
key:是这个共享内存段的名字
size:共享内存的大小
shmflg:相当于权限位(如0666)
返回值是共享内存段的标识码shmid,
例如:shmid = shmget(0x1111, 128, 0666);
//创建共享内存 , 相当于打开打开文件
//若共享内存存在 则使用 fopen()
//若共享内存 不存在 则报错 -1
shmid = shmget(0x1111, 128, 0666 | IPC_CREAT);
//创建共享内存 , 相当于打开打开文件
//若共享内存存在 则使用 fopen()
//若共享内存 不存在 则创建
shmid = shmget(0x1111, 128, 0666 | IPC_CREAT | IPC_EXCL);
//创建共享内存 , 相当于打开文件
//若共享内存存在 则报错
//若共享内存 不存在 则创建
//作用 IPC_EXCL判断存在不存在的标志 避免已经存在的文件 被覆盖
**2. void shmat(int shmid, const void shmaddr, int shmflg);0xaa11
功能:将共享内存段连接到进程地址空间
shmaddr:指定连接的地址,因为内存地址是段页式管理,所以有可能传入的地址并不就是那一页的开头位置,所以传入一个地址,传出的仍然是一个地址,传出的是具体开始存储的地址。所以我们通常传入NULL,让编译器直接分配个合适的位置给我们。
shmflg:它的两个取值可能是SHM_RND和SHM_RDONLY.
例: void *p = shmat(shmid, NULL, 0);
返回值:成功返回一个指针,指向共享内存第一个节,失败返回-1;
*3, int shmdt(const void shmaddr);
功能:将共享内存段与当前进程脱离,但并不等于删除共享内存段
*4, int shmctl(int shmid,int cmd,struct shmid_ds buf);
功能:用于控制共享内存
cmd:将要采取的动作
1,IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值
2,IPC_SET 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
3,IPC_RMID 删除共享内存段
buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构
例: shmctl(shmid, IPC_RMID, NULL);
//删除共享内存
若想要把旧的共享内存里面的内容保存下来,则传入一个地址,用来完成保存的功能
为什么链接共享内存时要设计shmid,创建时要传入key:
共享内存私有:
reactor模型组成
一
从这个描述中,我们知道Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。如果用图来表达:
从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。
二
reactor模型要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据、接受新的连接以及处理客户请求均在工作线程中完成。其模型组成如下:
1)**Handle:**即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。
2)Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。
3)Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。
4)Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
5)Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。
三
1、标准定义
两种I/O多路复用模式:Reactor和Proactor
一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。
在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。
而在Proactor模式中,处理器–或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。
举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。
在Reactor中实现读:
-
注册读就绪事件和相应的事件处理器
-
事件分离器等待事件
-
事件到来,激活分离器,分离器调用事件对应的处理器。
-
事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
在Proactor中实现读: -
处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
-
事件分离器等待操作完成事件
-
在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
-
事件分离器呼唤处理器。
-
事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。
可以看出,两个模式的相同点,都是对某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或已经完成)。在结构上,两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;不同点在于,异步情况下(Proactor),当回调handler时,表示IO操作已经完成;同步情况下(Reactor),回调handler时,表示IO设备可以进行某个操作(can read or can write)。
2、通俗理解
使用Proactor框架和Reactor框架都可以极大的简化网络应用的开发,但它们的重点却不同。
Reactor框架中用户定义的操作是在实际操作之前调用的。比如你定义了操作是要向一个SOCKET写数据,那么当该SOCKET可以接收数据的时候,你的操作就会被调用;而Proactor框架中用户定义的操作是在实际操作之后调用的。比如你定义了一个操作要显示从SOCKET中读入的数据,那么当读操作完成以后,你的操作才会被调用。
Proactor和Reactor都是并发编程中的设计模式。在我看来,他们都是用于派发/分离IO操作事件的。这里所谓的IO事件也就是诸如read/write的IO操作。"派发/分离"就是将单独的IO事件通知到上层模块。两个模式不同的地方在于,Proactor用于异步IO,而Reactor用于同步IO。
3、备注
其实这两种模式在ACE(网络库)中都有体现;如果要了解这两种模式,可以参考ACE的源码,ACE是开源的网络框架,非常值得一学。。
设计一下如何采用单线程的方式处理高并发
一
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来
二
(1)系统拆分,将一个系统拆分为多个子系统,用dubbo来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以抗高并发么。
(2)缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家redis轻轻松松单机几万的并发啊。没问题的。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。
(3)MQ,必须得用MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用redis来承载写那肯定不行,人家是缓存,数据随时就被LRU了,数据格式还无比简单,没有事务支持。所以该用mysql还得用mysql啊。那你咋办?用MQ吧,大量的写请求灌入MQ里,排队慢慢玩儿,后边系统消费后慢慢写,控制在mysql承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用MQ来异步写,提升并发性。MQ单机抗几万并发也是ok的,这个之前还特意说过。
(4)分库分表