面向过程是分析出解决问题所需的步骤,然后用函数把这些步骤一步步实现,使用时候一个个依次调用即可;
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为完成一个步骤,而是为描叙某个事物在解决问题的步骤中的行为;
宏定义相当于字符替换,而const是常量声明;
宏定义是预处理器处理,而const是编译器处理;
宏定义五类型安全检查,而const有类型安全检查;
宏定义不分配内存,const要分配内存;
宏定义存在代码段,const存在数据段;
const修饰变量:限定变量为不可修改;
const修饰指针:指针常量和指向常量的指针;
const修饰成员函数,主要目的是防止成员函数修改成员变量的值,即该成员函数并不能修改成员变量;
不可以。C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
sizeof 对数组,得到整个数组所占空间大小;
sizeof 对指针,得到指针本身所占空间大小;
类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
本质区别:默认访问控制(struct是共有的public class是私有的private)
数据成员中无指针时,可用浅拷贝。有指针时,若没有自定义拷贝构造函数,会调用默认拷贝构造函数,这样就会调用两次析构函数,第一次析构delete了内存,第二次就指针悬挂了,所以得采用深拷贝。
两者主要区别在于深拷贝在堆内存中另外申请空间来存储数据,也就解决了指针悬挂的问题。
结构体struct:把不同类型的数据组合成一个整体。struct里每个成员都有自己独立的地址;
共同体union:各成员共享一段内存空间, 一个union变量的长度等于各成员中最长的长度,以达到节省空间的目的;
修饰构造和转换函数时,可防止隐式转换
可以访问私有成员、友元关系不可传递且具有单向性;
尽量少使用 using 指示污染命名空间,多使用using声明std::cin、std::cout.
全局作用域符 ::name
类作用域符 class::name
命名空间作用域符 namespace:: name
用于检查实体的声明类型或表达式的类型及值分类
左值:可放在等号左边的、能够取地址、具名的值;(变量名、前置自增/减、赋值运算符、返回左值引用的函数调用、解引用)
右值:只能放在等号右边的、不能取地址、不具名的值;(纯右值:字面值、返回非引用类型的函数调用、后置自增/减、算术/逻辑/比较表达式
将亡值:(移动语义),将亡值用来触发移动构造或移动赋值构造进行资源转移,然后调用析构函数)
左值引用:常规引用,一般表示对象的身份;
右值引用:必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。右值引用可实现移动语义和精准传递,主要可用于 1. 消除两个对象交互式不必要的对象拷贝,节省运算存储资源; 2. 更简洁明确的定义泛型函数;
左值引用是对左值的引用,右值引用是对右值的引用
const左值引用能指向右值:局限是不能修改这个值
右值引用通过std::move() (将左值转换为右值,也可以触发移动语义) 可以指向左值
声明出来的左值引用或右值引用都是左值
左值引用避免对象拷贝
右值引用可实现移动语义( 解决对象赋值的问题,对象赋值时避免资源的重新分配 ,通过触发移动构造和移动拷贝构造来实现的 )、完美转发( 不仅能准确地转发参数的值,还能保证其左右属性不变。实现:万能引用 )
好处是少了一次调用默认构造函数的过程。
封装:客观事物封装成抽象的类,关键字public(任意实体访问)、protected(只允许被子类及本类的成员函数访问)、private(只允许被本类的成员函数、友元类或友元函数访问)
继承: 基类---->派生类
多态:多种状态,可将多态定义为消息以多种形式显示的能力,可简单概括为”一个接口,多种方法“,即用同一个接口,但效果不同,有两种形式多态,即静态多态( 本质上就是模板的具现化 )和动态多态( 虚函数 )。
C++多态分类及实现:
4.1 重载多态(编译期):函数重载、运算符重载
4.2 子类型多态(运行期):虚函数
4.3 参数多态性(编译器):类模板、函数模板
静态多态:函数重载
class A{
public:
void do(int a);
void do(int a,int b);
};
动态多态:虚函数(virtual修饰成员函数)、动态绑定(使用基类的引用或指针调用一个虚函数时将发生动态绑定)
注: 可将派生类的对象赋值给基类的指针或引用,反之不行。普通函数(非成员函数)、静态函数、构造函数(调构造时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针) 都不能是虚函数(内联函数不能是表现多态时的虚函数)。
虚函数是实现多态的一个技术之一,派生类的指针指向基类对象的地址(派生类对象的地址赋给基类指针)称为向上转型,c++允许隐式向上转型。将子类指向父类,向下转换则必须强制类型转换。我们可以用父类的指针指向其子类的对象,然后通过父类的指针调用实际子类的成员函数。如果子类重写了该成员函数就会调用子类的成员函数,没有声明重写就调用基类的成员函数。这种技术可以让父类的指针有“多种形态”;
c++没有强制规定虚函数的实现方式。编译器中主要用虚表指针(vptr)和虚函数表(vtbl)来实现的。当调用一个对象对应的函数时,通过对象内存中的vptr找到一个虚函数表vtbl,虚函数表内部是一个函数指针数组,记录的是该类各个虚函数的首地址。然后调用对象所拥有的函数。
虚函数表和虚函数指针位置:
构造函数可以设置为虚否?
不可,因为虚函数调用得通过”虚函数表“来进行,而虚函数表需要在对象实例化之后才能够进行调用。构造对象过程中,尚未给”虚函数表“分配内存,所以这个调用违背先实例化后调用准则。
友元函数不是虚函数,因为友元函数不是类成员,只有类成员才能是虚函数
静态成员函数不能是虚: 1. 因为static不属于任何类对象或类实例;2.静态成员函数没有隐藏的this指针,虚函数调用刚好需要this指针。在有虚函数的类实例中,this指针调用vptr指针,vptr找到vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数地址。总体来说虚函数调用关系是:this指针->vptr->vtable ->virtual虚函数。
如果类是父类,则必须声明为虚析构函数。基类声明一个虚析构函数,为了确保释放派生对象时,按照正确的顺序调用析构函数。如果析构函数不是虚的,那么编译器只会调用对应指针类型的虚构函数。切记,是指针类型的,不是指针指向类型的!! 而其他类的析构函数就不会被调用。
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。若基类有虚析构函数,那delete 释放内存的时候,先调用子类虚析构函数,再调用基类虚析构函数,防止内存泄漏。
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
带纯虚函数的类叫抽象类,这种类不能直接生成对象(抽象类无法实例化对象),而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
虚基类是虚继承中的基类。
虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定;
虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)
相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
不同之处:
noexcept:告诉编译器指定某个函数不抛异常;
oveerride: 告诉编译器要重写父类的方法;
final:该关键字用来修饰类,当用final修饰后,该类不允许被继承,在 C++ 11 中 final 关键字要写在类名的后面;
=default 、 =delete 、using等
new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
new在申请内存时会自动计算所需字节数,所以不需要指定内存块大小,而 malloc 则需我们自己输入申请内存空间的字节数。
new操作符分配对象会经历三个步骤:
new和malloc区别:malloc时c语言标准库函数,new时c++的运算符。malloc分配失败返回空指针,new分配失败默认抛出异常。malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
malloc/fre存在的必要性: 对一些非内部数据类型来说,只用malloc/free无法满足要求,对象在创建同时要自动执行构造函数,对象消亡时自动执行析构函数,而malloc/ free不在编译器的控制权限内,不能自动执行构造函数和析构函数。所以c++中需要可完成动态内存分配和初始化工作的运算符new和完成清理和释放内存工作的运算符delete。但c程序只能用这俩,所以依然存在malloc/free。
不是的,回收的内存首先被ptmalloc使用双链接表保存起来,当用户下次申请内存的时候先尝试从这些内存中寻找,可避免频繁系统调用,ptmalloc也可避免过多内存碎片。
类对象被创建时,编译器为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。因此构造函数的的作用是初始化对象的成员函数;
默认构造函数:若无自定义构造函数,编译器会自动默认生成一个无参构造函数
一般构造函数:
拷贝构造函数:此函数的函数参数为对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在的对象的数据成员的值一一复制到新创建的对象中。若没有写拷贝构造函数系统会默认创建一个拷贝构造函数,但类中有指针成员时,最好自己定义且在函数中执行深拷贝
移动构造函数:有时会遇到这样的情况,用对象a初始化对象b后对象a就不在使用了,但是对象a的空间还在(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那为啥不能直接使用a的空间呢?这样就避免新的空间的分配,大大降低了构造成本。这就是移动构造函数设计的初衷。拷贝构造函数中,对于指针,一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。
赋值构造函数:=运算符的重载,类似拷贝构造函数,将=右边的类对象赋值给类对象左边的对象,不属于构造函数,=两边的对象必须都要被创建。
指针管理存在困难:资源释放指针没有置空(野指针、指针悬挂(多个指针指向同一块内存,一个已经释放了,但其他指针还在傻傻等待着) )、没有释放资源产生内存泄漏、重复释放资源引发coredump;
智能指针种类:
share_ptr: 共享所有权,多个指针可以指向一个相同的对象,当最后一个share_ptr离开作用域时候才会释放掉内存。原理:在其内部有一个共享引用计数器来自动管理,计数器实际上就是指向该资源指针的个数,复制一个ptr时,引用计数会-1,引用计数为0时,delete内存。
wrak_ptr : weak指针的出现是为了解决shared指针循环引用造成的内存泄漏的问题。而weak_ptr不会增加引用计数,因此将循环引用的一方修改为弱引用,可以避免内存泄露。允许共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空。可打破环状引用。
unique_ptr : 它拥有对持有对象的唯一所有权,采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。即两个unique_ptr不能同时指向同一个对象。主要体现在:1. unique_ptr不能被复制到另外一个unique_pt; 2. unique_ptr所持有的对象只能通过转移语义将所有权转移到另外一个unique_ptr;
static_cast(非多态类型转换,安全性不如dynamic_cast)
void*和其它类型指针之间的转换、子类对象的指针转换成父类对象的指针
dynamic_cast(多态类型转换,安全性好)
用于执行 ”安全地向下转型“,他是唯一一个在运行时处理的
进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。
typeid
type_info
栈由系统自动分配(分配速度快,不会由碎片),堆是自己申请和释放的(速度慢,有碎片)
且栈顶和栈底时预设好的,大小固定,堆向高地址扩展,是不连续的内存区域,大小可灵活调整;
内存管理机制:
堆:系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中);
栈:只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了);
array,底层数据结构是数组,无序可重复且支持访问的;
vector, 底层数据结构是数组,
deque, 底层数据结构是双端队列
forward_list, 单向链表
list, 双向链表
stack,deque / list
queue, deque / list
priority_queue,底层数据结构是vector+max-heap
set,multiset, map,multimap 底层数据结构都是红黑树
unordered_set,unordered_multiset,unordered_map, unordered_multimap 底层数据结构是哈希表
堆是一种完全二叉树形式,可分为最大值堆和最小值堆
最大值堆:子节点均小于父节点,根节点是树中最大的节点
最大值堆:子节点均大于父节点,根节点是树中最小的节点
1.链式地址法:key相同的用单链表连接;
2.开放定址法:发生冲突时,若哈希表未被装满,可把这个值存放到冲突位置种的下一个空位置中;
由于AVL树在每次插入和删除操作时都要进行旋转操作来保持平衡,因此相比于其他平衡树结构,如红黑树,AVL树更加平衡,但也更消耗空间和时间。任何一个节点的左子支高度与右子支高度只差绝对值不超过1。
比起AVL树,红黑树的平衡调整操作简单有效,更适合对插入和删除操作频繁的场景。
红黑树主要特征:每个节点上增加一个属性表示节点颜色,可以红色或黑色。
性质
对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。
稳定:插入排序、冒泡排序、归并排序
不稳定:希尔排序、直接选择排序、堆排序、快速排序;
比较相邻元素,如果第一个比第二个大就进行交换,对每一队相邻元素做同样的工作 (排序算法稳定,时间复杂度 O(n^2) ,空间复杂度O(1))
#include
#include
using namespace std;
int main() {
int n,temp;
cin >> n;
vector<int>a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (a[j] < a[j + 1]) {
temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
}
cout << "冒泡排序结果:" << endl;
for (int i = 0; i < n; i++) {
cout << a[i] << endl;
}
}
每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。(稳定,时间:O(n^2))
#include
#include
using namespace std;
int main() {
int n,temp,j;
cin >> n;
vector<int>a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 1; i < n; i++) { // i从1开始遍历
temp = a[i]; //把这个值先存起来
// 看前面的值有没有比当前值大,有的话就逐个互换位置
for (j = i - 1; j >= 0 && temp < a[j]; j--) {
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
cout << "插入排序结果:" << endl;
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
每次从待排序列中找出最大或最小的元素,顺序放在待排序的数列的最前面,直到全部待排序的数据元素排完。(不稳定,时间O(n^2))
#include
#include
using namespace std;
int main() {
int n,temp,min;
cin >> n;
vector<int>a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < n; i++) {
min = i; // 从头遍历的时候,先假设最大值索引为 本次值i
for (int j = i + 1; j < n; j++) { // 然后从i后面的值开始遍历比大小
if (a[min] > a[j]) min = j; // 找到最小值的话就返回其索引
}
if (min != i) swap(a[i], a[min]); //根据索引交换相应元素
}
cout << "选择排序结果:" << endl;
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
return 0;
}
通过分开治理,先将分开的部分变得有序,再来进行合并,称为归并排序;
二路归并: 通过将待排序列分成两部分进行归并排序;(<=是稳定的,时间复杂度O(NlogN));
#include
using namespace std;
int a[8] = { 36,25,48,12,25,43,20,28 };
int b[8];
// 总体来说就是两两合并,先是两个元素合并,然后是两个组合并
void gsort(int left, int right) {
if (left == right) return;
int mid = (left + right) >> 1;
gsort(left, mid); // 分为左半区
gsort(mid + 1, right); // 右半区
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) { // 两个大块逐元素对比
if (a[i] <= a[j]) {
b[k] = a[i];
i++;
}
else {
b[k] = a[j];
j++;
}
k++;
}
while (i <= mid) b[k++] = a[i++]; // 合并左半区剩余元素
while (j <= right) b[k++] = a[j++]; //合并右半区剩余元素
for (int i = left; i <= right; i++) a[i] = b[i]; // 临时数组中合并后元素复制回原来的数组
}
int main() {
int n=8;
gsort(0,n-1);
cout << "归并排序结果:" << endl;
for (int i = 0; i < n; i++) {
cout << a[i] << endl;
}
return 0;
}
根据实验,直接从b输出是不行的,a[i]=b[i]这行代码是必须要有的;
分治思想,随机选一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全小于基准元素,一部分全大于基准元素,再按此方法递归对这两部分数据进行快速排序。(不稳定,时间复杂度O(NlogN))
#include
using namespace std;
int a[8] = { 12,45,56,23,65,1,23,1 };
int n = 8;
void qsort(int left, int right) {
int i = left, j = right;
int mid = a[(left + right) >> 1]; // 取出中间元素
do {
while (a[i] < mid) i++; // i到1时就不满足条件退出
while (a[j] > mid) j--; // j到n-1时不满足直接退出
if (i <= j) {
swap(a[i], a[j]); // 然后给大于mid的左边和小于mid的右边换一下
i++, j--; // 同时
}
} while (i <= j);
if (j > left) qsort(left, j); // 保证左边有序
if (i < right) qsort(i, right); // 保证右边有序
}
int main() {
qsort(0, n - 1);
cout << "快速排序结果:" << endl;
for (int i = 0; i < n; i++) {
cout << a[i] << endl;
}
return 0;
}
构造大顶堆,然后取出元素(直接把堆顶元素放到末尾,下次构建不考虑就相当于取出了),然后继续构造。(不稳定,时间复杂度O(NlogN))
class Singleton {
private:
static Singleton* instance; // 静态成员变量,保存单例对象的指针
Singleton(){} //私有构造函数 防止外部代码直接实例化对象
public:
//获取单例的实例
static Singleton* getInstance() { // getInstance是获取单例的实例的静态方法
if (instance == nullptr) {
instance = new Singleton(); // 为空就创建一个新的Singleton对象
}
return instance; // 不为空就返回已存在的对象
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
懒汉模式的好处是只有在需要使用单例对象时才会创建该对象,从而节省了系统资源。但缺点是第一次访问时需要创建对象,可能会导致一定的延迟,并且在多线程环境中需要特殊处理以保证线程安全。
class Singleton {
private:
static Singleton* instance;
//私有构造函数,防止外部创建对象
Singleton(){}
public:
//获取单例的实例
static Singleton* getInstance() {
return instance;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = new Singleton();
饿汉模式的好处是在程序启动时就创建了单例对象,可以避免在程序运行过程中再次创建对象导致的延迟和线程安全问题。但缺点是如果该对象很大或者初始化需要消耗大量资源,会导致程序启动变慢。
在多线程环境中,使用懒汉模式实现的单例模式可能会存在线程安全问题,因为多个线程可能同时通过getInstance()方法来创建对象。为了解决这个问题,可以采用双重检查锁定(Double-Checked Locking)机制来保证线程安全。
#include
#include
using namespace std;
class Singleton{
private:
static Singleton* instance;
static mutex mtx;
//私有构造函数 防止外部创建对象
Singleton(){}
public:
//获取单例的实现
static Singleton* getInstance() { // 双重检查锁定机制
if (instance == nullptr) {
unique_lock<mutex> lock(mtx); // 若为null 则进入临界区(加锁)
if (instance == nullptr) { // 再次检查是否为空 这样可避免多个线程同时创建对象
instance = new Singleton();
}
lock.unlock(); //解锁 当第一个线程退出临界区(解锁)后,会继续执行 并返回已存在的实例。
}
return instance;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx; // 使用互斥锁(std::mutex)来保证临界区的互斥访问,可以保证在多线程环境下的线程安全性。
int main() {
thread t1([]() {
Singleton* s1 = Singleton::getInstance();
cout << "Thread 1 - Singleton address:" << s1 << endl;
cout << endl;
});
thread t2([]() {
Singleton* s2 = Singleton::getInstance();
cout << "Thread 2 - Singleton address:" << s2 << endl;
cout << endl;
});
t1.join();
t2.join();
return 0;
}
进程是资源分配的基本单位,线程是运算调度的最小单位;
进程 | 线程 | |
---|---|---|
资源开销 | 创建、销毁、上下文切换开销高 | 线程开销小 |
通信与同步 | 管道、消息队列、共享内存 | 线程共享相同的内存空间,可直接访问共享数据,所以通信更方便 |
安全性 | 一个进程崩溃不会直接影响其他进程稳定性 | 线程共享相同的内存空间,一个线程错误可能影响整个进程稳定性 |
方法 | 实现 |
---|---|
先来先服务调度算法 | 按照进程到达的先后顺序进⾏调度,即最早到达的进程先执⾏,直到完成或阻塞 |
最短作业优先 | 优先选择运行时间最短的进程来运行 |
高响应比优先调度算法 | 综合考虑等待时间和服务时间的比率,选择具有最高翔硬币的进程来执行 |
时间片轮转调度算法 | 将CPU时间划分为时间片,每个进程在一个时间片内运行,然后切换到下一个进程 |
最高优先级调度 | 为每个进程分配一个优先级,优先级高的进程先执行。可能导致低优先级进程长时间等待,引发饥饿问题 |
多级反馈队列 | |
最短剩余时间优先 | 每次选择剩余执行时间最短的进程来执行 |
最大吞吐量调度 | 旨在最大化单位时间内完成的进程数量 |
方式 | 具体作用 |
---|---|
有名管道 | 可实现任意关系的进程间通信 |
无名管道 | 只能在父子进程间使用 |
信号量 | 一个计数器,可用来控制多个线程对共享资源的访问 |
信号 | 比较复杂的通信方式,用于通知接收进程某个事件已发生 |
消息队列 | 消息的链表,存放在内核中(任意进程间通信) |
共享内存 | 映射一段能被其他进程访问的内存,这段内存有一个进程创建,丹多个进程都可访问 |
Socket套接字 | 用于不同计算机的进程通信,支持TCP/IP网络通信的基本操作单元 |
作单元; |
锁机制:
互斥锁/量:提供了以排他方式防止数据结构被并发修改的方法;
读写锁:允许多个线程同时读共享数据,而对写操作是互斥的;
自旋锁:类似于互斥锁,都是为了保护共享资源,互斥锁是资源被占用,申请者进入睡眠状态,而自旋锁则循环检测保持者是否已经释放锁;
条件变量:以原子的方式阻塞进程直到某个特定条件为真为止,对条件的测试是在互斥锁保护下进行的。(始终与互斥锁一起使用);
信号量机制:
信号机制:类似进程间的信号处理;
屏障:
进程之间私有和共享的资源
私有:地址空间、堆、栈、全局变量、寄存器;
共享:代码段、公共数据、进程目录、进程ID;
多进程可靠性高,创建销毁,切换速度慢,内存资源占用大。多线程创建销毁和切换速度快,内存、资源占用小,但可靠性差;
两个或多个进程在争夺系统资源时,由于互相等待对⽅释放资源⽽⽆法继续执⾏的状态。当系统资源不足、资源分配不当、进程运行推进顺序不合适时就会产生死锁;
条件 | 具体说明 |
---|---|
互斥条件 | 一个进程占用了某个资源时,其他进程无法同时占用该资源 |
请求保持 | 一个线程因为请求资源而阻塞的时候,不会释放自己的资源 |
不可剥夺 | 资源不能被强制性地从一个进程中剥夺,只能由持有者自愿释放 |
环路等待 | 多个进程间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源 |
破坏上面任意条件就可破环死锁
条件 | 具体方案 |
---|---|
破环请求保持条件 | 一次性申请所有资源 |
破环不可剥夺条件 | 占用部分资源的线程进一步申请其他资源时,若申请不到可主动释放它占有的资源 |
破坏环路等待条件 | 靠按序申请资源来预防。让所有进程按照相同的顺序请求资源,释放资源则反序释放 |
满足 1.互斥、2.请求和保持、3.不可剥夺、4.环路 就可死锁;
虚拟内存在每一个进程创建和加载的过程中,会分配一个连续虚拟地址空间,不是真实存在的,而是通过映射与实际地址空间对应,这样就可使每个进程看起来有自己独立的连续地址空间,每个程序都认为它拥有足够的内存来运行。
需要虚拟内存的原因如下:
原因 | 具体说明 |
---|---|
内存扩展 | 虚拟内存是得每个程序都可以使用比实际可用内存更多的内存,从而允许运行更大的程序或处理更多的数据 |
内存隔离 | 虚拟内存提供了进程之间的内存隔离,每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存 |
物理内存管理 | 虚拟内存允许操作系统动态的将数据和程序的部分加载到物理内存中,以满足当前正在运行的进程的需求。物理内存不足时,操作系统可以将不常用的数据或程序暂时移到硬盘上,释放内存便于其他进程使用 |
页面交换 | 当物理内存不⾜时,操作系统可以将⼀部分数据从物理内存写⼊到硬盘的虚拟内存中,这个过程被称为⻚⾯交换 |
内存映射文件 | 虚拟内存还可以⽤于将⽂件映射到内存中,这使得⽂件的读取和写⼊可以像访问内存⼀样⾼效 |
**内存分段:**是将⼀个程序的内存空间分为不同的逻辑段 segments ,每个段代表程序的⼀个功能模块或数据类型,如代码段、数据段、堆栈段等。每个段都有其⾃⼰的⼤⼩和权限;
**内存分⻚:**是把整个虚拟和物理内存空间分成固定⼤⼩的⻚(如4KB)。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚ Page;
具体作用有:逻辑隔离、内存保护、虚拟内存、内存共享、内存管理;
分页管理:内存空间利用率高,不会产生外部碎片,只会有少量的页内碎片,但不方便按照逻辑模块实现信息的共享和保护;
分段管理:很方便按照逻辑模块实现信息 的共享和保护,但是如果段长过大,为其分配很大的连续空间会很不方便,段式管理会产生外部碎片;
进程同步:多个并发执行的进程之间协调和管理他们的执行顺序,以确保他们按照一定的顺序或时间间隔执行。
互斥:在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。
常见解决进程同步和互斥的方法:使用信号量和PV操作,PV操作是一种对信号量进行增加或者减少的操作,他们可用来控制进程之间的同步或者互斥。
以下方法也可解决进程同步和互斥问题:
方法 | 具体实现 |
---|---|
临界区 | 可能引发互斥问题的代码称为临界区,每个进程进入临界区前必须获取一个锁,退出后释放该锁,可确保同一时间只有一个进程可以进入临界区 |
互斥锁 | 一种同步机制,进程访问资源前获取互斥锁,用完后释放锁,只有获得所的进程才能访问共享资源 |
条件变量 | 用于在进程之间传递信息,一遍他们在特定条件下等待或唤醒。通常与互斥锁一起使用,以确保等待和唤醒的操作在正确的时机执行 |
中断和异常是两种不同的事件,它们都会导致CPU暂停当前的程序执⾏,转⽽去执⾏⼀个特定的处理
程序。但中断是由外部设备或其他处理器产生的,通常是异步的,且可以被屏蔽或禁止(CPU可以不鸟中断);异常是由CPU内部产生的,通常是同步的,且不可以被屏蔽或禁止(CPU必须立即响应,随时待命处理)
中断作用 | 中断产生 |
---|---|
外设异步通知CPU | 外设 |
CPU之间发送消息 | CPU |
处理CPU异常 | CPU异常( 陷阱、故障、中止 ) |
实现系统调用 | 中断指令 |
锁 | 解释 |
---|---|
互斥锁 | 用于实现互斥访问共享资源,任何时刻只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放 |
自旋锁 | 一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放 |
读写锁 | 允许多个线程同时读共享资源,只允许一个线程进行写操作 |
悲观锁 | 认为多线程同时修改共享资源的概率比较高,所以访问共享资源时要上锁 |
乐观锁 | 先啥也不管,修改了共享资源再说,若出现同时修改的情况,再放弃本次操作 |
为保证线程之间的互不干扰采用的一种机制,叫线程同步机制
互斥锁、条件变量、读写锁、信号量
物理层:通过媒介传输比特,确定机械及电器规范
数据链路层:将比特组装城帧和点到点的传递(帧Frame)
网络层:IP协议为计算机网络相互连接进行通信而设计的协议;
ARP(地址解析协议)、ICMP(网际控制报文协议)、IGMP(网际组管理协议)
VPN虚拟专用网、NAT网络地址转换
路由表:网络ID、子网掩码、下一跳地址/接口
运输层:TCP、UDP
TCP是面向连接的智能点对点通信,面向字节流(可能出现粘包问题)、可靠交互。TCP通过确认和超时重传、数据合理分片和排序、流量控制、拥塞控制、数据校验等机制保证可靠传输;
UDP是无连接的,尽最大努力交付的说明他不太可靠,面向报文的(不会出现粘包),没有拥塞控制,支持一对一、一对多、多对多交互通信的;
TCP | UDP | |
---|---|---|
可靠性 | 可靠 | 不可靠 |
连接性 | 面向连接 | 无连接 |
报文 | 面向字节流 | 面向报文 |
效率 | 低 | 高 |
双工性 | 全双工 | 一对一、一对多、多对一、多对多 |
流量控制 | 滑动窗口 | 无 |
拥塞控制 | 慢启动、拥塞避免、快重传、快恢复 | 无 |
传输速度 | 慢 | 快 |
应用场景 | 要求通信数据可靠场景(如⽹⻚浏览、⽂件传输、邮件传输、远程登录、数据库操作等) | ⽤于要求通信速度⾼场景(如域名转换、视频直播、实时游戏等) |
常见状态码:
1xx : 提示信息,协议处理的中间状态;
2xx : 请求成功;
3xx : 请求重定向;
4xx : 请求错误;
5xx : 服务器错误;(504:网关超时,服务器作为网关或代理,但没有及时从上游服务器收到请求)
HTTP常见字段:
Host字段:客户端请求发送时,用来指定服务器的域名;
Content-length字段:服务器返回数据时,带有该字段,表示回应的数据长度;
Connection字段:用于客户端要求服务器使用HTTP长连接时使用;
Content-Type字段:服务器返回时告诉客户端本次数据的 格式;
Content-Encoding字段:服务器返回的数据使用了什么压缩格式;
GET | POST | |
---|---|---|
作用 | 从服务端获取资源 | 向服务端提交数据 |
参数传递方式 | 一般写在URL中,且只接受ASCII字符 | 一般放在请求体中,对数据类型无限制 |
安全性 | 参数直接暴露在URL中不安全,不能传递敏感信息 | 安全 |
参数长度限制 | 数据量较小,不能大于2KB | 数据量较大,一般默认为不受限制 |
参数长度限制 | HTTP协议 | 没有Body和URL的长度限制,对URL限制的大多是浏览器和服务器的原因 |
编码方式 | URL编码 | 多种编码方式 |
缓存机制 | 请求被浏览器Cache,请求参数被完整保留在浏览器历史记录里,产生的URL地址可被保存为书签,在浏览器退出时是无害的 | POST不会被主动cache,参数不会被保留,不可保存为书签,浏览器回退时POST会再次提交请求 |
时间消耗 | 产生一个TCP数据包 | 产生两个TCP数据包 |
发送数据 | 浏览器把header和data一并发送出去,服务器响应200 | 浏览器先发header,服务器响应100continue,浏览器再发送data,服务器响应200ok |
幂等 | 因为只读操作,无论操作多少次,服务器数据都是安全的 | 因为是新增或提交数据操作,会修改服务器上资源,所以不安全,不是幂等的 |
缓存:减少不必要网络传输、节约带宽;更快加载页面;减少服务器负载,避免服务过载情况出现;
Cache-Control强缓存
last-modified
If-Modified-Since
if-None-Match
Etag什么的
HTTP1.0 | HTTP1.1 | |
---|---|---|
长连接 | 默认短连接,每次请求都需建立一个TCP连接 | 支持长连接,每个TCP连接上可传送多个HTTP请求和响应,默认开始Connection : Keep-Alive |
缓存 | 主要使⽤ If-Modified-Since/Expires 来做为缓存判断的标准 | 引⼊了更多的缓存控制策略例如 Entity tag / If-None-Match 等更多可供选择的缓存头来控制缓存策略 |
管道化 | 无 | 使请求能够”并行“传输,但响应必须按照请求发出的顺序依次返回 |
增加Host字段 | 无 | 一个服务器能够用来创建多个Web站点 |
状态码 | 无 | 新增了24个错误状态响应码 |
带宽优化 | 存在一些浪费带宽现象,如客户端仅需某个对象一部分,服务器将整个对象送过来,且不支持断点续传功能 | 在请求头引入了range头域,允许只请求资源的某个部分,返回码时206 |
HTTP1.1 | HTTP2.0 | |
---|---|---|
二进制分帧 | 无 | 在应用层(HTTP2.0)和传输层(TCP or UDP)间加入一个二进制分帧层,从而突破HTTP1.1的性能限制 |
多路复用 | 无 | 允许同时通过单⼀的 HTTP/2 连接发起多重的请求-响应消息,这个强⼤的功能则是基于“⼆进制分帧”的特性 |
首部压缩 | 不⽀持 header 数据的压缩 | 使⽤ HPACK 算法对 header的数据进⾏压缩,这样数据体积⼩了,在⽹络上传输就会更快。⾼效的压缩算法可以很⼤的压缩 header ,减少发送包的数量从⽽降低延迟。 |
服务端推送 | 无 | 服务器可对客户端的一个请求发送多个响应,即服务器可额外的向客户端推送资源,而无需客户端明确的请求 |
HTTP | HTTPS | |
---|---|---|
传输 | 明文传输 | 通过SSL\TLS加密传输 |
端口 | 80 | 443 |
CA证书 | 无 | 需要 |
连接 | 连接简单、无状态 | SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP安全 |
DNS域名管理系统是用户使用浏览器访问网址后使用的第一个重要协议,DNS解决的是域名和IP地址映射问题;
多TCP连接靠某些服务器对Connection: keep-alive 的 Header进⾏了⽀持,就是完成此HTTP请求后,不断开HTTP请求使用的TCP连接。这种操作的好处是连接可被重新使用,之后发送HTTP请求时不需要重新建立TCP连接,且维持连接的SSL开销也能避免;
HTTP的Keep-Alive是由应用层(用户态)实现的,称为HTTP长连接( 同一个TCP发送接收多个HTTP 请求/应答 避免了连接建立和释放的开销);HTTP短连接每次建立链接都只能请求一次资源,都要经历建立TCP->请求资源->响应资源->释放连接;
TCP的KeepAlive是由 TCP 层(内核态) 实现的,称为 TCP 保活机制:TCP有一个定时任务做倒计时,超时后触发任务,内容是发送一个探测报文给对端,用来判断对端是否存活;
方法名 | 具体实现 |
---|---|
数据块大小控制 | 应用数据被分割成TCP认为最合适发送的数据块,再传输给网络层,数据块被称为报文段或段 |
序列号 | TCP给每个数据包指定序列号,接收⽅根据序列号对数据包进⾏排序,并根据序列号对数据包去重 |
校验和 | TCP将保持它⾸部和数据的校验和,⽬的是检测数据在传输过程中的任何变化,若收到报文校验和有错,TCP丢弃此报文段 |
流量控制 | TCP连接的每⼀⽅都有固定⼤⼩的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收⽅来不及处理发送⽅的数据,能提示发送⽅降低发送的速率,防⽌包丢失。TCP⽤滑动窗⼝实现流量控制 |
拥塞控制 | 网络拥塞时,减少数据发送;慢启动、拥塞避免、超时重传、快速重传和快速恢复等 |
确认应答 | 通过ARQ协议实现。基本原理是每发完⼀个分组就停⽌发送,等待对⽅确认。如果没收到确认,会重发数据包,直到确认后再发下⼀个分组 |
超时重传 | TCP发出⼀个数据段后,它启动⼀个定时器,等待⽬的端确认收到这个报⽂段。如果不能及时收到⼀个确认,将重发这个报⽂段 |
方法名 | 具体实现 |
---|---|
慢启动 | 在连接刚开始时,发送方会逐渐增加发送窗口大小,从而以指数增长的速度增加发送的数据量 |
拥塞避免 | 慢启动阶段过后,发送方进入拥塞避免阶段,这个阶段发送方逐渐增加发送窗口的大小,但增加速率较慢,避免过快导致网络拥塞 |
超时重传 | 若发送方在超时时间内未收到确认,会认为数据包丢失,并重传这些数据包。这是拥塞窗口的最后手段,用于检测和处理网络中的丢包或拥塞情况 |
快速重传和快速恢复 | 发送方发送的数据包丢失或网络出现拥塞时,接收方会发送重复确认(ACK)通知发送方有数据包丢失。发送方受到一定数量的重复确认时,会立即重传丢失的数据包而不是等待超时 |
拥塞窗口调整 | 发送方根据网络的拥塞程度动态调整发送窗口大小,通过检测网络延迟和丢包情况确定合适的发送速率 |
两者都用于管理用户的状态和身份,Cookie 通过在客户端记录信息确定⽤户身份, Session 通过在服务器端记录信息确定⽤户身份。
Cookie | Session | |
---|---|---|
存储位置 | 用户浏览器 | 服务器 |
数据容量 | 较小,一般为几KB | 无固定限制,取决于服务器的配置和资源 |
安全性 | 可被用户读取和篡改 | 难以被访问和修改,所以更安全 |
传输方式 | 每次HTTP请求中都会被自动发送到服务器 | 通常通过Cookie或URL参数传递 |
TCP是一个基于字节流的传输服务,意味着TCP所传输的数据是没有边界的,所以可能会出现两个数据包黏在一起的情况;
解决方案:发送定长包;包头上加上包体长度,给他说明一下具体消息长度;在数据包之间设置边界,例如添加特殊符号\r\n标记之类的(FTP协议就这么做的。问题是如果正文中也有\r\n那也会误判消息边界);使用更加复杂的应用层协议;
流量控制是让发送方发送速率不要太快,要让接收方来得及接收;
主要方法是利用可变窗口进行流量控制
拥塞控制就是防止过多的数据注入到网络中,这样可使网络中的路由器或链路不至过载;
方法:慢启动、拥塞避免、快重传、快恢复;
TCP是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手),TCP释放连接时服务器的ACK和FIN是分开发送的(中间隔着数据传输),而TC建立连接时服务器的ACK和SYN是一起发送的(第二次握手)。
TCP 是全双⼯通信,可以双向传输数据。任何⼀⽅都可以在数据传送结束后发出连接释放的通知,待对⽅确认后进⼊半关闭状态。 当另⼀⽅也没有数据再发送的时候,则发出连接释放通知,对⽅确认后才会完全关闭了 TCP 连接。总结:两次握⼿可以释放⼀端到另⼀端的 TCP 连接,完全释放连接⼀共需要四次握手。
因为客户端请求释放连接时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端FIN请求(服务端发送ACK),然后传输数据,完成后,服务端再提出FIN请求(服务端发送FIN);而连接时则没有中间的数据传输;
DNS域名系统:作为将域名和IP地址相互映射的一个分布式数据库,能使人更方便的访问互联网。DNS使用TCP和UDP端口53;
域名:
FTP(文件传输协议):用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式,使用TCP数据报,提供交互式访问,双向传输。
TELNET:
HTTP:
GET:请求指定的页面信息,并返回实体主体;
POST:向指定资源提交数据进行处理请求(例如提交表单或上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和已有资源的修改;
状态码:
1xx : 通知信息,请求收到了或正在进行处理
2xx : 成功,如接收或知道了
3xx : 重定向,要完成请求还必须采取进一步的行动
4xx : 客户差错,如请求中有错误的语法或不能完成
5xx : 服务器差错,如服务器失效无法完成请求
其他协议:
SMTP(简单邮件传输协议):传输Email的标准,一个相对简单的基于文本的协议;
DHCP(动态主机设置协议):一个局域网的网络协议,使用UDP协议工作;
首先服务端先建立socket()接口,然后bind(),然后listen(),accept()等待客户端建立连接,TCP客户端也得先建立socket()接口,然后connect()建立连接,然后客户端的werite()函数写入数据到服务端,服务端利用read()函数读取数据,服务端处理完请求后利用write()函数写入响应给客户端,客户端利用read()函数读取响应的信息,完事后客户端可以close()关闭连接,服务端收到请求后也开始关闭连接。over
索引就是数据的目录,MySQL存储引擎有 MyISAM InnoDB Memory;
B+树索引:所有数据存储在叶⼦节点,复杂度为O(logn),适合范围查询
哈希索引:适合等值查询,检索效率⾼,⼀次到位
创建的主键索引和二级索引默认使用的是B+Tree索引,B+Tree是一种多叉树,叶子节点存放数据,非叶子节点只存放索引,且每个节点里的数据按照主键顺序存放的。每层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,且每个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表;
每读取一个节点当作一次I/O操作,如整个查询过程共经历了3个节点,就进行了3次I/O操作。B+Tree存储千万级的数据只需要3-4层高度就可满足,意味着从千万级的表查询目标数据最多需要3-4次磁盘I/O,所以B+Tree相比于B树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况下,查询一个数据的磁盘I/O依然维持在3-4次;
主键索引B+Tree和二级索引B+Tree区别:
若使用二级索引查询商品,先检二级索引中的B+Tree的索引值,找到对应的叶子节点,然后获取主键值,再通过主键索引中的B+Tree树查询到对应的叶子节点,然后获取整行数据(此过程叫回表,得差两个B+Tree才能查到数据)。但当查询的数据只能在二级索引的B+Tree的叶子节点里查询到,这时就不用再主键索引,这种在二级索引的B+Tree就能查询到结果的过程就叫做覆盖索引,也就是只需要查一个B+Tree就能找到数据;
在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。
CREATR TABLE table_name(
...
PRIMARY KEY(index_column_1) USING BTREE
);
建立在单列上的索引称为单列索引,如主键索引,建在多列上的索引称为联合索引;
将多个字段组合成一个索引,就是联合索引;
联合索引非叶子节点用两个字段的值作为B+Tree的key值,使用联合索引存在最左匹配原则(不遵循,联合索引就会失效);比如创建一个联合索引(a,b,c),要是不遵循最左匹配,那b,c是全局无序,局部相对有序的的话,是无法利用到索引的。(利用索引的前提是索引里的Key是有序的)
联合索引中并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询;
也可能存在部分字段用到联合索引的B+树,部分字段没有用到联合索引B+Tree的情况。最左匹配原则会一直享有匹配直到遇到 范围查询 就会停止匹配。
select * from t_table where a > 1 and b = 2
符合a>1条件的二级索引记录的范围里,b字段的值是无序的。条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引;
select * from t_table where a >= 1 and b = 2
在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的(因为对于联合索引,是先按照 a 字段的值排序,然后在 a 字段的值相同的情况下,再按照 b 字段的值进行排序。这条查询语句 a 和 b 字段都用到了联合索引进行索引查询
SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2
在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<。而有的数据库则不包含 value1 和 value2 边界值(类似于 > and <)。这条查询语句 a 和 b 字段都用到了联合索引进行索引查询
SELECT * FROM t_user WHERE name like 'j%' and age = 22
然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,但是对于符合 name = j 的二级索引记录的范围里,age字段的值是「有序」的(因为对于联合索引,是先按照 name 字段的值排序,然后在 name 字段的值相同的情况下,再按照 age 字段的值进行排序。这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。
综上:联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
索引下推
索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
索引区分度
实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
如何通过索引提高查询效率?( 联合索引进行排序 )
select * from order where status = 1 order by create_time asc
利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免在文件排序,提高了查询效率。
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。限制:
覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作.使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。
建表的时候,都会默认将主键索引设置为自增的
主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。
常见索引失效场景:
MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。
要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。
二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。
为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。
而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。
重点来了重点来了!!!
B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。
但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:
大体来说就是: 以很小的时间代价查找到、插入和删除效率高、适用于范围查询
count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个。
count(*) 其实等于 count(0),也就是说,当你使用 count() 时,MySQL 会将 * 参数转化为参数 0 来处理。所以,count() 执行过程跟 count(1) 执行过程基本一样的,性能没有什么差异。
原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态;
一致性:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的;
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失;
脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象;
不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象;
幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象;
以下主要是隔离性:
多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,为避免这些问题,SQL提出四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高性能越差,InnoDB引擎默认隔离级别是可重复读;
Read View 的时机不同:
3.1 读提交隔离级别是在每个 select 都会生成一个新的 Read View,也意味着事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务;
3.2 可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录;
上面两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。
在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select … for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。
举例了两个发生幻读场景的例子:
全局锁主要应用于做全库逻辑备份, 这样在备份数据库期间,不会因为数据或表结构的更新而出现备份文件的数据与预期不一样。加上全局锁,意味着整个数据库都是只读状态。若数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。若数据库引擎支持的事务支持可重复读的隔离级别
1.表锁
表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。当会话退出后,也会释放所有表锁。不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁。
元数据锁(MDL)
当我们对数据库表进行操作时,会自动给这个表加上 MDL:对一张表进行 CRUD 操作时,加的是 MDL 读锁;
对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
意向锁
在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
也就是,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。意向锁的目的是为了快速判断表里是否有记录被加锁。
AUTO-INC锁
AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放。
在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉
Record Lock ( 记录锁 )
锁住的是一条记录。而且记录锁是有 S 锁和 X 锁(排他锁和共享锁)之分的 ( SS兼容 SX不兼容 XX不兼容 )
Gap Lock( 间隙锁 )
只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但并无区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的;
Next-Key Lock ( 临建锁 )
Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的;
插入意向锁
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。若有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态;
如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。
非唯一索引和主键索引范围查询加锁规则不同点在于某些情况下,唯一索引的next-key lock退化为间隙锁或记录锁,而非唯一索引的next-key lock不会退化。
在线上在执行 update、delete、select … for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。在数据库层面,有两种策略通过打破循环等待条件解除死锁:
Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。Buffer Pool 以页为单位缓冲数据,InnoDB通过三种链表来管理缓存页,Free List (空闲页链表)管理空闲页;Flush List (脏页链表)管理脏页;
LRU List,管理脏页+干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去:
InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做 2 点优化:
undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC;
redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;
c++内存分区:从高到底分别为:
栈区:函数内局部变量的存储单元可在栈上创建,函数执行结束后,这些存储单元被自动释放;
堆:由new分配的内存块,手动去释放内存,一个new对应一个delete
自由存储区:堆是操作系统维护的一块内存,那自由存储区就是new和delete动态分配和释放对象的抽象概念;
全局/静态存储区:全局变量和静态变量被分配到同一块内存中
常量存储区:存放的常量,不可修改
代码区:存放函数体的二进制代码
内存泄漏指的是堆内存泄露,动态分配的内存没有被释放。1. 使用new malloc分配内存后,没有使用delete和free进行内存释放;2. 对已释放的内存进行访问或操作,释放已经释放的内存;3 循环引用,两个或多个对象之间相互持有对方的引用,导致无法正确释放他们之间的内存;
避免及解决的方法:计数法,用new时+1,delete时-1; 一定要将基类的析构函数声明为虚函数;保证new和malloc成对出现;
智能指针啥的 也能解决问题好像
类是一种面向对象编程的主要概念,提供一种封装数据以及相关行为的方式,类可以将数据和其操作相关的函数封装在一起,类可将对象的属性和行为组织成一个逻辑单元,类可将实现细节隐藏在类的内部,只提供必要的公共接口,类可通过继承关系建立继承链,避免重复编写相同代码。通过多态,派生类的对象可表现出与基类不同的行为,实现更灵活的代码结构和动态绑定能力。
同一事物表现出不同事物的能力,向不同对象发送同一消息,不同对象在接受时会产生不同的行为 ( 重载实现编译时多态,虚函数实现运行时多态 )
函数重载:同一个类中定义函数名相同,但参数列表不同的函数;
函数重写:也称为虚函数重写,指的是在派生类中重新定义(覆盖)基类中已有的虚函数;
虚函数:用父类指针指向子类的对象,然后通过父类指针调用实际子类的成员函数,如果子类重写了该成员函数就会调用子类的成员函数,没有声明重写就调用基类的成员函数。
虚函数工作原理:主要通过虚表指针和虚函数表实现,调用对象的函数时,对象内存中的虚表指针找到一个虚函数表,虚函数表内部是一个函数指针数组,记录的时虚函数的首地址,然后调用对象拥有的函数。
vector底层:数组,内存连续; 链表底层:双向链表,内存不用连续
vector: 顺序内存,支持随机访问,但插入删除性能差; 链表:随机访问性能差,但插入删除性能好;
vector:一次性分配好内存,不够时才进行翻倍扩容; list:每次插入新节点都会进行内存申请;
删除链表元素:
找到待删除元素的前一个结点,将这个节点的next指针指向待删除结点的下一个节点,跳过待删除结点。释放待删除结点的内存空间;
一种自平衡的二叉搜索树,在插入和删除节点时通过特定的旋转和涂色操作来维持平衡 用于c++中map和set等
特性:
节点是红色或者黑色、根节点是黑色、所有叶子节点(NULL节点)都是黑色、若一个节点是红色,那两个子节点都是黑色的、从任意一个节点到叶子节点,经过黑色节点的数量都相同,即黑高度相同;
按照一定顺序访问书的每个节点,常用树的遍历方式包括 深度优先遍历、广度优先遍历
深度优先: 前序遍历、中序遍历、后序遍历
广度优先:从根节点开始,逐层访问树上的每个节点,先访问根节点,然后从左到右一次访问同一层每个节点
随机选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全小于基准元素,一部分全大于基准元素,然后递归对这两部分数据进行快速排序
#include
using namespace std;
int a[8] = { 12,43,1,90,0,65,4,56 };
int n = 8;
void qsort(int left, int right) {
int i = left, j = right;
int mid = a[(left + right) / 2];
do{
while (a[i] < mid) i++;
while (a[j] > mid) j--;
if (i <= j) {
swap(a[i], a[j]);
i++, j--; // 交换后还得继续遍历两边有没有其他元素符合条件的
}
} while (i <= j);
if (j > left) qsort(left, j);
if (i < right) qsort(i, right);
}
int main() {
qsort(0, n - 1);
for (int i = 0; i < n; i++) {
cout << a[i] << endl;
}
return 0;
}
TCP/IP协议:用户互联网通信的基础协议,包括IP ICMP(在IP网络上传递错误报告、控制消息的协议) UDP等
HTTP:用于客户端和服务器之间传输超文本;
HTTPS:基于HTTPS的安全通信协议,使用SSL/TLS来加密和保护数据传输;
FTP:在客户端和服务器之间传输文件;
SMTP:在邮件传输代理之间传递电子邮件;
DNS:域名解析;
慢启动:连接刚开始,发送方会逐渐增加发送窗口大小,从而以指数增长速度增加发送数据量;
拥塞避免:慢启动后,发送方进入拥塞避免阶段,这个阶段发送方逐渐增加发送窗口的大小,但增加速率慢,避免过快导致网络拥塞;
超时重传:发送方超时时间内未收到确认,认为数据包丢失,并重传这些数据包;
快速重传和快恢复:快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞;
拥塞窗口调整:发送方根据网络的拥塞程度动态调整发送窗口大小;
TCP头里有一个字段叫window,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接受数据,于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来;所以通常窗口的大小是由接收方的窗口大小来决定的。
TCP是面向连接的字节流,低效率,全双工的通信,传输速度慢,而UDP是不可靠的,无连接的面向报文的高效的通信协议,不仅支持点对点,还支持一对多,多对一,多对多。TCP由流量控制,拥塞控制等措施保证可靠性,而UDP没有这些操作,所以此协议是尽最大可能交付,不保证可靠性;
应用场景:
TCP要求通信数据可靠场景(网页浏览、文件传输、邮件传输、远程登录、数据库操作)
UDP要求通信速度高场景(视频直播、实时游戏等)
HTTPS在HTTP于TCP层之间加入了SSL/TLS协议,即可保证安全性;
HTTPS通过混合加密方式实现信息的机密性,解决了窃听的风险;摘要算法实现了完整性,可以为数据生成独一无二的指纹,解决了数据被更改的可能性,将服务器公钥放入到数字证书中,解决了被冒充的风险;
SSL/TLS协议基本流程:
1)客户端向服务器索要并验证服务器的公钥;
2)双方协商生产会话密钥
3)双方采用会话密钥进行加密通信
https实现基础步骤:
客户端发起https连接请求、服务器准备证书、客户端验证证书、握手过程、数据传输;
TLS握手阶段涉及四次通信,使用不同的密钥交换算法握手流程也不一样,常用密钥交换算法有两种:RSA算法、ECDHE算法;
TLS协议建立详细流程:
1)客户端向服务器发起加密通信请求 ( clienthello请求 ),客户端主要向服务器发送 (1)客户端支持的TLS版本 (2)客户端生产的随机数(后面用于生成 会话密钥 的条件之一)(3)客户端支持的密码套件 ( RSA加密算法);
2)服务器收到客户端请求后,向客户端发出响应(severhello),服务端回应的内容有(1)确认TLS协议版本,若浏览器不支持,就关闭加密通信。(2)服务器生产的随机数(后面用于生产会话密钥条件之一)。(3)确认的密码套件列表(RSA加密算法)。(4)服务器的数字证书;
3)客户端收到服务器的回应后,先通过浏览器或者操作系统中的CA公钥,确认服务器的数字证书的真实性。没问题,客户端就从数字证书中取出服务器公钥,使用它加密报文,并给服务端发送(1)一个随机数,该随机数被服务器公钥加密通信(2)加密算法改变通知,表示后面的信息都用会话密钥加密通信(3)客户端握手结束通知。
4)服务器收到客户端的第三个随机数后,通过协商的加密算法计算出本次通信的 会话密钥,向客户端发送(1)加密通信算法该表通知,随后用会话密钥加密通信 (2)服务器握手结束通知。
进程的创建、销毁、上下文切换开销都比线程 大。进程的通信与同步方式有 管道、消息队列、共享内存等,而线程因为共享相同的内存空间,可直接访问共享数据,所以通信更加方便。一个进程的崩溃不会影响其他进程稳定性,但线程因为共享内存空间,所以一个线程错误会影响整个进程的稳定性;
有名管道/无名管道:任意关系/父子关系进程间通信;
信号量:一个计数器,可用之多个线程对共享资源的访问;
信号:用于通知接收进程某个时间已发生;
消息队列:消息的链表,存放在内核中;
共享内存:映射一段能被其他进程访问的内存;
socket套接字:用于不同计算机进程通信,支持TCP/IP网路通信的基本操作单元;
在多进程通信时候使用;
数据共享时候使用:多进程可能需要共享相同的数据;
注意: 同步机制、内存管理、数据保护、安全性;
日志和错误信息、调试器、内存调试工具(Valgrind)、核心转储文件;
互斥锁、读写锁、条件变量、信号量、原子操作
使用索引、COUNT()、避免不需要的列和排序;
// 回文子串
int countsub(string s) {
int n = s.size();
vector<vector<bool>>dp(n, vector<bool>(n, false));
int result = 0;
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (s[i] == s[j]) {
if (j - i <= 1) {
result++;
dp[i][j] = true;
}
else if (dp[i + 1][j - 1]) {
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
// 双指针法
int extend(const string& s, int i, int j, int n) {
int res = 0;
while (i >= 0 && j < n && s[i] == s[j]) {
i--;
j++;
res++;
}
return res;
}
int countsub1(string s) {
int result = 0;
for (int i = 0; i < s.size(); i++) {
result += extend(s, i, i, s.size());
result += extend(s, i, i + 1, s.size());
}
return result;
}
int main() {
string s = "caabaa";
cout << "动规:";
cout << countsub(s) << endl;
cout << "双指针:";
cout << countsub1(s) << endl;
}
webserver开启讲述,课外学了opencv解决图像分帧问题
25.手撕 很像leetcode原题,笔试第2道相似
了解过一下基础的命令 ls:列出当前目录下的文件和文件夹; cd:更改当前工作目录;
mkdir:创建新的目录 rm:删除文件 cp:复制文件或目录 mv : 移动或重命名文件或目录
cat: 显示文件的内容 touch:创建新文件或更新文件的访问和修改时间
grep: 在文件中搜索指定的模式 chmod:修改文件或目录的权限 ps aux 显示当前正在运行的进程状态
ifconfig : 显示和配置网络接口信息 ping : 测试与指定主机连接
栈内存的分配和释放时自动的,堆是由程序员手动进行分配和释放的;
栈大小通常较小,由编译器预先分配的固定空间,而堆的大小通常较大,受限于操作系统的虚拟内存大小;
栈上分配和释放速度比堆上块;
栈上变量具有局部作用,生命周期随函数的执行自动开始和结束,堆上分配的内存可以在多个函数之间共享,程序员释放前一直存在;
栈(直接访问)上内存访问速度比堆(通过指针访问)块
1)存储方式:数组在内存中按照顺序存储元素结构、内存中连续分布,通过索引直接访问和修改元素。链表使用指针将不同节点链接在一起,节点可在内存中离散分布,通过指针连接进行访问;
2)大小调整:数组大小创建时确定且固定不变,列表的大小可以动态调整,在需要时可以添加或删除节点,不需要重新分配和复制整个数据。
3)插入和删除:数组的元素在内存中是连续存储的,插入和删除操作可能涉及到大量的数据移动;对于列表,由于节点之间通过指针链接,插入和删除节点只需要修改指针的指向,具有较低的时间复杂度。
4)随机访问性能:数组的连续存储方式,可以通过索引直接访问元素,具有O(1)的随机访问性能。而对于列表,需要从头节点开始顺序访问链表,具有O(n)的访问性能;
5)内存分配:数组需要一段连续的内存空间来存储所有元素,因此,在创建数组时需要首先分配足够的内存。而链表可以根据需要动态增长,每个节点可以独立分配内存;
二面
简化代码的逻辑,多态可将一组具有相同接口或继承关系的对象视为共同类型,从而简化代码逻辑,如在一个汽车类中,各种具体的汽车可以继承同一个基类,实现各自的启动、加速、停止等方法。
多态可以提高代码的可读性、可维护性和可扩展性,通过将不同的对象视为同一类型,实现统一的接口和行为。多态在面向对象编程中的应用非常广泛,特别是在需要处理不同类型的对象集合时,能够带来很多优势
多态的最典型应用是在继承关系中,当多个子类继承自同一个父类时,可以使用多态来实现统一的接口,提高代码的可读性和可维护性。
在需要处理多种类型对象的情况下,可以使用多态来统一对这些对象进行处理。例如,可以定义一个通用的函数或方法,接受基类(父类)指针或引用作为参数,然后根据对象的实际类型执行不同的操作。
在设计模式中,多态也被广泛应用。例如,策略模式和工厂模式等都利用了多态的特性来实现不同算法或对象的动态切换。
当需要扩展程序功能时,通过添加新的子类并重写父类的方法,可以使用多态来保持原有代码的兼容性,而无需修改现有代码。
webserver走起
选择一个基准元素,然后让左边小于基准元素,右边大于基准元素,然后递归左右两边直到排序完成;
// 快排走一波
int a[8] = { 23,12,67,34,0,77,12,0 };
int n = 8;
void qsort(int left,int right) {
int i = left, j = right;
int mid = a[(left + right) / 2];
do {
while (a[i] < mid) i++;
while (a[j] > mid) j--;
if (i<=j) { //这一步是必须要有滴
swap(a[i], a[j]);
i++, j--;
}
} while (i <= j);
if (left < j)qsort(left,j); //保证左边有序
if (i < right) qsort(i, right); //保证右边有序
}
int main() {
qsort(0, n - 1);
for (int i = 0; i < n; i++) {
cout << a[i] << endl;
}
return 0;
}
#include
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
// 快慢指针直接拿下
int findMiddleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow->val;
}
int main() {
// 创建一个链表: 1 -> 2 -> 3 -> 4 -> 5 -> nullptr
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
int middleNode = findMiddleNode(head);
std::cout << "Middle Node Value: " << middleNode << std::endl;
return 0;
}
利用调试工具、打印调试信息、缩小问题范围、搜索互联网资源和文档
死循环、内存泄露、资源竞争、无限递归、外部原因(硬件故障、操作系统错误等)
解决: 使用调试工具、添加日志输出、使用断点、隔离问题
线程安全性、合适的通信机制(线程间通信)、死锁与饥饿(设置合适的资源分配策略)、资源回收与内存管理、并发性(使用合适的同步机制避免出现意外的并发问题)
互斥、占有且等待、不可强占用、循环等待;只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立
预防和解决死锁的方法包括:
避免循环等待:通过规定资源的获取顺序,破坏循环等待条件,避免死锁的发生。
使用资源分级:按照固定的层次顺序获取资源,在申请资源时遵循固定的顺序。
引入超时机制:在申请资源时设置超时参数,一段时间内未获取到资源则放弃申请,避免长时间等待。
资源预分配:根据需求预先分配所需的资源,避免在运行时竞争资源,降低死锁风险。
死锁检测和恢复:周期性地检测是否出现死锁,如果发现死锁,则采取相应的恢复策略,如终止某个进程来解除死锁。
表情识别项目走起
表情识别走起
AVL树,是一种二叉树的特殊形式,其左右子树的高度差不超过1;
平衡二叉树适用于较为平衡的数据分布情况。在数据分布极端不平衡的情况下,例如插入数据有序或逆序排列,平衡二叉树可能会失去平衡,造成性能下降
读写锁(Read-Write Lock)是一种在多线程环境中使用的同步机制,它允许多个线程同时读数据,而对写操作进行互斥。
高读取频率:当存在大量读取操作,并且读取操作不会修改共享数据时,可以使用读写锁。读写锁允许多个线程同时读取数据,提高了并发性和吞吐量。
低写入频率:如果写入操作相对较少,而读取操作较频繁,读写锁可以提供更好的性能。只有在没有写入操作时,才会允许多个线程同时读取数据,减少了互斥的开销。
读操作耗时较长:如果读取操作需要花费较长的时间,为了避免写操作长时间被阻塞,可以使用读写锁。读写锁允许多个线程同时读取数据,提高了并发性和响应性。
DNS解析:浏览器会提取出输入的地址,并发送域名(例如"www.example.com")到DNS服务器进行解析。DNS服务器将域名解析为对应的IP地址(例如"192.0.2.123"),以便浏览器能够向服务器发送请求。
建立连接:浏览器通过TCP/IP协议与服务器建立连接。这个过程中,浏览器会将请求发送给服务器的IP地址,建立起客户端与服务器之间的通信通道。
发送HTTP请求:浏览器发送HTTP请求给服务器。这个请求中包含了请求的方法(如GET、POST)、路径(URL)、请求头(headers)等信息。
服务器处理请求:服务器接收到浏览器发送的请求后,根据请求进行相应的处理。如果请求的是静态资源(如HTML文件、图片、CSS文件等),服务器会直接将资源返回给浏览器;如果请求的是动态资源(如通过脚本语言生成的内容),服务器会执行相应的处理逻辑,生成内容后再发送给浏览器。
接收响应:浏览器接收到服务器返回的响应数据,其中包括HTTP状态码、响应头(headers)、响应内容等信息。
渲染页面:浏览器根据接收到的响应数据,将其解析并渲染为用户可见的页面。这个过程中,浏览器会按照规则解析HTML、CSS和JavaScript,加载显示页面的内容。
断开连接:一旦页面渲染完成,浏览器与服务器之间的连接将会断开。如果在同一网站上浏览其他页面,浏览器可能会重用已经建立的TCP连接来提高性能。
确定目标IP地址:在进行IP寻址之前,需要明确寻址的目标IP地址,该地址标识了要进行通信的目标主机或网络。
确定子网掩码:在IPv4网络中,每个IP地址通常与一个子网掩码(Subnet Mask)配对使用,用于确定主机所属的网络范围。子网掩码与目标IP地址进行按位“与”运算,可以获得主机所在的网络地址。
路由查找:主机根据自身配置的路由表进行路由查找,确定到达目标IP地址所需经过的下一跳路由器。路由表中包含了网络前缀和对应的下一跳路由器的信息。
ARP解析:如果下一跳路由器的MAC地址未知,主机需要进行ARP(Address Resolution Protocol)解析,以获取目标IP地址对应的MAC地址。主机会广播一个ARP请求,请求目标IP地址的MAC地址,并等待目标主机的响应。
数据传输:一旦获得了目标IP地址的MAC地址,主机将网络数据包封装成数据帧,并通过物理介质(如以太网)向目标主机发送数据包。数据包在网络链路中依照路由表中的指引进行传输,经过多个路由器直至到达目标网络。
目标主机接收与处理:目标主机接收到数据包后,根据目标IP地址和端口,进行数据处理。其中,目标IP地址与主机的IP地址进行比较,确定该数据包是否为自己应该接收的。
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数
示例 1:
输入:s = “1 + 1”
输出:2
示例 2:
输入:s = " 2-1 + 2 "
输出:3
示例 3:
输入:s = “(1+(4+5+2)-3)+(6+8)”
输出:23
#include
#include
int caclue(string s) {
stack<int>st;
int n = s.size(), sign = 1, sum = 0,i=0,ret=0;
st.push(1);
while (i < n) {
if (s[i] == ' ') {
i++;
}
else if (s[i] == '+') {
sign = st.top();
i++;
}
else if (s[i] == '-') {
sign = -st.top();
i++;
}
else if (s[i] == '(') {
st.push(sign);
i++;
}
else if (s[i] == ')') {
st.pop();
i++;
}
else {
long num = 0;
while (i < n && s[i] >= '0' && s[i] <= '9') {
num += num * 10 + s[i] - '0';
i++;
}
ret += sign * num;
}
}
return ret;
}
int main() {
string s = "(1+(4+5+2)-3)+(6+8)";
cout << caclue(s) << endl;
return 0;
}
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。你可以假设给定的表达式总是有效的。注意:不允许使用任何将字符串作为数学表达式计算的内置函数。
示例 1:
输入:s = “3+2*2”
输出:7
示例 2:
输入:s = " 3/2 "
输出:1
示例 3:
输入:s = " 3+5 / 2 "
输出:5
#include
#include
int caclue(string& s) {
int n = s.length();
char sign = '+';
vector<int>st;
long num = 0;
for (int i = 0; i < n; i++) {
if (isdigit(s[i])) num = num * 10 + s[i] - '0';
if (!isdigit(s[i]) && s[i] != ' ' || i == n - 1) {
// if (s[i] == '+' || s[i]=='-' || s[i]=='*' || s[i]=='/') {
switch (sign) {
case '+':
st.push_back(num);
break;
case '-':
st.push_back(-num);
break;
case '*':
st.back() *= num;
break;
case '/':
st.back() /= num;
}
num = 0;
sign = s[i];
}
}
return accumulate(st.begin(), st.end(), 0);
}
int main() {
string s = " 2 + 3 * 5 -32 / 4 -2 +12";
int res = caclue(s);
cout << res << endl;
}
快递公司有一个业务要求,所有当天下发到快递中转站的快递,最迟在第二天送达用户手中。
假设已经知道接下来n天每天下发到快递中转站的快递重量。快递中转站负责人需要使用快递运输车运输给用户,每一辆运输车最大只能装k重量的快递。
每天可以出车多次,也可以不出车,也不要求运输车装满。当天下发到快递中转站的快递,最晚留到第二天就要运输走送给用户。
快递中转站负责人希望出车次数最少,完成接下来n天的快递运输。
输入:
输入第一行包含两个整数n(1<= n<=200000),k(1<=k<=100000000)
第二行包含n个整数ai,表示第i天下发到快递中转站的快递重量。
输出:
输出最少需要的出车次数。
输入:
3 2
3 2 1
输出:3
解释:
第一天的快递出车一次送走2个重量,留1个重量到第二天
第二天送走第一天留下的1个重量和当前的1个重量,留1个重量到第三天送走。
#include
#include
using ll = long long;
int main() {
int n, m;
cin >> n >> m;
vector<ll>weight(n+1);
for (int i = 0; i < n; i++) {
cin >> weight[i];
}
ll total = 0;
for (int i = 1; i <= n; i++) {
ll de = weight[i - 1] / m;
if (weight[i - 1] % m) {
de += 1;
}
ll res = de * m - weight[i - 1];
total += de;
weight[i] = max(weight[i] - res, 0LL);
}
cout << total << endl;
}
局一局域网内的设备可以相互发现,具备直连路由的两个设备可以互通。假定设备A和B互通,B和C互通,那么可以将B作为中心设备,通过多跳路由策略使设备A和C互通。这样,A、B、C三个设备就组成了一个互通设备集。其中,互通设备集包括以下几种情况:
直接互通的多个设备
通过多跳路由第略间接互通的多个设备
没有任何互通关系的单个设备现给出某一局域网内的设备总数以及具备直接互通关系的设备,请计算该局域网内的互通设备集有多少个?
输入:
第一行: 某一局域网内的设备总数M,32位有符号整数表示。1<= M<=200
第二行:具备直接互通关系的数量N,32位有符号整数表示。0<= N<200
第三行到第N+2行: 每行两个有符号32位整数,分别表示具备直接互通关系的两个设备的编号,用空格隔开。每个设备具有唯一的编号,0<设备编号< M
输出:
互通设备集的数量,32位有符号整数表示。
输入:
3
2
0 1
0 2
输出:1
解释:
编号0和1以及编号0和2的设备直接互通,编号1和2的设备可通过编号0的设备建立互通关系,互通设备集可合并为1个。
#define MAXN 40001
int fa[MAXN];
void init(int n) {
for (int i = 0; i < n; i++) {
fa[i] = i;
}
}
int find(int i) {
if (fa[i] == i) return i;
else {
return find(fa[i]);
}
}
void unnion(int i, int j) {
int i_fa = find(i);
int j_fa = find(j);
fa[i_fa] = j_fa;
}
int main() {
int m, n;
cin >> m >> n;
init(m);
int dev1, dev2;
vector<vector<int>> connection(m, vector<int>(m, 0));
for (int i = 0; i < n; i++) {
cin >> dev1 >> dev2;
connection[dev1][dev2] = 1;
connection[dev2][dev1] = 1;
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < i; j++) {
if (connection[i][j]) unnion(i, j);
}
}
int ans = 0;
for (int i = 0; i < n; i++) {
if (find(i) == i) ans += 1;
}
cout << ans << endl;
return 0;
}
三、三面
1.实习做了什么,获得了什么
实践技能:实习是一个将学习应用到实际工作中的机会。通过实习,您将有机会运用学到的理论知识,在真实的工作环境中进行实践,并提升相关技能。这包括专业技能,如编程、数据分析等,以及软技能,如沟通、团队合作等。
自我认知和成长:实习是一个自我认知和成长的机会。通过实践和面对挑战,您将能够更好地认识自己的优势和不足,并有机会发展和提升自己的能力。实习经历可以帮助您建立自信和适应性,为将来的职业生涯打下坚实的基础。
2.遇到的难题怎么解决的
3.对公司的了解
我觉得能进公司的人都是很优秀的人 跟优秀的人在一块共事我觉得很开心 自己也会变得优秀,公司是个很大的平台 我们无论是往哪个方向走都可以提供一个良好的平台 无论是做技术还是人力资源等等 当然我目前是想走技术路线的
核心网:华为云核心网是华为云服务的重要组成部分,它构建了强大的网络基础架构,为用户提供高性能、可靠的云计算环境。该网络架构采用了先进的技术,并结合了多种功能和服务,使华为云核心网成为行业的领先者之一。
核心网(Core Network)位于计算机网络中的网络层(Network Layer),也可以看作是TCP/IP协议栈中的网络层。网络层负责处理数据包的路由选择和转发,为不同主机之间提供可靠性和连接性。
核心网是一个高度复杂的网络系统,用于连接不同类型的网络和设备,例如运营商的网络、企业内部网络以及互联网等。它扮演着数据传输、流量调度和信令传递等重要角色,为用户提供诸如互联网接入、通信服务、数据传输等多种功能。
在TCP/IP协议中,核心网使用IP协议进行数据包的传输与路由选择。它通过路由器和交换机等设备来实现数据的转发和交换,并借助路由协议进行路由表的维护和更新。核心网的设计和运营需要考虑大规模网络的管理、安全性、性能优化等方面的问题,以满足用户对网络速度、可靠性和服务质量的要求。
华为云核心网提供了稳定的网络连接和传输功能,使得数据能够在不同的数据中心之间高效、安全地传输。它还具备强大的网络安全和隔离机制,保护用户的数据和资源免受恶意攻击和非法访问的威胁。
另外,华为云核心网基于全球骨干网络,通过网络优化和调度,实现了全球范围内的高速、低延迟的云计算连接。用户可以享受到稳定可靠的网络服务,实现多地区的业务需求。
华为云核心网还提供了丰富的功能和服务,包括虚拟专用云、弹性IP、私有网络互连和防火墙等,为用户构建和管理复杂的网络架构提供了综合解决方案。
总之,华为云核心网凭借其先进的技术、强大的网络连接和传输能力,以及多样化的功能和服务,为用户提供了出色的云计算环境。它在提供高性能、高可靠性的云服务方面发挥着重要的作用,并对用户的业务发展起到了积极的推动作用。
4.哪门课学的好,怎么学的,如果回到过去,你要怎么学
6.进入公司要学习新业务,会遇到挑战你怎么看
学习曲线:学习新业务肯定会有一个学习曲线,尤其是在进入一个陌生行业或领域时。您可能需要学习新的专业术语、业务流程和相关技能。这可能需要花费一些时间和努力,但随着经验的积累,您将逐渐适应并掌握新的业务。
挑战意味着成长:挑战是成长的机会。面对新业务的挑战,您需要思考解决问题的方法和策略。这有助于培养您的解决问题的能力、创新能力和适应能力。通过克服挑战,您将不断提升自己,并获得更多的经验和技能。
学习机会:学习新业务可以给您带来新的学习机会。您将有机会了解不同的行业和领域,并接触到新的知识和技术。这将增加您的知识广度,并为将来可能面临的机会和挑战做好准备。
请教和合作:在学习新业务的过程中,不要害怕向同事、上级或其他专业人士寻求帮助和指导。他们可能拥有更丰富的经验和知识,愿意与您分享并提供支持。积极与他人合作和交流,可以加速您的学习进程,并加深对新业务的理解。
自信和积极态度:面对挑战,保持自信和积极的态度非常重要。相信自己的能力并相信自己可以学会新的业务。与此同时,保持开放的心态,接受新的观点和方法,愿意不断学习和改进。
7.解决了什么难题最能体现出你的技术实力
我这边只能展现我解决问题的能力
8.职业规划
1年:
掌握核心编程语言和技术:在第一年,您应该专注于深入学习和掌握核心的编程语言和技术,如Java、Python、C++等,以及相关的开发框架和工具。同时,了解软件开发的基本流程和团队协作方式。
参与项目开发:争取参与不同的项目开发,并积极投入到实际的软件开发任务中。通过实践中的经验,熟悉项目开发的流程、沟通和协作技巧,并提升自己的编程能力和问题解决能力。
建立技术基础:在这一年中,建立起坚实的技术基础,包括掌握常用的开发工具、版本控制系统和调试技巧。此外,关注行业的最新发展和趋势,了解新兴技术和领域。
3年:
深入技术领域:在前三年,您可以选择在特定的技术领域进行深入研究和学习,如移动端开发、云计算、大数据、人工智能等。通过专注于一个领域,成为该领域的专家,并具备解决复杂问题和设计系统架构的能力。
担任技术角色:根据您的兴趣和能力,争取在团队中担任技术角色,如技术负责人、架构师或项目经理。这将使您有机会领导团队、参与决策,并在项目中发挥更大的影响力。
探索新技术和方法:继续保持对新技术和方法的开放态度,参与行业的培训、研讨会和社区活动,与其他专业人士交流和分享经验。通过不断学习和实践,扩展自己的技术视野,并应用到实际的项目中。
5年:
带领团队:在五年时间内,您可以朝着带领团队的方向发展。作为资深软件开发人员,您可以带领和指导新人,分享经验和知识,推动团队的整体成长和发展。同时,积极参与项目管理和决策,推动业务发展和产品创新。
拓展领域和市场:五年的经验使您得以更好地了解市场需求和趋势。您可以考虑在不同的业务领域进行尝试,拓展自己的技术广度和业务深度。通过与客户和合作伙伴交流合作,拓展业务网络和资源。
深造与终身学习:在职业的五年时间内,持续深造和终身学习是非常重要的。关注领域内的最新技术和趋势,参与相关的证书考试和培训课程,不断提升自己的技术素养和管理能力。同时,积极参与行业组织和社区,扩展人脉和学习机会。
9.为什么不去读博?
读博给我的反馈没有工业界来的直接 读博是为了一个未知的结果 一个网络模型去调研 实验等等 完事还得写好故事 把论文很好的呈现出来 到最后发出来是要经过一个很长的时间沉淀的 而工业界不一样了 反馈很积极 那我在这段时间里拿下这个项目 两三个月是吧 ok直接项目结束 客户需求解决 ,自我价值得到了体现 我觉得很有意义 技术也提升了 我觉得我适合工业界。
10.介绍项目,科研项目主要实现的功能
表情识别走起
11.为了进入公司,做了哪些努力
自我学习和技能提升:您可以讨论您主动学习和提升自己的努力。这可能包括通过自学、在线课程、培训、参加相关研讨会和工作坊等方式来扩展自己的知识和技能。强调您的自我驱动力和对持续学习的承诺。
项目经验和实践:描述您在个人项目、学校项目或志愿者工作中积累的实践经验。说明您如何主动参与不同类型的项目,如开发个人应用、参与团队项目、为开源项目进行贡献等。强调您在这些项目中取得的成果和能力的增长。
网络和行业参与:提及您积极参与行业相关的社群、论坛和网络平台。这可以包括参与技术社区、在开源项目中建立联系、参加行业活动和会议等。强调您通过这些参与展示了您对行业和技术的热情,并与其他领域专业人士建立了联系。
实习和兼职经验:如果您有相关的实习或兼职经验,强调您在这些机会中学到的知识和技能,以及如何与团队合作和应对工作挑战。描述您如何将这些经验应用到实际工作中,并为您进入新公司带来的价值。
深入了解公司和行业:突出您对目标公司和行业的研究和了解。分享您在准备面试过程中所进行的调查和学习,包括研究公司的产品和服务、行业趋势和竞争情况等。这表明您对行业有浓厚的兴趣,并认识到加入这个特定公司对您的职业目标有何重要性。
13.设计模式有了解吗
单例模式:
工厂模式:
14.看过哪些书?课程和非课程的?
15.我觉得你不喜欢软件开发
我觉得我喜欢,因为编程的逻辑、创造力和问题解决能力会让我有很大思维能力上的提升,会对一件事情的看法到一个相对微观的地步,并且通过编程可以解决现实世界的问题(最基本的,抛开业务不谈,我们处理一个文件、表格、都可以直接代码实现)。
因为软件开发可以与科技紧密联系 紧跟时代潮流 我们是科技公司 那有什么新鲜的科技玩意 身处这个行业我觉得会及时接触到这些东西 这是个科技不断迭代更新的时代,我们身处这个行业就不会落后于时代。让我觉得我在跟世界一块进步。开发行业提供了广泛的学习资源和发展机会,我非常乐于接受新的技术和工具,愿意不断学习和提升自己的技能。
16.加班看法
网上说的我是不太认同的。 只有亲身经历体验了 我相信自己的眼光 而且有项目也可以体现出一个公司的活力, 一直是在发展的。年轻就应该多提升自己。跳出舒适区才能更快成长,而且每个人根据自身周围环境,自身情况,对加班的看法也是不一样的。
运动 音乐 无压力,热爱读书 生活 民族企业 加班没啥 良性竞争 跑步中 跳出舒适区,年轻 多锻炼自己的能力,实现自我价值等等
优缺点:争强好胜心,过于谨慎,慢热。
反问:
1)您也见过很多的应届生,您根据您的经验,以过来人的角度, 跟您聊天也比较轻松加愉快,能不能给我这个毕业后初入职场提一些建议,就假设我已经进了咋们公司了,随便一个角度,技术、工作汇报等等各方面都行,您觉得重要的点。
2)公司这边支持提前过来实习吗?
此项目是在学习计算机网络和操作系统过程中开发的一个运行在Linux系统下的轻量级Web服务器,一个Web Server就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。其主要功能是通过HTTP协议与客户端(通常是浏览器(Browser))进行通信,来接收,存储,处理来自客户端的HTTP请求,并对其请求做出HTTP响应,返回给客户端其请求的内容(文件、网页等)或返回一个Error信息。
项目由IO多路复用模块、定时器模块、线程池模块组成。实现了浏览器访问服务器,获取服务器资源的功能。
项目的框架采用的Preactor 的事件处理模式。
在主线程里通过IO多路复用监听多个文件描述符上的事件。主线程负责连接的建立和断开,同时将读写和逻辑处理任务加入线程池里的任务队列,由线程池里的工作线程负责完成相应操作实现任务的并行处理。此外,通过定时器来清除不活跃的连接减少高并发场景下不必要的系统资源的占用【文件描述符的占用、维护一个TCP连接所需要的资源】,通过及时释放资源来确保服务器在高并发场景下也能够稳定可靠。对于到达的HTTP报文,采用了有限状态机和正则表达式进行解析,
资源的响应则通过 集中写 和 内存映射 的方式进行传输。通过构建线程池完成多个读写任务和逻辑处理任务的并行处理。
通过这个项⽬,我学习了两种Linux下的⾼性能⽹络模式,熟悉了Linux环境下的编程。
通常用户使用Web浏览器与相应服务器进行通信。在浏览器中键入“域名”或“IP地址:端口号”,浏览器则先将你的域名解析成相应的IP地址或者直接根据你的IP地址向对应的Web服务器发送一个HTTP请求。这一过程首先要通过TCP协议的三次握手建立与目标Web服务器的连接,然后HTTP协议生成针对目标Web服务器的HTTP请求报文,通过TCP、IP等协议发送到目标Web服务器上。
I/O 处理单元是服务器管理客户连接的模块。它通常要完成以下工作:等待并接受新的客户连接,接收
客户数据,将服务器响应数据返回给客户端。但是数据的收发不一定在 I/O 处理单元中执行,也可能在
逻辑单元中执行,具体在何处执行取决于事件处理模式。
一个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传递给 I/O 处理单元或者直
接发送给客户端(具体使用哪种方式取决于事件处理模式)。服务器通常拥有多个逻辑单元,以实现对
多个客户任务的并发处理。网络存储单元可以是数缓存和文件,但不是必须的。
请求队列是各单元之间的通信方式的抽象。I/O 处理单元接收到客户请求时,需要以某种方式通知一个
逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处
理竞态条件。请求队列通常被实现为池的一部分。
首先是服务器的一个参数初始化操作。通过构造WebServer这个对象传递参数进行服务器相关参数的设定,主要参数有设置定时器超时时间、设置Epoll触发模式、设置线程池的线程数量。然后是通过设定的参数对服务器的各个模块进行初始化。主要有线程池、IO复用、HTTP对象、阻塞队列等模块。
线程池采用RAII手法,在构造时创建线程,析构时唤醒所有睡眠的线程使其退出循环。IO复用是对epoll函数调用的一个封装。HTTP对象主要设置文件存放的相关路径。缓冲区和阻塞队列主要完成指定大小的参数设定。
服务器各个模块初始化完成之后就是主线程里IO复用监听事件的循环。监听事件有新连接到达、读事件和写事件和异常事件。根据不同的事件进行一个任务处理。
当新连接到达的时候,通过调用accept取出新连接(ET模式下需要循环操作),将新连接的文件描述符用来初始化HTTP连接(套接字地址和文件描述符绑定到一个HTTP对象),完成绑定定时器的初始化,同时添加监听读事件,设置其文件描述符为非阻塞。
当有异常事件发生的时候,关闭该连接同时移除监听事件和定时器。
当触发读事件的时候,调整定时器的定时时间,将读任务放入线程池的任务队列当中去。这个时候线程池对象里的多个线程,处于一个睡眠或者竞争任务并执行的过程,任务加入到任务队列当中去时会发送一个唤醒信号,如果有睡眠的线程则会被唤醒,进入循环里探测任务队列是否为空,取出任务并执行或者队列为空继续睡眠。
线程执行读任务函数主要是完成一个非阻塞读取调用直到读完,将数据缓存在用户缓冲区中,接着执行一个消息解析的操作,根据HTTP解析是否成功的判断来决定重新注册写事件还是读事件。如果解析失败那么重新注册读事件等待下次读取更多数据直到一个完整的HTTP请求。如果是解析成功的话就制作响应报文并且注册写事件,等待内核缓冲区可写触发事件时,将其写入内核缓冲区。
这部分的重点是逻辑处理的过程,也就是HTTP解析和HTTP报文的制作?
解析采用的是状态机和正则表达式,每次都读取以\r\n结尾的一段字符串,通过状态机来判定获取的字符串是属于HTTP请求的哪一部分,再跳转到相应的函数进行解析,如果读取的字符串没有以\r\n结尾则认为此次数据获取不完整,返回解析失败重新注册读事件。如果解析完成之后则根据解析过程中保存的相应信息,制作响应报文,通过 集中写 将资源文件和响应报文分别发送回客户端。
web服务器通过socket监听来自用户的请求;
很多用户会尝试去connect这个webserver上正在监听的port,而监听到的这些连接会排队等待被accept,由于用户连接请求时随机到达的异步事件,每当监听socket,监听到新的客户连接并放入监听队列,我们都需要告诉我们的web服务器有新连接过来,accept这个连接,并分配一个逻辑单元来处理这个用户请求,而且我们在处理这个请求的同时,还需要继续监听其他客户的请求并分配其另一逻辑单元来处理(并发,同时处理多个事件,后面会提到使用线程池实现并发),服务器通过epoll这种IO复用技术来实现对监听socket和连接socket的同时监听。
服务器程序通常需要处理三类事件:I/O事件,信号及定时事件。
同步IO:在一个线程中,cpu执行代码速度很快,然后一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作,这种称为同步IO;
异步IO:当代码执行一个耗时的IO操作时,只发出IO指令,并不等待IO结果,然后去执行其他代码,一段时间后,当IO返回结果时,在通知CPU进行处理。
要求主线程只负责监听 文件描述符上是否有事件发生(可读、写),若有则立即通知工作线程,将socket可读可写事件放入请求队列,交给工作线程处理,此外主线程不做任何操作,什么读写数据、接受新的连接、处理客户请求啥的都在工作线程中完成。以同步IO(epoll_wait为例)实现的reactor模式工作流程:
1)主线程往内核事件表中注册socket上的读就绪事件,然后主线程调用epoll_wait等待socket有数据可读;
2)socket上有数据可读时,epoll_wait通知主线程,主线程将可读事件放入请求队列,睡在请求队列上的某个工作线程被唤醒,从socket读取数据,然后处理请求;
3)往epoll内核事件表中注册该socket上的写就绪事件,主线程调用epoll_wait等待socket可写;
4)socket上有数据可写时,epoll_wait通知主线程,将可写事件放入请求队列,睡在请求队列上的某个工作线程被唤醒,往socket上写入服务器处理客户请求的结果。
将所有的I/O操作都交给主线程和内核来处理(进行读、写),工作线程仅负责处理逻辑,如主线程读完成后,选择一个工作线程来处理客户请求。使用异步IO模型(aio_read, aio_write为例),实现proactor模式的工作流程是:
1)主线程调用aio_read函数往内核注册 socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读完成时如何通知应用程序;
2)主线程处理其他逻辑 ( 因为是异步嘛,所以主线程肯定忙其他事情去了!!)
3)当socket上的数据被读入用户缓冲区后,内核向应用程序发送一个信号,通知应用程序数据可用了,然后信号处理函数选一个工作线程来处理这个请求;
4)工作线程处理完客户请求后,调用aio_write函数向内核注册 socket上写完成事件,并告诉内核用户写缓冲区位置,以及写操作完成时如何通知应用程序;
5)主线程继续处理其他逻辑 ( 因为是异步嘛,所以主线程肯定忙其他事情去了!!)
6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,然后信号处理函数选一个工作线程来善后。
通常使用同步I/O模型(epoll_wait)实现reactor,异步I/O(aio_read和aio_write)实现proactor。此项目中使用同步IO模拟proactor事件处理模式。原理是:主线程执行数据读写操作,读写完成后主线程向工作线程通知这一“完成事件”,从工作线程角度看,他们就直接得到了数据读写结果,接下来只要做对读写结果进行逻辑处理。
1)主线程往epoll内核事件表中注册socket上的读就绪事件,然后调用epoll_wait等待socket上有数据可读;
2)有数据可读时epoll_wait通知主线程,主线程从socket循环读取数据,读完后将数据封装成一个请求对象插入请求队列。睡在请求队列上的工作线程被唤醒,处理请求;
3)然后往epoll内核事件表中注册socket上的写就绪事件,主线程调用epoll_wait等待socket可写,可写时epoll_wait通知主线程,主线程往socket上写入服务器处理客户请求的结果;
select和poll,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需将整个集合拷贝到内核态,epoll则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需执行一个系统调用,所以在有很多短期活跃连接的情况下,epoll可能慢于select和poll,;
select使用线性表描述文件描述符集合,文件描述符有上限;poll使用链表来描述;epoll底层通过红黑树来描述文件描述符集合,并且维护一个ready list,将事件表中已经就绪的事件添加到这里,在使用epoll_wait调用时,仅观察这个list中有没有数据即可。
select和poll的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行select或poll调用时,会遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理。
ET模式时,必须保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);且每次调用read和write的时候都必须等到它们返回EWOULDBLOCK(确保所有数据都已读完或写完)。
LT模式下只要内核缓冲区还有数据可读便会提醒(哪怕已经提醒过,针对同一事件可以多次提醒);
ET模式下,每一次事件到来只通知一次(针对一个事件只提醒一次而不是提醒多次),没有及时读取完,该部分剩余数据直到下次触发,才能被读取(有可能永远也读不到,如果没有再次触发文件描述符上的该事件);
因为ET模式下是无限循环读,直到出现错误为EAGAIN或者EWOULDBLOCK,这两个错误表示socket为空,不用再读了,然后就停止循环了,如果是阻塞,循环读在socket为空的时候就会阻塞到那里,主线程的read()函数一旦阻塞住,当再有其他监听事件过来就没办法读了,给其他事情造成了影响,所以必须要设置为非阻塞。
LT用于并发量小的情况:LT通知用户后,会一直保留fd,随fd增多,就绪链表越大,每次都要从头开始遍历找到对应的fd,并发量越大效率越低,而ET会将fd从就绪链表中删除;
Get是请求获取数据,不对服务器产生影响,所以是安全(不会破环资源)幂等的(多次执行相同操作,结果相同)
Post是向服务器提交数据,会对服务器产生影响,是不安全不幂等的。
get方法只产生一个TCP数据包,浏览器把请求头和请求数据一并发送出去,服务器响应200ok,而post会产生两个TCP数据包,浏览器会先将请求头 发给服务器,服务器响应100continue后,再发送请求数据,服务器响应200ok(返回数据);
GET请求参数会被保存在浏览器历史记录里,参数在URL中,而post通过请求体传递参数,参数不会被保留
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/,/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
利用线程池并发处理用户请求,主线程负责读写,工作线程(线程池中的线程)负责处理逻辑(HTTP请求报文解析等等),通过之前的代码,将listenfd上到达的连接 通过accept接收,并返回一个新的socket文件描述符connfd用于和用户通信,对用户请求返回响应,同时将这个connfd注册到内核事件表中,等用户发来请求报文。
过程是:通过epoll_wait发现这个connfd上有可读事件了(EPOLLIN),主线程就将这个HTTP的请求报文读进这个连接socket的读缓存中users[sockfd].read(),然后将该任务对象(指针)插入线程池的请求队列中pool->append(users + sockfd),线程池的实现还需依靠锁机制以及信号量机制来实现线程同步,保证操作的原子性。
线程同步机制:临界区 互斥量 信号量 事件
线程池中的工作线程是一直等待吗?
阻塞等待的模式下为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上
你的线程池工作线程处理完一个任务后的状态是什么?
这里要分两种情况考虑(1) 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态(2) 当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格
问服务器如何处理高并发的问题对子线程循环调用来解决高并发的问题的。
通过子线程的run调用函数进行while循环,让每一个线程池中的线程永远都不会终止,他处理完当前任务就去处理下一个,没有任务就一直阻塞在那里等待。这样就能达到服务器高并发的要求
若一个客户请求需占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?
会影响接下来的客户请求,因为线程池内线程的数量是有限的,如果客户请求占用线程时间过久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。
应对策略:我们可以为线程处理请求对象设置处理超时时间, 超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。
线程数目:CPU是4核的,若是CPU密集型任务(如视频剪辑等),线程池中线程数量也设置为4,若是IO密集型任务,一般多于CPU核数。
主线程使用某种算法来主动选择子线程。最简单、最常用的算法是随机算法和 Round Robin(轮流选取)算法,但更优秀、更智能的算法将使任务在各个工作线程中更均匀地分配,从而减轻服务器的整体压力。
主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在该工作队列上。当有新的任务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线程将获得新任务的”接管权“,它可以从工作队列中取出任务并执行之,而其他子线程将继续睡眠在工作队列上。
通过以上操作,我们已经对读到的请求做好了处理,然后也对目标文件的属性作了分析,若目标文件存在、对所有用户可读且不是目录时,则使用 mmap将其映射 到内存地址m_file_address处,并告诉调用者获取文件成功FILE_REQUEST。 接下来要做的就是根据读取结果对用户做出响应了,也就是到了process_write(read_ret);这一步,该函数根据process_read()的返回结果来判断应该返回给用户什么响应,我们最常见的就是404错误了,说明客户请求的文件不存在,除此之外还有其他类型的请求出错的响应,具体的可以去百度。然后呢,假设用户请求的文件存在,而且已经被mmap到m_file_address这里了,那么我们就将做如下写操作,将响应写到这个connfd的写缓存m_write_buf中。
首先将状态行写入写缓存,响应头也是要写进connfd的写缓存(HTTP类自己定义的,与socket无关)中的,对于请求的文件,我们已经直接将其映射到m_file_address里面,然后将该connfd文件描述符上修改为EPOLLOUT(可写)事件,然后epoll_wait监测到这一事件后,使用writev来将响应信息和请求文件聚集写到TCP Socket本身定义的发送缓冲区(这个缓冲区大小一般是默认的,但我们也可以通过setsockopt来修改)中,交由内核发送给用户。OVER!
HTTP报文可以拆分为请求行、头部字段、请求体。每个部分之间都通过特殊界限符划分。在我们获得一个数据包(以\r\n结尾的数据包)的时候可以根据状态机的状态变量判断如何处理当前的数据包,并且在执行完相应操作后设置状态变量进行状态转移完成整个报文的解析工作。每个状态都有一系列的转移,每个转移与输入和另一状态相关。当输入进来,如果它与当前状态的某个转移相匹配,机器转换为所指的状态,然后执行相应的代码。
传统应用程序的控制流程基本是按顺序执行的:遵循事先设定的逻辑,从头到尾的执行。简单来说如果想在不同状态下实现代码跳转时,就需要破坏一些代码,会很复杂;
process_read()函数的作用就是将类似上述例子的请求报文进行解析,因为用户的请求内容包含在这个请求报文里面,只有通过解析,知道用户请求的内容是什么,是请求图片,还是视频,或是其他请求,我们根据这些请求返回相应的HTML页面等。
项目中使用主从状态机的模式进行解析,从状态机(parse_line)负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。每解析一部分都会将整个请求的m_check_state状态改变,状态机也就是根据这个状态来进行不同部分的解析跳转的:
parse_request_line(text),解析请求行,也就是GET中的GET /562f25980001b1b106000338.jpg HTTP/1.1这一行,或者POST中的POST / HTTP1.1这一行。通过请求行的解析我们可以判断该HTTP请求的类型(GET/POST),而请求行中最重要的部分就是URL部分,我们会将这部分保存下来用于后面的生成HTTP响应。
parse_headers(text);,解析请求头部,GET和POST中空行以上,请求行以下的部分。
parse_conten数t(text);,解析请求据,对于GET来说这部分是空的,因为这部分内容已经以明文的方式包含在了请求行中的URL部分了;只有POST的这部分是有数据的,项目中的这部分数据为用户名和密码,我们会根据这部分内容做登录和校验,并涉及到与数据库的连接。
得到一个完整的,正确的HTTP请求时,就到了do_request代码部分,我们需要首先对GET请求和不同POST请求(登录,注册,请求图片,视频等等)做不同的预处理,然后分析目标文件的属性,若目标文件存在、对所有用户可读且不是目录时,则使用mmap将其映射到内存地址m_file_address处,并告诉调用者获取文件成功。
抛开mmap这部分,先来看看这些不同请求是怎么来的:
假设你已经搭好了你的HTTP服务器,然后你在本地浏览器中键入localhost:9000,然后回车,这时候你就给你的服务器发送了一个GET请求,什么都没做,然后服务器端就会解析你的这个HTTP请求,然后发现是个GET请求,然后返回给你一个静态HTML页面,也就是项目中的judge.html页面,那POST请求怎么来的呢?这时你会发现,返回的这个judge页面中包含着一些新用户和已有账号这两个button元素,当你用鼠标点击这个button时,你的浏览器就会向你的服务器发送一个POST请求,服务器通过检查action来判断你的POST请求类型是什么,进而做出不同的响应。
因为缓冲区的大小是固定的大小,而我们通常是一次性将数据全部读取到缓冲区,那么就有可能装不下数据,所以需要临时创建一个缓冲区来缓解,将存不下的放到临时缓冲区,这样就可以一次性将所有的数据读入,这里利用临时缓冲区的技术是一个 分散读 的技术,即将数据分散读取到内存中不同的位置
当固定的缓冲区想要继续写数据的时候,发现剩余的位置不够写的时候那么就可以先把数据写入到临时缓冲中,再将临时数组的数据读取到固定缓冲区来处理
如果我们想要写入数据,那么此时可以利用的空间就是最前面的部分和最后面的部分的位置,但是写入数据一定要连续,所以唯一的办法就是将中间的数据移动到最前面,这样就可以将空闲的区域连接在一起,方便后面的写数据。具体的实现就是将读指针到写指针之间的数据复制到最前面,再更改读指针和写指针的位置,这就是利用一个缓冲实现自动增长的原理
因为读设置的是边沿触发,需要一次性读完所有数据。所以定义一个大小1024的容器,但是有可能放不下,所以在定义一个65535的备用容器,采用分散读的形式,读到这两个容器里。然后整合这两个容器里数据(因为后序要吧数据取出来进行解析请求,所以需要合到一起):如果第一个能放下,写指针向后移动;如果放不下,看看能否凑出来,能凑则凑,凑不出来,第一个容器自动扩容resize。这样所有数据都在第一个容器里了。没必要一开始就用大容器:占内存,影响性能。
解析http请求,生成http响应响应首行,响应头在buffer里,响应体在内存映射里
写数据:边沿触发,一次性把数据从缓冲区buffer、内存映射写到socket中,所以需要分散写。
因为数据的处理只能放到固定大小的缓冲中进行处理,即上述的1024字节的缓冲中,那么如果很多数据都读取到这块区域的话,那么肯定是放不下的,我们就可以利用一个临时的缓冲区,把放不下的数据先放到临时的缓冲的位置,等到1024字节大小的内存有剩余的空间的时候我再将临时缓冲区的数据放入到1024的位置进行处理
HTTPS=HTTP+TLS/SSL
TLS/SSL协议位于应用层协议和TCP之间,构建在TCP之上,由TCP协议保证数据传输版的可靠性,任何数据到权达TCP之前,都经过TLS/SSL协议处理。https是加密传输协议,可以保障客户端到服务器端的传输数据安全。用户通过http协议访问网站时,浏览器和服务器之间是明文传输,这就意味着用户填写的密码、帐号、交易记录等机密信息都是明文,随时可能被泄露、窃取、篡改,被第三者加以利用。安装SSL证书后,使用https加密协议访问网站,可激活客户端浏览器到网站服务器之间的"SSL加密通道"(SSL协议),实现高强度双向加密传输,防止传输数据被泄露或篡改。
HTTPS的SSL连接过程
1.客户端提交https请求;
2.服务器响应客户,并把证书公钥发给客户端;
3.客户端验证证书公钥的有效性;
4.有效后,生成一个会话密钥;
5.用证书公钥加密这个会话密钥后,发送给服务器;
6.服务器收到公钥加密的会话密钥后,用私钥解密,回去会话密钥;
7.客户端和服务器利用这个会话密钥加密要传输的数据进行通信;
两种方法:小根堆 升序链表
如果某一用户connect()到服务器之后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。这时候就应该利用定时器把这些超时的非活动连接释放掉,关闭其占用的文件描述符。这种情况也很常见,当你登录一个网站后长时间没有操作该网站的网页,再次访问的时候你会发现需要重新登录。
项目中使用的是SIGALRM信号来实现定时器,利用alarm函数周期性的触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。alarm函数会定期触发SIGALRM信号,这个信号交由sig_handler来处理,每当监测到有这个信号的时候,都会将这个信号写到pipefd[1]里面,传递给主循环,在主循环中处理啥的;
但基于升序链表存在缺点:
每次遍历添加和修改定时器的效率偏低(O(n)),使用最小堆结构可以降低时间复杂度降至(O(logn))。
每次以固定的时间间隔触发SIGALRM信号,调用tick函数处理超时连接会造成一定的触发浪费,举个例子,若当前的TIMESLOT=5,即每隔5ms触发一次SIGALRM,跳出循环执行tick函数,这时如果当前即将超时的任务距离现在还有20ms,那么在这个期间,SIGALRM信号被触发了4次,tick函数也被执行了4次,可是在这4次中,前三次触发都是无意义的。对此,我们可以动态的设置TIMESLOT的值,每次将其值设置为当前最先超时的定时器与当前时间的时间差,这样每次调用tick函数,超时时间最小的定时器必然到期,并被处理,然后在从时间堆中取一个最先超时的定时器的时间与当前时间做时间差,更新TIMESLOT的值。
除了小根堆实现之外,还有使用时间轮和基于升序链表实现的定时器结构。
基于升序链表实现的定时器结构按照超时时间作为升序做排序,每个结点都链接到下一个结点,由于链表的有序性以及不支持随机访问的特性,每次插入定时器都需要遍历寻找合适的位置,而且每次定时器调整超时时间时也需要往后遍历寻找合适的位置进行挪动,遍历操作效率较低。同时也需要通过多次计时(通过信号中断实现)来检测链表中的定时器是否超时并进行处理。 优化方式可以通过利用IO复用的超时选项。
不同于采用单条链表实现的定时器每次插入更新进行遍历来寻找合适的位置进行操作,时间轮利用哈希思想,将相差整个计时周期整数倍的定时器散列到不同的时间槽中,减少链表上的定时器数量避免过多的顺序遍历操作。时间轮通过提高时间槽的数量来提高查找效率【使每个时间槽里的链表长度尽可能短】,通过减少计时间隔来提高定时器的精度【使定时时间尽可能准确】,设计时需要考虑这两个因素,时间槽多但是定时器数量少则会造成效率低下,可以通过多级时间轮优化,但是实现起来复杂。
小根堆实现的定时器结构,每次取堆头都是最短超时时间,能够利用IO复用的超时选项,每次的计时间隔动态调整为最短超时时间,确保每次调用IO复用系统调用返回时都至少有一个定时事件的超时发生或者监听事件的到达,有效地减少了多余的计时中断(利用信号中断进行计时)。最主要是确保每次定时器插入、更新、删除都能实现稳定的logn时间复杂度【该时间复杂度是调整堆的代价,定时器的定位利用哈希表实现O1查找】,而不像链表一样依赖于定时器数量的大小以及时间轮需要兼顾时间精度和效率的问题。
Webbench 首先 fork 出多个子进程,每个子进程都循环做 web 访问测试。子进程把访问的结果通过pipe写端告诉父进程,父进程做最终的统计结果。
一个服务器项目,你在本地浏览器键入localhost:8000发现可以运行无异常还不够,你需要对他进行压测(即服务器并发量测试),压测过了,才说明你的服务器比较稳定了。 用到了一个压测软件叫做Webbench,可以直接在Gtihub里面下载,解压,然后在解压目录打开终端运行命令(-c表示客户端数, -t表示时间)
虚拟机上创建子进程,同时也在虚拟机上运行服务器(创建子进程会消耗资源,可以在另一机器上fork,来访问此服务器)。
在单核【2.4GHz】2G的云服务器上实现了并发量2W+,QPS8K+的效果。
在满载(开始有连接崩溃的临界)的情况下,内存占用达到60%,这个和缓冲区大小的设置有关,可以通过进一步提高其大小来提高性能。同时CPU占用率较高(45%),推测和单核进行频繁的上下文切换(多线程)有关。
整个项目更偏向于IO密集型。