(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来
的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单
元。
(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。
(3)可以有const指针,但是没有const引用;
(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(5)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(6)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(7)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
(8)指针和引用的自增(++)运算意义不一样;
(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLa5vLaD-1649731834089)(C:\Users\纠结伦\AppData\Roaming\Typora\typora-user-images\image-20220218143849841.png)]
所谓稳定性是指待排序的序列中有两元素相等,排序之后它们的先后顺序不变.
关于稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。桶排序、计数排序(插捅毛龟唧唧)
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。(快选系队)
封装:封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问。
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
多态:调用一个同名的函数根据上下文而表现出不同的状态。有静态多态和动态多态(静态多态有:函数重载和函数模板,动态多态通过虚函数实现。)
1应用层HTTP/HTTPS协议/DNS协议/DHCP协议(UDP)
2传输TCP/UDP/
3网络IP/ICMP/ARP协议/NAT协议
1.进程要分配一大部分的内存,而线程只需要分配一部分栈就可以了.
2.一个程序至少有一个进程,一个进程至少有一个线程.
3.进程是资源分配的最小单位,线程是程序执行的最小单位。
4.一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.
线程独享:1.线程ID 2.寄存器 3.栈 4.线程优先级
线程也可以创建私有数据,通过pthread_key_create创建*
多线程私有数据pthread_key_create - 邶风 - 博客园 (cnblogs.com)
一文读懂什么是进程、线程、协程 - 云+社区 - 腾讯云 (tencent.com)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YHs7fMQ-1649731834090)(C:\Users\纠结伦\AppData\Roaming\Typora\typora-user-images\image-20220227151343887.png)]
协程详解:
(32条消息) C++与协程_?。。。。的博客-CSDN博客
协程优点:
1.内存小。协程只需要极少的栈内存(大概是2~5KB),默认情况下,线程栈的大小为1MB
2.效率高。
协程是用户态线程。
线程之间的切换受到操作系统的调度。
pc计数器需要从线程A的保存点切换到线程B的上次保存点继续执行,还需要保存线程A的当前上下文。这些事情均由操作系统内核完成。那么将意味着线程切换势必造成系统调用。由用户态->系统态->用户态这样两次上下文切换的过程。
协程之间的切换是由虚拟机或者语言层面上的调度器调度
其中的切换是用户态->用户态→用户态(中间这个是调度器代码)而且,因为协程的占用内存小,切换所需要保存/恢复上下文也小
3. 减少同步锁
为什么协程不需要使用锁?因为协程最终还是运行在线程上,本质上还是单线程运行,没有临界区域的话自然不需要锁的机制。多协程自然没有竞争关系。
协程是碰着阻塞式I\O会导致整个线程被挂起么?
是的,在协程中调用阻塞式i/o会阻塞整个线程,因为协程之间的调度是用户在协程中主动调用协程切换功能(比如yield语句)手动实现的,而不是系统自动强制执行的,如果一个协程调用了阻塞式i/o,这个协程就被阻塞了,哪里还有机会调用协程切换功能呢?既然无法调用协程切换功能去执行其它协程,也就意味着整个线程被阻塞了,所以协程中是不能直接调用任何会阻塞线程的功能的,需要进行封装。
对阻塞式操作的封装不是简单的让出协程执行权,因为一旦让出执行权,该协程就不会被继续执行,后续的阻塞式操作也就没有机会被完成了,正确的做法是新建一个线程或者从线程池中分配一个线程,在这个线程中执行需要的阻塞式操作,同时将当前协程休眠(让出执行权),在新线程中的操作完成后,再唤醒协程。
协程主要分为对称式(symmetric)、非对称(asymmetric)式两种(参见boost协程库),两者的主要区别在于:
1、对称协程只提供一种传递操作,用于在协程间直接传递控制,协程每次需要挂起时需要指定一个明确切换的目标协程,也就是说控制权只能在协程间跳转。
2、非对称协程提供调用和挂起两种操作,挂起时控制权返回给调用者。被调用的协程可以看成时从属于调用者,这种协程在日常使用中更常见。
————————————————
线程池线程数
CPU 密集型来说
理论上 线程数量 = CPU 核数(逻辑)
实际上,数量一般会设置为 CPU 核数(逻辑)+ 1(经验值)。
计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
I/O 密集型程序的最佳线程数就是:
最佳线程数 = CPU核心数 * (1/CPU利用率)
系统调用态更换过程
当客户程序执行到的API刚好是个系统调用或者涉及系统调用
此时就会产生0x80号中断,并且由用户态切换到内核态中
内核态中维护一张表,这张表对应着相应的系统调用与系统调用号,即系统调用表,记录函数入口地址
用户在进入内核态前会将对应的系统调用号,通过寄存器eax传入给内核态
然后内核空间根据系统调用号和系统调用表相对应,调用相应的系统调用
对应的函数执行完以后,再由内核态切换到用户态。
由于切换到内核态的时候,将用户空间的堆栈地址保存了下来,所以cpu的堆栈指针寄存器执行用户空间的堆栈地址就可以了,这是完成了内核态向用户态的切换;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agrHWC8o-1649731834091)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220310165939990.png)]
1.管道
管道分为有名管道和无名管道
无名管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系一般指的是父子关系。无明管道一般用于两个不同进程之间的通信。当一个进程创建了一个管道,并调用fork创建自己的一个子进程后,父进程关闭读管道端,子进程关闭写管道端,这样提供了两个进程之间数据流动的一种方式。
有名管道也是一种半双工的通信方式,但是它允许无亲缘关系进程间的通信。需要mkfifo创建管道文件
2.消息队列
消息队列是保存在内核中的链表,如果没有关闭消息队列它会一直存在内核中。发送消息时,数据会被分成一个个独立的固定大小的、双方约定好的数据类型的消息块,而不像管道那样的无格式字节流。
消息队列缺点:通信不及时,同时不适合较大数据的传输。每个消息体都有最大长度限制。同时通信时,会涉及到内核数据和用户数据的拷贝。
3.共享内存
共享内存就是不同进程可以将同一物理内存区域映射到各自的用户空间中。这段共享内存由一个进程创建,但多个进程都可以访问.共享内存是最快的IPC(进程间通信)方式,它是针对其它进程间通信方式运行效率低而专门设计的.它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步与通信.
4.信号量
为防止多进程竞争共享资源,而造成数据错乱。通过PV操作实现,实现资源的同步或互斥。P为加+1,V为减1。
5.信号
进程间通信机制中唯一的异步通信机制,可以在任何时候向某一个进程发送信号,一旦信号发生,进程用户对信号做出处理:1.执行默认操作、2.捕捉信号、执行相应的信号处理函数、3.忽略信号。
6.socket
跨网络与不同主机上的进程进行通信,也可以和本地进行通信。只需要指定函数的第一个参数,为AF_INET(IPV4或IPV6)或AF_LOCAL
ftok函数,共享内存,消息队列,信号量它们三个都是找一个中间介质
1、ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key,该路径可以随便设置。
2、该路径是必须存在的,ftok只是根据文件inode在系统内的唯一性来取一个数值,和文件的权限无关。
3、proj_id是可以根据自己的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255;
————————————————
版权声明:本文为CSDN博主「satellite13」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013485792/article/details/50764224
迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。
迭代器是一种类似于指针的存在,迭代器最重要就是对operator*
和operator->
进行重载
迭代器失效分三种情况考虑,也是分三种数据结构考虑,分别为数组型,链表型,树型数据结构。
**数组型数据结构:**该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);
**链表型数据结构:**对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).
树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
注意:经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V7wCUjPE-1649731834091)(C:\Users\纠结伦\AppData\Roaming\Typora\typora-user-images\image-20220218143205073.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M71QaW1T-1649731834092)(C:\Users\纠结伦\AppData\Roaming\Typora\typora-user-images\image-20220218143400032.png)]
1.阻塞式IO
2.非阻塞式IO
3.IO多路复用
类似与非阻塞,只不过轮询不是由用户线程去执行,而是由内核去轮询,内核监听程序监听到数据准备好后,调用内核函数复制数据到用户态
下图中select这个系统调用,充当代理类的角色,不断轮询注册到它这里的所有需要IO的文件描述符,有结果时,把结果告诉被代理的recvfrom函数,它本尊再亲自出马去拿数据
IO多路复用至少有两次系统调用,如果只有一个代理对象,性能上是不如前面的IO模型的,但是由于它可以同时监听很多套接字,所以性能比前两者高
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a89BOZFa-1649731834093)(C:\Users\纠结伦\AppData\Roaming\Typora\typora-user-images\image-20220218142903033.png)]
多路复用包括:
select:线性扫描所有监听的文件描述符,不管他们是不是活跃的。有最大数量限制(32位系统1024,64位系统2048)
poll:同select,不过数据结构不同,需要分配一个pollfd结构数组,维护在内核中。它没有大小限制,不过需要很多复制操作
(21条消息) poll函数详解_skypeng57的专栏-CSDN博客_poll函数
epoll:用于代替poll和select,没有大小限制。使用一个文件描述符管理多个文件描述符,使用红黑树存储。同时用事件驱动代替了轮询。epoll_ctl中注册的文件描述符在事件触发的时候会通过回调机制激活该文件描述符。epoll_wait便会收到通知。
4.信号驱动式IO
5.异步IO
调用函数
文件描述符数量
将文件描述符从用户传给内核
内核判断就绪的文件描述符
应用程序索引就绪文件描述符
poll[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PKKnXb4D-1649731834093)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220308105247629.png)]
工作模式
应用场景
https://cloud.tencent.com/developer/article/1862671
# 响应式(Reactive)
在 I/O 多路复用技术中,服务端程序(线程)需要维护一个 Socket 的集合(可以是数组、链表等),然后定期遍历这个集合。这样的做法在客户端 Socket 较少的情况下没有问题,但是如果接入的客户端 Socket 较多,比如达到上万,那么每次轮询的开销都会很大。
从程序设计的角度来看,像这样主动遍历,比如遍历一个 Socket 集合看看有没有发生写入(有数据从网卡传过来),称为**命令式的程序**。这样的程序设计就好像在执行一条条命令一样,程序主动地去查看每个 Socket 的状态。
![img](https://ask.qcloudimg.com/http-save/yehe-8916337/09a0f09f5102d81701a8b8aec4c9addc.png?imageView2/2/w/1620)
**命令式会让负责下命令的程序负载过重,例如,在高并发场景下,上述讨论中循环遍历 Socket 集合的线程,会因为负担过重导致系统吞吐量下降**。
与命令式相反的是响应式(Reactive),响应式的程序就不会有这样的问题。在响应式的程序当中,每一个参与者有着独立的思考方式,就好像拥有独立的人格,可以自己针对不同的环境触发不同的行为。
**从响应式的角度去看 Socket 编程,应该是有某个观察者会观察到 Socket 文件状态的变化,从而通知处理线程响应。线程不再需要遍历 Socket 集合,而是等待观察程序的通知**。
![img](https://ask.qcloudimg.com/http-save/yehe-8916337/4b62fbabca7efb189f031e0b14ae3bf6.png?imageView2/2/w/1620)
当然,最合适的观察者其实是操作系统本身,因为只有操作系统非常清楚每一个 Socket 文件的状态。原因是对 Socket 文件的读写都要经过操作系统。在实现这个模型的时候,有几件事情要注意。
\1. 线程需要告诉中间的观察者自己要观察什么,或者说在什么情况下才响应?比如具体到哪个 Socket 发生了什么事件?是读写还是其他的事件?这一步我们通常称为注册。
\2. 中间的观察者需要实现一个高效的数据结构(通常是基于红黑树的二叉搜索树)。这是因为中间的观察者不仅仅是服务于某个线程,而是服务于很多的线程。当一个 Socket 文件发生变化的时候,中间观察者需要立刻知道,究竟是哪个线程需要这个信息,而不是将所有的线程都遍历一遍
------
# 为什么用红黑树?
关于为什么要红黑树, 再仔细解释一下。考虑到中间观察者最核心的诉求有两个。
**第一个核心诉求,是让线程可以注册自己关心的消息类型**。
比如线程对文件描述符 =123 的 Socket 文件读写都感兴趣,会去中间观察者处注册。当 FD=123 的 Socket 发生读写时,中间观察者负责通知线程,这是一个响应式的模型。
**第二个核心诉求,是当 FD=123 的 Socket 发生变化(读写等)时,能够快速地判断是哪个线程需要知道这个消息**
所以,**中间观察者需要一个快速能插入(注册过程)、查询(通知过程)一个整数的数据结构,这个整数就是 Socket 的文件描述符**。综合来看,能够解决这个问题的数据结构中,跳表和二叉搜索树都是不错的选择。
因此,在 Linux 的 epoll 模型中,选择了红黑树。红黑树是二叉搜索树的一种,红与黑是红黑树的实现者才关心的内容,对于
我们使用者来说不用关心颜色,Java 中的 TreeMap 底层就是红黑树
纯右值就是C++98中的右值的概念,就是指临时变量和一些不跟对象关联的值,比如1+2就是指临时变量,true和‘c’就是一些和对象不相关的值,还有函数的返回值和lamdba函数等
将亡值指的是与右值引用相关的表达式。比如:std::move()的返回值、类型为T&&的函数返回值等。
————————————————
版权声明:本文为CSDN博主「StephenZou14」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/StephenZou14/article/details/77817114
移动语义前面已经介绍了,我们知道对象的移动语义的实现是依靠移动构造函数和移动赋值操作符。但是前提是你传入的必须是右值,但是有时候你需要将一个左值也进行移动语义(因为你已经知道这个左值后面不再使用),那么就必须提供一个机制来将左值转化为右值。在C++
中,std::move
就是专为此而生,看下面的例子:
vector<int> v1{1, 2, 3, 4};
vector<int> v2 = v1; // 此时调用复制构造函数,v2是v1的副本
vector<int> v3 = std::move(v1); // 此时调用移动构造函数,v3与v1交换:v1为空,v3为{1, 2, 3, 4}
可以看到,我们通过std::move
将v1
转化为右值,从激发v3
的移动构造函数,实现移动语义。
move
move的作用是将左值转换为右值,类似static_cast
forward
如果使用右值引用作为参数传递时会认定该参数为左值,如果需要传递右值就需要使用forward,forward 也称完美转发,他可以吧左值引用转换成左值,非左值引用转换为右值。
中断分为外中断和内中断。
外中断——就是我们指的中断——是指由于外部设备事件所引起的中断,如通常的磁盘中断、打印机中断等;
内中断——就是异常——是指由于 CPU 内部事件所引起的中断,如程序出错(非法指令、地址越界)。内中断(trap)也被译为“捕获”或“陷入”。
异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。
中断则是由于系统中某事件引起的,该事件与现行指令无关。
1.map始终保证遍历的时候是按key的大小顺序的,这是一个主要的功能上的差异。(有序无序)
2.时间复杂度上,红黑树的插入删除查找性能都是O(logN)而哈希表的插入删除查找性能理论上都是O(1),他是相对于稳定的,最差情况下都是高效的。哈希表的插入删除操作的理论上时间复杂度是常数时间的,这有个前提就是哈希表不发生数据碰撞。在发生碰撞的最坏的情况下,哈希表的插入和删除时间复杂度最坏能达到O(n)。
3.map可以做范围查找,而unordered_map不可以。
5.因为3,所以对map的遍历可以和修改map在一定程度上并行(一定程度上的不一致通常可以接受),而对unordered_map的遍历必须防止修改map的iterator可以双向遍历,这样可以很容易查找到当前map中刚好大于这个key的值,或者刚好小于这个key的值这些都是map特有而unordered_map不具备的功能。
extern关键字 - 知乎 (zhihu.com)
**extern放在变量和函数声明之前,表示该变量或者函数在别的文件中已经定义,提示编译器在编译时要从别的文件中寻找。**除此之外,extern还可以用来进行链接指定。
**(1) 声明外部变量。**在声明全局变量时,不同的文件在编译器编译时是不透明的,在A.c中定义 int i,同时在B.c中定义 int i,编译器编辑时是不会报错的,但是当链接linking…时会报错重复定义,链接是不同文件是透明的,因此在定义全局变量是不能够重复定义。当需要使用同一个全局变量时,如:在A.c中定义了int i,在B.c中需要调用i,只需要在B.c中声明extern int i,表示该变量在别的文件中已经定义,编译时便不会出错,在linking…的时候会自动去查找定义过的变量i。
**(2) extern函数声明。**Extern void fun() 暗示该函数可能在别的文件中定义过,它和定义为void fun(),没什么区别,其用处在于在复杂的项目用通过在函数前添加extern声明来取代利用include”*.h”来声明函数。
(3) extern c。
使用 extern 和包含头文件来引用函数有什么区别呢?
与 include 相比,extern 的引用方式比包含头文件要更简洁:
extern 引用另一个文件的范围小,想引用哪个函数就用 extern 声明哪个函数;
而 include 可以引用另一个文件的全部内容。
这样做的一个明显的好处是,
会提高程序编译(预处理)效率,节省时间。
在大型 C++ 程序编译过程中,这种差异是非常明显的。
————————————————
版权声明:本文为CSDN博主「一得同学」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44648216/article/details/117928026
[一次完整的HTTP请求过程 - 白日梦想家12138 - 博客园 (cnblogs.com)](https://www.cnblogs.com/xuzekun/p/7527736.html#:~:text=下面我们来详细看看这几个过程的具体细节: 1 域名解析 2 TCP连接(三次握手) 3,建立TCP连接之后,发起HTTP请求 4 服务器端响应http请求,浏览器得到html代码 5 浏览器解析html代码,并请求html代码中的资源 6 浏览器对页面进行渲染呈现给用户)
通用顶级域在1985年1月创立,当时共有6个通用顶级域,主要供美国使用:
权威域服务器比如百度,腾讯,谷歌goole.cn
1.最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数
GET比POST更不安全,因为参数直接暴露在URL中,所以不能传递敏感信息。
2.GET请求会被浏览器主动cache,而POST不会,除非手动设置。
3.GET产生一个TCP数据包;POST产生两个TCP数据包
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200;
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok
一般情况下 无论get、post 客户端只发一次HTTP请求。特殊情况下,POST 请求发送2次HTTP请求。
如果客户端需要跟服务端确认 一个大的文件上传等场景服务端是否接受,客户端发送 试探性的请求,请求头带上 Expect: 100 continue, 这多出来一个 HTTP 请求,而不是多一个tcp 数据包。
1、GET在浏览器回退时是无害的,而POST会再次提交请求。
2、GET产生的URL地址可以被Bookmark,而POST不可以。
3、GET请求会被浏览器主动cache,而POST不会,除非手动设置。
4、GET请求的参数会完整的被保存在历史记录里,POST不会。
5、GET请求参数放在URL中,POST放在request body中。
6、GET请求只能进行url编码,POST请求支持多种编码方式。
7、对于参数类型,GET只接受ASCII字符,而POST没有限制。
8、GET请求在URL中传递的参数是有长度限制的,而POST没有。
9、GET比POST更不安全,因为参数直接暴露在URL中,所以不能传递敏感信息。
最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数
GET产生一个TCP数据包;POST产生两个TCP数据包
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200;
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok
作者:dex2jar4script
链接:https://www.nowcoder.com/discuss/735035?source_id=discuss_experience_nctrack&channel=-1
来源:牛客网
server需要调用哪些api、client呢。(server写了socket() bind() listen() accept() read(),write() client写了socket(),connect(), read(),write() )
对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接(三次握手详情,请看《浅谈 TCP 三次握手》),最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)
在执行connect时,内核发现没有绑定端口,就会自动选择一个合适的端口绑定到socket。
shutdown与close
close和shutdown关闭TCP连接 - SegmentFault 思否
int close(int sockfd)
close函数会对套接字引用计数(引用了这个套接字描述符的进程数)减一,一旦发现套接字引用计数到0,就会对套接字进行彻底释放,并且会关闭TCP两个方向的数据流并回收连接和相关资源,是所谓的粗暴式关闭:
int shutdown(int sockfd, int howto)
shutdown函数可以单向或者双向的关闭连接,是所谓的优雅式关闭,howto来设置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wyv0T1HM-1649731834094)(5d5538141f29561dbcdcea17c393092.jpg)]
https://blog.csdn.net/qq_16933601/article/details/119360513
DMA
一个完整的DMA传输过程必须经过下面的4个步骤。
1.DMA请求
CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。
2.DMA响应
DMA控制器对DMA请求判别优先级及屏蔽,向总线裁决逻辑提出总线请求。当CPU执行完当前总线周期即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经响应,通过DMA控制器通知I/O接口开始DMA传输。
3.DMA传输
DMA控制器获得总线控制权后,CPU即刻挂起或只执行内部操作,由DMA控制器输出读写命令,直接控制RAM与I/O接口进行DMA传输。
4.DMA结束
当完成规定的成批数据传送后,DMA控制器即释放总线控制权,并向I/O接口发出结束信号。当I/O接口收到结束信号后,一方面停 止I/O设备的工作,另一方面向CPU提出中断请求,使CPU从不介入的状态解脱,并执行一段检查本次DMA传输操作正确性的代码。最后,带着本次操作结果及状态继续执行原来的程序。
由此可见,DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,使CPU的效率大为提高。
ELF文件在计算机科学中,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。
Step 1. 符号解析(Symbol resolution)
– 程序中有定义和引用的符号 (包括变量和函数等)
– 编译器将定义的符号存放在一个符号表( symbol table)中
– .symtab 节记录符号表信息,是一个结构数组
– 每个表项包含符号名、长度和位置等信息
– 链接器将每个符号的引用都与一个确定的符号定义建立关联
每个可重定位目标模块都有一个符号表,它包含了在模块中定义和引用的符号。有三种链接器符号:
例如,非static C函数和非static的C全局变量(指不带static的全局变量)
符号定义的实质就是指被分配了存储空间。为函数名即指其代码所在区;为变量名即指其所占的静态数据区。所有定义符号的值就是其目标所在的首地址。
符号解析也称符号绑定,目的是将每个模块中引用的符号与某个目标模块中的定义符号建立关联。
每个定义符号在代码段或数据段中都被分配了存储空间,将引用符号与定义符号建立关联后,就可在重定位时将引用符号的地址重定位为相关联的定义符号的地址。
本地符号在本模块内定义并引用,因此,其解析较简单,只要与本模块内唯一的定义符号关联即可。
Step 2. 重定位
– 合并相关.o文件。将多个代码段与数据段分别合并为一个单独的代码段和数据段
– 对定义符号进行重定位(确定地址),计算每个定义的符号在虚拟地址空间中的绝对地址
例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址
完成这一步后,每条指令和每个全局或局部变量都可确定地址
– 对引用符号进行重定位(确定地址),将可执行文件中符号引用处的地址修改为重定位后的地址信息
需要用到在.rel_data和.rel_text节中保存的重定位信息
(28条消息) c语言中总是从main函数开始,C语言总是从main函数开始执行吗_symbo i的博客-CSDN博客
(28条消息) 12. 重定位及动态链接_baidu_41667019的博客-CSDN博客_动态重定向
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FKrGK5OP-1649731834094)(3a057a2e230105283ba8f2f0d1cd7fc.png)]
main只是开发工具所规定的一个特殊函数名称而已,程序的入口点记录在可执行文件中的一个数据,该数据标明程序从哪个位置开始执行,这个数据是连接程序的时候由link.exe确定的,可以把程序的入口点 指定为任意函数,甚至可以自己编辑可执行文件修改程序的入口点。在默认情况下,link.exe会连接开发工具中带有的一个obj文件,并设置该obj中 的固定函数为程序的入口点,这个函数执行后会做一些初始化的事情,然后调用main函数。在执行连接的时候,如果不连接这个obj,程序中就可以没有 main函数。
程序最终生成的exe执行时,开始执行的是mainCRTStartup(或 wmainCRTStartup…以及其他)函数之一,而不是我们程序所写的main或WinMain等。连接器为什么要这样做?因为我们写的程序必须要使用到各种各样的运行时库函数才能正常工作,所有在执行我们自己写程序之前必须要先准备好所需要的一切库,之所以要连接它们是因为他们肩负着很重要的使命,就是初始化好运行时库,准备我们的程序执行时调用。
一个典型程序的大致运行步骤
*操作系统创建进程后,把控制权交到了程序入口,这个入口往往是程序运行库中的某个入口函数。
*入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造等等。
*入口函数在完成初始化之后,调用main函数,正式开始执行函数主体部分。
*main函数执行完毕之后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。
总结:
1.根据可执行文件头表中的entry-point-address找入口点
2默认情况下是连接开发工具带有的obj文件的固定函数(开始执行的是mainCRTStartup(或 wmainCRTStartup…以及其他)函数之一)作为入口点,这个函数为我们进行初始化(因为我们写的程序必须要使用到各种各样的运行时库函数才能正常工作,所有在执行我们自己写程序之前必须要先准备好所需要的一切库,之所以要连接它们是因为他们肩负着很重要的使命,就是初始化好运行时库),然后调用main函数。
如果入口点不设置为不连接这个obj,程序中就可以没有 main函数,设置为自己的函数。
3在main函数执行完成以后,再返回到初始化部分,进行一些清理工作,然后结束进程。
如何将其他函数设置为函数入口
第一步:工程属性-----链接器-----系统-----子系统下选择控制台(/SUBSYSTEM:CONSOLE).
第二步:编辑代码
#include
#pragma comment(linker,"/entry:FunTest")//预处理指令 ; FunTest是作为入口函数的函数名
using namespace std;
void FunTest ()//定义作为入口的函数
{
cout<<"hello"<
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LY77kkWE-1649731834094)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220306215202321.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bkrqzptX-1649731834095)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220306201215374.png)]
1 服务器保持了大量TIME_WAIT状态
1.1 原因
这种情况比较常见,一些爬虫服务器或者WEB服务器(如果网管在安装的时候没有做内核参数优化的话)上经常会遇到这个问题,这个问题是怎么产生的呢?
值得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,假如Server一秒钟接收1000个请求,那么就会积压240*1000=240000个TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIME_WAIT,所以对于新的 TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。
HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个 request/response,一个主要原因就是发现了这个问题。
现在来说如何来解决这个问题。
解决思路很简单,就是让服务器能够快速回收和重用那些TIME_WAIT的资源。
解决办法:1. 客户端 HTTP请求头部Connection
设置为keep-alive, 保证存活一段时间;2. 服务端允许 time_wait
状态的socket被重用,缩减 time_wait
时间,设置为1MSL,另外一般设置服务器不允许主动关闭连接
下面来看一下对/etc/sysctl.conf文件的修改:
#表示当keepalive启用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
#减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
123456789101112
修改完之后执行/sbin/sysctl -p
让参数生效。
2 服务器保持了大量CLOSE_WAIT状态
TIME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。
但是CLOSE_WAIT就不一样了,从上面的图可以看出来,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ACK信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
举个场景,来说明CLOSE_WAIT和TIME_WAIT的区别:
服务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常情况下,如果请求成功,那么在抓取完资源后,服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。如果一旦发生异常呢?假设请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了让HttpClient释放连接,那就会造成CLOSE_WAIT的状态了。
所以如果将大量CLOSE_WAIT的解决办法总结为一句话那就是:查代码。因为问题出在服务器程序里头啊。
消息队列主要由以下作用:解耦,削峰,异步
消息队列作用 - 简书 (jianshu.com)
此时,快递员只需要把快递放到柜子里,不需要关心小明是否在家,是否在睡觉。小明也不需要一直等待给快递员开门,两个人解耦了。
快递员把快递放到柜子里发个信息就可以去送下一件,不需同步等待结果。
这样每个快递的处理速度(响应时间)都变得极短,每天送的快递数量(吞吐量)也变多了。
这次又到了双十一,小明还是一天要到100个快递,由于小明一天只能消化10个快递,剩下的就放在了柜子里,等10天后才拿完。
快递员由于是异步送快递,双11根本不是事,这点吞吐量完全搞得定。
小明以前需要一件一件收取快递,现在放在了柜子(队列)里,那等攒够了10件去取一次(buffer->reduce),好省时间!其他时间都可以快快乐乐约会了
解耦,生产端和消费端不需要相互依赖
异步,生产端不需要等待消费端响应,直接返回,提高了响应时间和吞吐量
削峰,打平高峰期的流量,消费端可以以自己的速度处理,同时也无需在高峰期增加太多资源,提高资源利用率
提高消费端性能。消费端可以利用buffer等机制,做批量处理,提高效率。
C++中static_cast和dynamic_cast强制类型转换 - 泡面小王子 - 博客园 (cnblogs.com)
RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
只能在有虚函数的类层次之间使用dynamic_cast。要实现dynamic_cast,编译器会在每个含有虚函数的类的虚函数表的前四个字节存放一个指向_RTTICompleteObjectLocator结构的指针,当然还要额外空间存放_RTTICompleteObjectLocator及其相关结构的数据。以上面代码的类C来说:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUCtDUQ2-1649731834095)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220310112909276.png)]
这个_RTTICompleteObjectLocator就是实现dynamic_cast的关键结构。里面存放了vfptr相对this指针的偏移,构造函数偏移(针对虚拟继承),type_info指针,以及类层次结构中其它类的相关信息。如果是多重继承,这些信息更加复杂。
所以,dynamic_cast的时间和空间代价是相对较高的,在设计时应避免使用。
如果整个工程都不需要dynamic_cast,可以禁用运行时类型信息(vs2008默认是启用的),这样编译器就不会产生_RTTICompleteObjectLocator及相关数据。
类型转换不安全性来源于两个方面:
其一是类型的窄化转化,会导致数据位数的丢失;比如int类型转short。float类型转int。
其二是在类继承链中,将父类对象的地址(指针)强制转化成子类的地址(指针)。
因此上行转换一般是安全的,下行转换很可能是不安全的。子类中包含父类,所以上行转换(只能调用父类的方法,引用父类的成员变量)一般是安全的。但父类中却没有子类的任何信息,而下行转换会调用到子类的方法、引用子类的成员变量,这些父类都没有,所以很容易“指鹿为马”或者干脆指向不存在的内存空间。
(1) 函数参数,默认调用惯例情况下从右向左的顺序依次把参数压入栈中。由函数调用方执行。
(2) 函数的返回地址,即调用方调用此函数(如call func1)的下一条指令的地址。函数调用方(call指令)执行。
(3) 保存调用方函数的EBP寄存器,即将调用方函数的EBP压入堆栈,并令EBP指向此栈中的地址:pushl %ebp; movl %esp, %ebp。由被调函数执行。
(4) 上下文:保存在函数调用过程中需要保持不变的寄存器(函数调用方的),如ebx,esi,edi等。由被调函数执行。
(5) 临时变量,如非静态局部变量。
esp: 栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
ebp: 基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6TurAOOF-1649731834095)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220310174133194.png)]
// 最大堆,存储左边一半的数据,堆顶为最大值
priority_queue<int, vector<int>, less<int>> maxHeap;
// 最小堆, 存储右边一半的数据,堆顶为最小值
priority_queue<int, vector<int>, greater<int>> minHeap;
.so结尾的文件是动态链接库,动态链接库不能主动运行,只能被动调用。
.so的使用方法:
(32条消息) 动态库的使用方法_CodeCAT-CSDN博客
首先先生成动态库.so文件
gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so
-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的(好像不是必须的哦)
-shared : 该选项指定生成动态连接库,这个是必须的
需要这个库放置到程序的搜索路径下
添加环境变量,因为程序在执行搜索库的话会去默认的文件夹下搜
一、把库文件拷贝到系统查找库时的路径中,生成的共享库拷贝到/usr/lib/或/lib目录下
cp libtest.so /usr/lib/
二、告诉系统把当前路径添加到系统环境变量中,让系统查找库时在这个路径下查找。
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ///注意有个. 代表当前路径,
//如果要添加其他路径只需要将.换成目标路径,
//这种方法只有临时有效,重启后丢失配置,
//如果需要一直保存此路径可以修改配置文件,
//具体方法在此不多做讲解,自行百度
需要这个库提供的头文件在c/c++程序中链接调用
在main函数中一定要加头文件,
c/c++程序执行时会到搜索路径下动态加载.so库
gcc test.c -L. -ltest -o test
-L. : 表示需要链接的共享库在当前目录;
-Itest : 表示调用的共享库的文件名为 lib+test+.so 即libtest.so
同时要将相应链接的库-ltest显示写出。如果不写-L只写-ltest等时,会出现"无法找到-ltest"等类似错误;如果不将链接库的目录显示加入搜索目录中时,会出现"未定义的声明"等类似错误。
静态库
在Linux上创建静态库的步骤如下:
写源文件,通过 gcc -c xxx.c
生成目标文件。
使用gcc,为这两个源文件生成目标文件:
gcc -c my_print.c my_math.c
我们就得到了 my_print.o 和 my_math.o。
用 ar
归档目标文件,生成静态库。
ar crv libmylib.a my_print.o my_math.o
我们就得到了libmylib.a,这就是我们需要的静态库。
上述命令中 crv 是 ar的命令选项:
- c 如果需要生成新的库文件,不要警告
- r 代替库中现有的文件或者插入新的文件
- v 输出详细信息
配合静态库,写一个使用静态库中函数的头文件。
生成对应的头文件
头文件定义了 libmylib.a 的接口,也就是告诉用户怎么使用 libmylib.a。
新建my_lib.h, 写入内容如下:
#ifndef __MY_LIB_H__
#define __MY_LIB_H__
int add(int a, int b);
int subtract(int a, int b);
void cout(const char *);
#endif
测试我们的静态库
在同样的目录下,建立 test.c:
#include "my_lib.h"
int main(int argc, char *argv[])
{
int c = add(15, -21);
cout("I am a func from mylib ...");
return 0;
}
这个源文件中引用了 libmylib.a 中的 cout
和 add
函数。
使用静态库时,在源码中包含对应的头文件,链接时记得链接自己的库。
这个源文件中引用了 libmylib.a 中的 cout
和 add
函数。
编译test.c:
gcc test.c -L. -lmylib
将会生成a.out,通过 ./a.out 可以运行该程序。说明我们的静态库能正常工作。
上面的命令中 -L.
告诉 gcc 搜索链接库时包含当前路径, -lmylib
告诉 gcc 生成可执行程序时要链接 libmylib.a
认真分析mmap:是什么 为什么 怎么用 - 胡潇 - 博客园 (cnblogs.com)
常规文件系统操作(调用read/fread等类函数)中,函数的调用过程:
1、进程发起读文件请求。
2、内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。
3、inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。
4、如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。
总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
**总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。**说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。
锁优化有3个方向:
1.减少持有锁的时间:例如,将CPU密集和I/O密集的任务移到锁外,可以有效减少持有锁的时间,从而降低其他线程的阻塞时间。
2.减小加锁的粒度:常用的方法就是分片 (Shard),将一把锁分成几把锁,每个锁控制一个分片。
将单个独占锁变为多个锁,从而将加锁请求均分到多个锁上,有效降低对锁的竞争。但是,增加锁的前提是多线程访问的变量间相互独立,如果多线程需要同时访问多个变量,则很难进行锁分解,因为要维持原子性。
3.放弃使用独占锁,使用非阻塞算法来保证并发安全。例如,使用并发容器、读写锁、不可变对象、原子变量、线程封闭等技术。
保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-haaGZjUC-1649731834096)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220312141408132.png)]
1、互斥同步
互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。
2、非阻塞同步
随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。
CAS缺点:
ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3、无需同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1)可重入代码
可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。
(类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)
2)线程本地存储
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。
————————————————
版权声明:本文为CSDN博主「LemmonTreelss」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_26545305/article/details/79516610
C++中头文件相互包含与前置声明 - 木有Some - 博客园 (cnblogs.com)
1.加前置声明
2.将类实例化的对象都用指针。
为什么会这样呢?因为C++编译器自上而下编译源文件的时候,对每一个数据的定义,总是需要知道定义的数据的类型的大小。在预先声明语句class B;之后,编译器已经知道B是一个类,但是其中的数据却是未知的,因此B类型的大小也不知道。这样就造成了编译失败,VC++6.0下会得到如下编译错误:
error C2079: ‘b’ uses undefined class ‘B’
将A中的b更改为B指针类型之后,由于在特定的平台上,指针所占的空间是一定的(在Win32平台上是4字节),这样可以通过编译。
//文件A.h中的代码
#pragma once
#include "B.h"
class B;
class A
{
public:
B* b;
};
//文件B.h中的代码
#pragma once
#include "A.h"
class B;
class B
{
public:
A* a;
};
由于早期MySQL版本中binlog只有statement模式,而在读提交(RC)隔离级别下记录的binlog使用statement模式会导致主从数据不一致的问题,所以,MySQL选择使用可重复读(RR)作为默认隔离级别以保证主从复制数据一致性。
InnoDB 的默认隔离级别是什么? - 掘金 (juejin.cn)
为了说明数据湖与数据仓库的不同,James Dixon说:“如果数据集市是一个商店的瓶装水,经过过滤包装和结构化以供使用——数据湖则是在更自然状态下的大量的水。数据湖中的数据来源于不同地方,用户可以进入数据湖中提取所需要的数据”。
作者:RonnieZhang1989
链接:https://www.jianshu.com/p/e2beae3fddc9
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4KrWqWu-1649731834096)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220321160815228.png)]
大多数人都熟悉结构化数据的工作原理。结构化数据,可以从名称中看出,是高度组织和整齐格式化的数据。它是可以放入表格和电子表格中的数据类型。它可能不是人们最容易找到的数据类型,但与非结构化数据相比,无疑是两者中人们更容易使用的数据类型。另一方面,计算机可以轻松地搜索它。
结构化数据也被成为定量数据,是能够用数据或统一的结构加以表示的信息,如数字、符号。在项目中,保存和管理这些的数据一般为关系数据库,当使用结构化查询语言或SQL时,计算机程序很容易搜索这些术语。结构化数据具有的明确的关系使得这些数据运用起来十分方便,不过在商业上的可挖掘价值方面就比较差。
典型的结构化数据包括:信用卡号码、日期、财务金额、电话号码、地址、产品名称等。
非结构化数据本质上是结构化数据之外的一切数据。它不符合任何预定义的模型,因此它存储在非关系数据库中,并使用NoSQL进行查询。它可能是文本的或非文本的,也可能是人为的或机器生成的。简单的说,非结构化数据就是字段可变的的数据。
非结构化数据不是那么容易组织或格式化的。收集,处理和分析非结构化数据也是一项重大挑战。这产生了一些问题,因为非结构化数据构成了网络上绝大多数可用数据,并且它每年都在增长。随着更多信息在网络上可用,并且大部分信息都是非结构化的,找到使用它的方法已成为许多企业的重要战略。更传统的数据分析工具和方法还不足以完成工作。
结构化数据与非结构化数据有什么区别? - 知乎 (zhihu.com)
1.在浏览器输入URL后,
2.浏览器查看是否有相同URL的缓存,如果缓存未过期,则直接返回缓存/并跳转网页
3.如果没有命中缓存,则解析URL,解析出域名以及所请求的文件
4.生成HTTP请求消息。
5.知道了域名,下一步通过DNS域名解析协议获取IP地址
6.然后把HTTP的传输工作通过调用SOCKET库,交给操作系统的协议栈。
7.协议栈的上半部分为TCP/UDP协议。在进行TCP数据传输时,首先要三次握手建立连接。保证双方都有发送和接收能力。
8.HTTP首部和数据共同组成TCP数据部分,如果TCP数据部分过长需要对其进行分割,按照MSS长度进行分割。分割后每个部分都增加TCP头部。生成TCP报文,交给协议栈下半部分IP协议
9.IP协议会为TCP报文增加源IP地址和目的IP地址。
10.但是多网卡多个IP如何选择源IP地址?查询当前系统的路由表,决定哪个网卡的IP地址。通过将目的IP地址与子网掩码进行与运算,然后和路由表上每个网卡的目的地址进行对比,如果相同就发送给相应网卡的IP地址。如果都不同则用默认网卡IP地址。
11.生成了IP数据包后,需要增加MAC地址。发送发的MAC地址和接收方的MAC地址
发送方的MAC地址是在网卡生产时就写入ROM里。
12.接收方的MAC地址首先查操作系统的ARP缓存,如果没有查到需要通过ARP协议在以太网中以广播的形式进行传播,进行获得。生成MAC报文生成。
13.网络包为存储在内存的二进制数字信息,网卡将数字信息转换为电信号。增加报头和起始分界符和FCS冗余校验码。通过网线发送出去。
14.交换机。交换机根据MAC地址表查找MAC地址表,然后将信号发送给相应的端口。如果没有相应的MAC和端口的对应,则将所有端口都发送该数据包,接收者会返回相应包,交换机将其加入MAC地址表。
15.路由器,MAC头部的作用就是将包送达路由器,因此将包送达路由器后,MAC头部就会被丢弃。
如果网关是IP地址,则这个IP地址为需要继续转发的目标地址
如果网关为空,说明已经找到目的IP地址。
此时知道了IP地址,需要查询缓存和ARP协议继续查找MAC地址。
路由器为基于IP设计的,路由器每个端口都具有MAC地址和IP地址
交换机为基于以太网设计的,不具有MAC地址。