栈由操作系统分配释放,用于存放函数的参数值、局部变量等,栈中存储的数据的生命周期随着函数的执行完成而结束。
堆由开发人员分配和释放,若开发人员不释放,程序结束时由操作系统回收。
区别:
**(1)管理方式不同。**栈由操作系统分配释放,无需我们手动控制。堆的申请和释放工作由程序员控制,容易产生内存泄露。
**(2)空间大小不同。**每个进程拥有的栈大小要远远小于堆大小。
**(3)生长方向不同。**堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
**(4)分配方式不同。**堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca()
函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。
**(5)分配效率不同。**栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
**(6)存放内容不同。**栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。
定义:栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变。
发生栈溢出的情况:
(1)最常见的就是递归。每次递归就相当于调用一个函数,函数每次调用时都会将局部数据放入栈中。递归10000次,就将10000份这样的数据放入栈中。然而只有当递归结束时,这些数据占用的内存才会被释放。如果递归次数过多,并且局部数据也多,那么将占用大量的栈内存,很容易造成栈溢出。
(2)在函数内部定义超大数组也会导致栈溢出。
解决方法:
(1)不要静态分配,用new动态创建,从堆中分配,堆的空间足够大
(2)改变默认栈的空间大小
可以嵌套调用函数,不能嵌套定义函数,嵌套调用函数也容易导致栈溢出。
不能直接用==判断
所以判断浮点数是否相等的常用方法是:
取两数差值的绝对值判断其是否在某一范围内(作差)
定义:
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
C++中的内存泄露,总的来说,就是new出来的内存没有通过delete合理的释放掉!
后果:
从性能不良(并且逐渐降低)到内存完全用尽。
更糟的是,泄漏的程序可能会用掉太多内存,以致另一个程序失败,而使用户无从查找问题的真正根源。
此外,即使无害的内存泄漏也可能是其他问题的征兆。
内存泄露类型:
(1)堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
(2)系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
解决方法:
智能指针!!!
因为智能指针可以自动删除分配的内存
多态,就是指当完成某个行为时,不同的对象去完成会产生出不同的状态。(定义)
多态分为静态多态和动态多态。
静态多态主要指编译时期的多态,例如函数重载和模板。
动态多态是指运行时期的多态,比如虚函数。
比如,将基类中的类成员函数定义成一个虚函数,在派生类中可以定义这个函数的不同实现,然后通过基类的指针或者引用指向派生类对象。
出现了虚函数,那个它会生成一个虚函数指针,存储在对象的头4个字节(vfptr 32位 4字节 64位 8字节),然后通过这个虚函数指针指向虚函数表(编译阶段产生的,运行时加载到.rodata段),虚函数表里存储了重写后虚函数的地址,最后进行动态绑定调用实现动态多态
虚函数=》vfptr=》vftable=》重写=》动态绑定调用=》动态多态
利用栈上的对象出作用域自动析构的特征,来做到资源的自动释放,能够很好的解决内存泄漏问题。
但是普通智能指针解决不了浅拷贝问题,因为在析构的时候只析构了一次,将拷贝的智能指针析构掉,原先的指针没有被析构,造成了野指针的存在。
不带引用计数的智能指针
auto_ptr scoped_ptr unique_ptr
带引用计数的智能指针,使用资源的时候引用计数加1,不使用资源的时候引用计数减1,使用多个智能指针管理同一个资源
shared_ptr 可以改变资源的引用计数
weak_ptr 不可以改变资源的引用计数
避免只使用强指针导致的循环引用(只使用强指针交叉引用,会导致new出来的资源无法释放),定义对象的时候用强指针,引用对象的时候用弱指针
强弱指针一起用可以保证线程安全,通过引用计数避免已经析构的对象仍然访问了他的方法
(1)指针未初始化
(2)数组指针越界
(3)释放内存后没有把指针指向null
解决方法:
定义的时候就初始化;
严格限定变量范围,防止数组指针越界;
内存释放后将指针指向空。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9bBI4G9q-1657069471273)()]
int *p[10]:存放了10个int * 类型的数组
int(*p)[10]:指向整型数组的指针
int *p(int);
p(int) 说明p为函数,返回值为 int类型的指针 (指向int的指针)。
int ( *p)(int);
( * p)说明 p 为指针,(*p)() 说明该指针指向函数, 函数的返回值为in
虚函数:
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
静态类型:对象在声明时采用的类型,在编译期既已确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。
shared_ptr 的构造要求比较高,如果对象在创建的时候没有使用共享指针存储的话,之后也不能用共享指针管理这个对象了。如果有引用循环 (reference cycle), 也就是对象 a 有指向对象 b 的共享指针,对象 b 也有指向对象 a 的共享指针,那么它们都不会被析构。
函数模板和类模板
函数模板是一类函数的抽象,代表了一类函数,这一类函数具有相同的功能,代表一 具体的函数,能被类对象调用,而函数模板绝不能被类对象调用.
类模板是对类的抽象,代表一类类,这些类具有相同的功能,但数据成员类型及成员函数返回类型和形参类型不同
const_cast
1、常量指针被转化成非常量的指针,并且仍然指向原来的对象;
2、常量引用被转换成非常量的引用,并且仍然指向原来的对象;
3、const_cast一般用于修改指针。如const char *p形式。
static_cast
static_cast 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患。
用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。注意:进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护。
static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性。(前两种可以使用const_cast 来去除)
在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现
reinterpret_cast: C风格的类型转换
reinterpret_cast是强制类型转换符用来处理无关类型转换的,通常为操作数的位模式提供较低层次的重新解释!
dynamic_cast:支持RTTI信息识别的类型转换
dynamic_cast强制转换,应该是这四种中最特殊的一个,因为他涉及到面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关.所以不能完全使用C语言的强制转换替代,它也是最常有用的,最不可缺少的一种强制转换.
deque:双端队列容器
底层数据结构:动态开辟的二维数组
扩容方式:一维数组从2开始,以2倍的方式进行扩容,每次扩容后,原来第二维的数组,从新的第一维数组的下标oldsize/2开始存放,上下都预留相同的空行,方便支持deque的首尾元素添加
deque deq;
增加:
deq.push_back(20); 从末尾添加元素 O(1)
deq.push_front(20); 从首部添加元素 O(1) // vec.insert(vec.begin(), 20) O(n)
deq.insert(it, 20); it指向的位置添加元素 O(n)
删除:
deq.pop_back(); 从末尾删除元素 O(1)
deq.pop_front(); 从首部删除元素 O(1)
查询搜索:
iterator(连续的insert和erase一定要考虑迭代器失效的问题)
底层数据结构:
增删查改:
随机访问:
内存扩容方式:
vector:底层数据结构为数组 ,支持快速随机访问。
list:底层数据结构为双向链表,支持快速增删。
deque:底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问。
stack:底层一般用deque/list实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。
queue:底层一般用deque/list实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。
priority_queue:的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现。
set:底层数据结构为红黑树,有序,不重复。 multiset:底层数据结构为红黑树,有序,可重复。
map:底层数据结构为红黑树,有序,不重复。 multimap:底层数据结构为红黑树,有序,可重复。
unordered_set:底层数据结构为hash表,无序,不重复。
unordered_multiset:底层数据结构为hash表,无序,可重复 。
unordered_map:底层数据结构为hash表,无序,不重复。 unordered_multimap:底层数据结构为hash表,无序,可重复。
数组:增加删除O(n) 随机访问O(1)
链表:(考虑搜索的时间)增加删除O(1) 查询O(n)
1.底层数据结构:数组 双向循环链表
插入删除效率高:
对于关联容器来说,不需要做内存拷贝和内存移动。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点
(底层数据结构)红黑树性质:
1.每个节点都是红色或黑色
2.根节点为黑色
3.叶节点为黑色的NULL结点。
4.如果结点为红,其子节点必须为黑
5.任一节点到NULL的任何路径,所含黑结点数必须相同
相关操作:
1.构造(默认构造 拷贝构造)
赋值
2.插入
删除
清空
3.查找 find(key)
统计 count(key)
(1) 他们的底层都是以红黑树的结构实现,因此**插入删除等操作都在O(logn)**时间内完成,因此可以完成高效的插入删除;
(2) 在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是key+value,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value
(3) 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。
(1) 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;
(2) 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。
(3) lambda表达式的语法定义如下:
[capture] (parameters) mutable ->return-type {statement};
(4) lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;
语法:
(1) [函数对象参数]
**标识一个 Lambda 表达式的开始,这部分必须存在,不能省略。**函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)。函数对象参数有以下形式:
函数对象参数有以下形式:
空。没有任何函数对象参数。
=。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
&。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
this。函数体内可以使用 Lambda 所在类中的成员变量。
a。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
&a。将 a 按引用进行传递。
a,&b。将 a 按值传递,b 按引用进行传递。
=,&a,&b。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
&,a,b。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
(2) (操作符重载函数参数)
**标识重载的 () 操作符的参数,没有参数时,这部分可以省略。**参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递。
(3)mutable 或 exception 声明
**这部分可以省略。**按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。
(4)返回值类型
标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
(5) {函数体}
push_back()方法要调用构造函数和复制构造函数,这也就代表着要先构造一个临时对象,然后把临时的copy构造函数拷贝或者移动到容器最后面。
而emplace_back()在实现时,则是直接在容器的尾部创建这个元素,省去了拷贝或移动元素的过程。
emplace_back() 函数在原理上⽐ push_back() 有了⼀定的改进,包括在内存优化⽅⾯和运⾏效率⽅⾯。内存优化主要体现在使⽤了就地构造(直接在容器内构造对象,不⽤拷⻉⼀个复制品再使⽤) + 强制类型转换 的⽅法来 实现,在运⾏效率⽅⾯,由于省去了拷⻉构造过程
结论:在C++11情况下,果断用emplace_back代替push_back
(1)关键字
(2)智能指针
(3)多线程
(4)函数对象、绑定器、lamda表达式
(5)容器
set和map:红黑树 O(lgn)
unordered_set和unordered_map:哈希表 O(1) 提高增删查的效率
array:数组 vector
forward_list:前向链表 list
const就是只读的意思,只在声明中使用,意即其所修饰的对象为常量((immutable)),它不能被修改,并存放在常量区。
static一般有两个作用,规定作用域和存储方式(静态存储)。对于局部变量,static规定其为静态存储方式每次调用的初始值为上一次调用后的值,调用结束后存储空间不释放;对于全局变量,如果以文件划分作用域的话,此变量只在当前文件可见,对于static函数也是如此。static修饰的变量如果没有初始化,则默认为0.
浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变,拷贝了基本类型的数据
深复制----在计算机中开辟了一块新的内存地址用于存放复制的对象。
(1) new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;
(2) 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
(3) new操作符内存分配成功时,new返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
(4) new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
(5) new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
使用限制:
inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。
如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(1) 指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
(2) 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)
(3) 有多级指针,但是没有多级引用,只能有一级引用。
(4) 指针和引用的自增运算结果不一样。(指针是指向下一个地址,引用是引用的变量值加1)。
(5) sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小。
(6) 引用访问一个变量是直接访问,而指针访问一个变量是间接访问。
(7) 使用指针前最好做类型检查,防止野指针的出现;
(野指针:指针变量的值未初始化;指向的地址空间已经被free/delete;超出作用域)
(8) 引用底层是通过指针实现的;
(9) 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。
线程池(thread pool)这个东西,在面试上多次被问到,一般的回答都是:“管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复。
简单来说就是有一堆已经创建好的线程(最大数目一定),初始时他们都处于空闲状态。当有新的任务进来,从线程池中取出一个空闲的线程处理任务然后当任务处理完成之后,该线程被重新放回到线程池中,供其他的任务使用。当线程池中的线程都在处理任务时,就没有空闲线程供使用,此时,若有新的任务产生,只能等待线程池中有线程结束任务空闲才能执行。
线程池的好处:
(1)避免线程开的过多,使内存耗尽
(2)避免了创建和销毁线程的代价
(3)任务和执行分离
在多核CPU中合理的调度线程在各个核上运行可以获得更高的性能。在多线程编程中,每个线程处理的任务优先级是不一样的,对于要求实时性比较高的线程或者是主线程,对于这种线程我们可以在创建线程时指定其绑定到某个CPU核上,以后这个核就专门处理该线程。这样可以使得该线程的任务可以得到较快的处理,特别是和用户直接交互的任务,较短的响应时间可以提升用户的体验感。
一种用户态的轻量级线程,完全由用户调度控制,拥有自己的寄存器上下文和栈,协程调度切换的时候,先将寄存器上下文和栈保存到其他地方,切换回来的时候再恢复之前保存的寄存器上下文和栈。直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
但是同一时间只能执行一个协程,大致来说是一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态,适合对任务进行分时处理;而线程,一次可以执行多个线程,适合执行多任务处理。
在半同步/半反应堆模型下,主线程为异步线程,主要负责监听所有的事件 。1. 如果事件为连接事件:主线程负责将连接请求接收并且向epoll内核事件表中注册事件。2. 如果事件为非连接事件:主线程负责将连接请求加入请求队列,线程池中的工作线程通过竞争来执行请求队列中的任务。以上模型也存在一定的缺点:1. 首先请求队列是唯一的,那么请求队列在其中便扮演了临界资源的角色。对于临界资源的访问必须要加锁保护,消耗了一定CPU时间。2. 线程池中每一个工作线程同一时刻只能处理一个请求。如果在任务量比较大的情况下,请求队列中会堆积任务,客户端响应变慢。
传统的IO编程一般使用一个while循环不断地监听端口是否有新的连接,如果有,就调用一个处理函数来完成传输处理。
这种模式有一个缺点,因为是阻塞的,所以前一个请求没有处理完成,那个后一个就无法处理,所以就诞生了一个线程处理一个连接的IO编程模式。早期的Tomcat就是这么做的。
对于大量的连接,需要耗费大量的线程资源,对线程资源要求太高。
线程池为了防止频繁的创建线程
Reactor模型
(1)等待IO
(2)处理IO
Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
因此,Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。这里的「事件」就是有新连接、有数据可读、有数据可写的这些 I/O 事件这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。
举个实际生活中的例子,Reactor 模式就是快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快递。而在 Proactor 模式下,快递员直接将快递送到你家门口,然后通知你。
无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。
select:
将文件描述符收集过来,交给内核,让内核来判断哪一个有数据,有数据的会在bitmap里置位,同时将select函数返回,不再阻塞,最后进行遍历查询即可。
缺点:
(1)bitmap只有1024
(2)用户态和内核态切换开销过大
poll:
epoll:
当多个线程同时访问其所共享的进程资源时,需要相互协调,以防止出现数据不一致、不完整的问题。这就叫线程同步。
用锁解决
互斥锁、自旋锁、读写锁
内存共享: 两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区,它是访问共享资源的代码片段,一定不能给多线程同时执行。
互斥
首先明白临界区是指一段代码,这段代码是用来访问临界资源的。临界资源可以是硬件资源,也可以是软件资源。但它们有一个特点就是,一次仅允许一个进程或线程访问。当有多个线程试图同时访问,但已经有一个线程在访问该临界区了,那么其他线程将被挂起。临界区被释放后,其他线程可继续抢占该临界区。
临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多。
进程通信:
管道、信号、信号量、套接字、消息队列、共享内存
进程、线程基本原理
多进程和多线程
线程池
进程同步、进程通信、进程调度
虚拟地址、物理地址
用户空间、内核空间
一、操作系统让进程访问的是虚拟地址空间,而不是物理地址
1.任何程序在编译时都会产生指令和数据,进行地址编号,但是如果地址不连续,就会程序运行不起来,编译器的地址管理比较麻烦(无法动态的获知物理空间的使用情况,也就无法为数据进行编号)
2.进程直接访问物理地址,如果此时有一个野指针,那么在进行操作野指针的时候可能会改变其他空间的数据,造成不安全的事件发生(无法进行内存访问控制)
3.程序运行空间通常需要一块连续的空间,空间利用率低,通过虚拟地址空间映射到物理内存上进行数据存储,可以实现数据在物理空间上离散式存储,提高内存的利用率,并且每个进程都有一份属于自己的连续空间使用
虚拟内存主要是解决内存空间不足的问题,但其实它面对的问题又是内存空间利用率低的问题。
产生这个矛盾的原因在于,进程的创建的时候操作系统会给进程分配内存空间,但是进程在程序运行过程中,在每一个时间段内,只会用到程序中的一部分数据,而存储的其他数据实际上被没有被用到。但是还是要给每个进程划分一块内存,并且确保这一块内存能把进程在执行过程中的所有数据都存储起来,所有内存又显得很不足。
这种内存分配的方式是一种对外抠门,对内大方的方式,在分配时,尽量多拿,拿到之后用不用是另一回事,正就像某些社会团体的表现了。
虚拟内存就是解决这个问题的,它首先认识到每个进程分配到内存没有被有效利用的问题,第二,它有效利用了硬盘这个存储空间大,但是读写性能较差的存储设备。
虚拟内存的方案是:把内存分为很多大小相同,空间连续的页框,然后给每一个进程分配一个4G的逻辑地址,逻辑地址也是按照页去划分的,页和页框之间存在一个映射关系,但是每一个进程中,只有一部分的页对应的页框中存储了对应的数据,大多数的页框其实并不会存储映射的数据,这些数据交给硬盘来存储,当发生缺页中断时,从硬盘中读取相应的数据,并按照一个策略将页框中每一个数据与之置换(页面置换),同时会修改对应的页表的标志位。
这时候4G的内存被充分的利用了起来,并且对于每个进程来说,自己似乎独占了4G的内存,双赢的方案,膜拜计算机的前辈吧。
死锁只有同时满足以下四个条件才会发生:
互斥锁、自旋锁、读写锁、乐观锁、悲观锁
加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。
自旋锁是通过 CPU 提供的 CAS
函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。
所以,读写锁适用于能明确区分读操作和写操作的场景。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
进程是资源分配和调度的一个独立单位;而线程是进程的一个实体,是CPU调度和分配的基本单位。
同一个进程中的多个线程的内存资源是共享的,各线程都可以改变进程中的变量。因此在执行多线程运算的时候要注意执行顺序。
进程与线程最大的区别在于上下文切换过程中,线程不用切换虚拟内存,因为同一个进程内的线程都是共享虚拟内存空间的,线程就单这一点不用切换,就相比进程上下文切换的性能开销减少了很多。
多进程和多线程的主要区别是:线程是进程的子集(部分),一个进程可能由多个线程组成。多进程的数据是分开的、共享复杂,需要用IPC;但同步简单。多线程共享进程数据,共享简单;但同步复杂。
进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。
上下文切换指的是CPU从一个进程(线程)切换到另一个进程(线程)。
进程是正在执行的一个程序的实例,在Linux中,线程可以算作轻量级进程,线程可以并发执行,并且同一进程创建的线程可以共享同一片地址空间及其它资源,即该进程的进程地址空间及属于该进程的其它资源。
进程和线程加以区分
内存池(Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
定义
短连接:例如普通的web请求,在三次握手之后建立连接,发送数据包并得到服务器返回的结果之后,通过客户端和服务端的四次握手进行关闭断开。
长连接:区别于短连接,由于三次握手链接及四次握手断开,在请求频繁的情况下,链接请求和断开请求的开销较大,影响效率。采用长连接方式,执行三次握手链接后,不断开链接,保持客户端和服务端通信,直到服务器超时自动断开链接,或者客户端主动断开链接。
适用场景
短连接:适用于网页浏览等数据刷新频度较低的场景。
长连接:适用于客户端和服务端通信频繁的场景,例如聊天室,实时游戏等。
本文发布于:2024-02-04 10:46:39,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170705404354884.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |