- 博客(61)
- 收藏
- 关注
原创 实现一个可中断线程的线程类-续
中实现的线程中断退出方案,为了让线程和线程对象共享中断标识变量stop_token的所有权,把它在堆上分配,并使用std::shared_ptr来管理,其实根据它的应用场景,也可以不用在堆上分配。stop_token在目标线程退出后也随着销毁了,既然线程退出了也就不需要再向它发送中断信号了,也就用不着stop_token了。
2025-09-08 19:33:11
716
原创 实现一个可中断线程的线程类
在虽然能够满足线程中断功能要求,但是在方案设计上仍有一些问题:首先,它是是专用的,无法做到通用性,它是为一个特殊的应用场景实现的,如果换成另一个场景,就得重新实现。其次,中断标记变量stop_token封装性不好,每次启动一个线程,都需要手动定义一个stop_token变量。再次,stop_token的生存期和线程及线程对象是分离的,在启动线程时以引用类型作为参数传递,存在悬挂引用的风险。既然C++是面向对象的语言,可以考虑使用类来封装,让stop_token成为线程对象的数据成员。
2025-09-08 19:26:06
1036
原创 谈谈线程的中断退出
线程启动后可以一直运行,直到线程函数运行完毕,即(Run To Complete:一站到底?)模式。线程在运行过程中也可以被中断,提前结束线程运行,即。相比RTC模式,该模式就不容易实现了,因为需要多线程之间的通信,涉及到了线程安全问题。如何让线程能够安全地中断退出呢?
2025-08-30 20:04:59
673
原创 C++析构函数和线程退出2
上一篇文章是线程库从C++11的std::thread升级到C++20的std::jthread,因为jthread的新特性而导致的对象析构顺序问题,部分内容涉及到了std::jthread提供的协作式中断/取消线程的知识。本文也和C++编译环境的升级有关,从更早的版本C++03升级到C++11时,因线程取消操作而导致的C++对象析构异常问题。
2025-08-18 10:06:13
402
原创 C++析构函数和线程退出1
线程作为程序在操作系统中的执行单元,它是活动对象,有生命周期状态,它是有始有终的。有启动就有结束,在中讨论了线程作为数据成员启动时的顺序问题,如何避免构造函数在初始化对象时对线程启动的负面影响,是关于线程的“始”,而“作为“始”的对应方“终”,也和构造函数的对应方析构函数也密切相关,同样如果使用不当,可能也会遇到问题。现在简单地讨论一下线程退出时和C++析构函数的关系,析构函数的语义对线程退出时的影响。开始之前,先看一下析构函数的特点。
2025-08-17 19:03:11
823
原创 C++自旋锁的后退机制简介
介绍了5种后退机制,前两种是在用户层进行的,线程不会阻塞,第一种是纯指令的软件实现的本地自旋,防止内存总线出现流量风暴,“后退”让出的是内存总线资源,而第二种需要CPU的特殊状态,以节省CPU耗电量,“后退”让出的是CPU逻辑核的执行资源。剩下的三种都需要操作系统介入了,因为涉及到线程的调度和阻塞,“后退”让出的都是CPU资源。
2025-08-16 20:30:05
776
原创 构造C++对象时如何控制执行顺序3
第三篇讨论解决线程启动时的顺序要求的方案。在C++标准库中,std::thread线程对象没有提供专门的启动函数(不同于Java线程对象,它提供了启动线程的start()方法),在创建线程对象时,会同时启动线程。因此如果线程对象作为数据成员存在于一个类中,它的线程函数访问了数据成员,或者线程函数是所在类的成员函数,那么线程对象的初始化顺序,就得要注意了,如果使用不当,就容易产生不易察觉的错误。
2025-08-16 07:19:19
786
原创 构造C++对象时如何控制执行顺序1
在C++中,一个派生类的各个基类和它自己的各个数据成员之间的构造顺序有严格要求,这个顺序无法手动地调整,只能无条件地遵从C++的语义规则要求。但是数据成员之间的初始化顺序,多个基类之间的继承顺序,以及构造函数和委托构造函数之间的调用顺序,是可以手动控制的,因此如果发生了和初始化顺序有关的bug,解决思路也基本上从这些方面入手。
2025-08-09 10:13:08
731
原创 C++11原子操作实现公平自旋锁
这种情况下,可能出现先自旋的线程未能获得锁,而后来的线程反而成功的情况。公平锁采用了类似原理:每个申请锁的线程会被分配一个有序编号,只有当前面的编号持有者完成操作后,后续编号的线程才能获得锁资源。本该算法虽然确保了锁的公平性,但存在明显的性能隐患。当某个排队等待锁的线程被调度出去时,即便前一个线程已释放锁,由于该线程无法及时获取锁(仍处于调度状态),后续所有持有更大ticket编号的线程都会被阻塞。释放锁时,sync会执行加1操作,从而确保ticket值较小的线程总能优先获得锁,实现了公平的锁获取顺序。
2025-07-31 22:19:43
870
原创 智能指针之设计模式6
智能指针所用的设计模式思想1、工厂方法模式创建sp除了常规的使用构造函数之外,还提供了两个工厂方法。sp的make_shared() 来生成一个shared_ptr对象,是简单工厂模式。把使用视频和创建sp的职责分离了,工厂方法能够控制对象的生成过程,在这里,make把资源对象和控制对象在一个内存块中了,这样可以更好的提高cache line的预取了。sp把托管资源对象和控制块(头部信息)分配在一起了,故不能在mak_shared中指定deleter对象。enable_shared_from_th
2025-04-28 19:34:11
820
1
原创 智能指针之设计模式5
这里的问题是内存资源是使用二维指针来初始化和分配的,但是智能指针只能管理一维指针,getline()的参数需要二维指针,而unique_ptr::get()返回的是一维指针,无法对它进行取地址操作&。从这些例子中,我们可以发现,智能指针管理的裸指针都是一维指针类型,但是有一些场合需要使用二维指针,比如在C语言中,因为没有引用类型,一个函数在对指针做出修改时,往往需要使用二维指针作为它的参数类型,在函数内部来初始化、修改或者销毁指针参数指向的内存资源,此时,就无法直接使用智能指针进行管理了。
2025-04-27 09:37:57
759
原创 智能指针之设计模式4
前面的介绍了使用工厂模式来封装智能指针对象的创建过程,下面介绍一下工厂类enable_shared_from_this的实现方案。
2025-04-22 20:51:53
942
原创 汇编语言中的数据
虽然在前面我把数据分成了三部分进行说明,实际上它们三者是同时密切联系在一起的。1、源数据位于内存中,是一个“基址+变址*因子+偏移量”的寻址方式,位置是经过基址寄存器rdi和变址寄存器rsi运算后的内存地址,数据长度是4字节(dword ptr指示),而地址数据存放在寄存器rdi中,它的长度是8字节,相当于rdi对应C/C++中的指针类型;2、目的数据位于寄存器edx中,它的长度是4字节3、指令操作符是add,是算数加法操作,因此源数据和目的数据都是整型数,但是有符号还是无符号数仍不知道。int x。
2025-04-18 21:55:14
1208
原创 优化自旋锁的实现
1、TAS算法实现自旋锁,会导致内存总线流量风暴,全局系统影响大。2、TTAS虽然抑制了流量风暴的产生,减轻了全局内存总线的竞争程度,但是又导致CPU耗电量大、发热等情况。3、使用pause指令缓解了超线程核心的系统资源竞争和降低了耗电量,但是又增长了获取锁时的延迟。4、TTAS算法和pause指令是在程序和CPU上面对自旋锁的优化,如果获取不到锁时,线程仍然处于自旋中,不会发生调度和阻塞现象,没有改变自旋锁的本质特征。
2025-04-18 17:34:14
889
原创 智能指针之设计模式2
前面介绍了控制了智能指针和资源对象的创建过程,现在介绍一下智能指针是如何利用代理模式来实现“类指针(like-pointer)”的功能,并控制资源对象的销毁过程的。
2025-04-13 22:32:45
1163
原创 智能指针之设计模式1
本文探讨一下智能指针和GOF设计模式的关系,如果按照设计模式的背后思想来分析,可以发现围绕智能指针的设计和实现有设计模式的一些思想体现。当然,它们也不是严格意义上面向对象的设计模式,毕竟它们没有那么分明的类层次体系,和GOF经典设计模式在外在形式上有所差别,重点是理解设计模式的思想在它们身上的体现,以及怎样帮助它们实现意图的。限于篇幅,分成了几篇文章来介绍,先从对象的创建开始。
2025-04-13 18:57:14
913
原创 C++11实现一个自旋锁
自旋锁也是一种互斥锁,和mutex锁相比,它的特点是不会阻塞:如果申请不到锁,就会不断地循环检测锁变量的状态,直到申请到锁。它的核心算法是一个循环检查锁变量的操作,是CPU自我循环的过程,因此称为自旋锁。底层实现有两种算法,一种是把目标值和锁状态变量的原值进行交换,这个过程要求是一个原子操作,然后检查交换出来的值是否是期望的值,称为TAS算法;另一种是先检查锁状态变量是否是期望值,如果是就设置为目标值,并返回成功,这个过程也要求是一个原子操作,称为CAS算法。
2025-04-07 20:55:26
1020
原创 为什么函数对象作为函数参数时,一般使用值类型形式?-番外篇
而nmax的第1个参数是回调函数comp的函数指针,第2个参数a是comp的第1个参数,第3个参数b是comp的第2个参数,因此,在nmax中调用comp时,需要把nmax的第2个参数rsi和第3个参数rdx分别赋值到rdi和rsi寄存器中,以符合调用约定,同时第1个参数rdi存放的是comp的函数指针,还得要把这个rdi寄存器腾让出来,以存放comp的第1个参数,因此会有更多的指令来调整这些参数的位置存放。因为要使用rdi寄存器来传递this指针,需要对rdi的原值进行缓存,不得不花费额外的指令。
2025-04-03 16:04:22
930
原创 为什么函数对象作为函数参数时,一般使用值类型形式?
总之,空函数对象作为参数时,在按值和按引用传参时的开销几乎是一样的,无非是按值传参时先不传递函数对象,延迟到在算法函数调用回调函数对象时再临时创建和传递函数对象,而按引用传参时在调用算法函数时就创建了函数对象,无非就是先做还是后做的区别,二者性能几乎完全一样。算法是函数模板,编译器在编译过程中可以看到算法实现的源码,如果在调用时对它进行了内联,同时编译器也知道所设置的回调函数的源码,那么就意味着整个调用链路编译器都是可见的,也就说无论是传递值还是引用,在内联优化展开代码时,没有区别,因此生成了一样的代码。
2025-04-03 13:09:37
760
原创 巧用临时对象之五
这样,如果后续再有读者进行读操作时,通过这个已经更新的全局指针来访问vector就可以了,同样,如果后面再有新的写操作,继续创建一个新的vector对象并进行更新,然后让全局指针指向它。不过,在实践中最好不要使用auto定义变量来接收那个代理对象,如果类型转换时,直接让一个具体类型的变量来接收临时代理对象,如果是调用其它操作符时如“->”,则直接调用,这样代理对象就会自动进行类型转换或者调用其它操作符,因为代理对象是匿名的临时对象,用户也就意识不到是在和一个代理对象打交道,实现的代码看起来也非常干净利索。
2025-04-02 17:13:54
765
原创 编译器眼中的-标量对象和空对象的传参及优化
C++编译器在编译程序时,只要不违背C++语义,可以对程序进行一些变换或者优化。如果C++对象是一些特殊类型的对象,当它们是一个函数的参数(当然也包括成员函数,因为成员函数隐藏着一个this指针,在调用时把对象自己作为参数传递)时,编译器在传递参数时可以进行一些特殊的优化,以提高传递函数参数和访问数据成员的性能。
2025-03-27 18:59:14
1098
原创 编译器眼中的虚函数动态绑定
根据前面的图示,vptr存放在一个对象this指针指向的对象实例中,而对象的this指针的值,只能在运行时才能知道,因此要得到虚函数,那也就得在程序运行的时候了,这个阶段也只能在运行时,无法在编译时静态绑定,所以也就是所谓的动态绑定。综上,动态绑定并不神秘,形象一点说,就是编译时构造了一个绑定虚函数的“公式”,该“公式”在编译的时候确定它的常量系数,在运行的时候得到它的和实际类型相关的动态参数,进行计算后,就能得到实际类型的虚函数指针,是编译器编译时的静态分析和程序运行时的动态查找,它们共同合作的结果。
2025-03-27 16:21:48
665
原创 cmpxchg16b指令的实现分析
我们知道C++11以后提供了原子操作类型:atomic<T>,该原子模板类可以实现原子操作,比如exchange、compare_exchange_weak、fecth_add等,T类型一般都是非常简单的类型,比如整型、指针等类型,这些类型都可以实现指令级的原子操作,所操作的数据类型的长度一般也不能大于内存数据总线的宽度,比如在32/64bit的处理器中,T类型的数据长度不能超过32/64bit。它会比较指针ptr指向的值与给定的旧值oldval,如果相同,则将新值newval写入,并返回真(true);
2025-01-24 19:00:02
1267
原创 巧用临时对象(四)
因为它的数据成员都是引用类型,销毁对象时也无需管理资源,过程非常简单,实际上是对临时对象进行了“去皮留瓤”的操作,即在外在形式上,虽然临时对象(皮)销毁后,但在变量x、y、z中却留下了临时对象的各个数据成员的值(瓤),也就是实现了对my_data对象的结构化绑定。那么,我们转换一下思路,把需要绑定的各个独立变量,作为某个类的数据成员,然后重载这个类的赋值操作符operator=,参数是my_data类型,这样,在赋值操作符函数中就可以把my_data对象各个数据成员赋值给每个独立变量了。
2024-12-19 10:55:31
954
原创 巧用临时对象(三)
每当调用sync_shared_ptr的operator->()操作符时,都会创建并返回一个内部类raii的临时对象,因为这个临时对象也重载了操作符->,所以继续调用它的operator->()操作符,最后返回了sync_shared_ptr所保存的裸指针,该指针指向一个vector对象,使用->操作符可以访问它所指向的vector对象的成员函数。对象obj调用locked()函数返回的值被赋值给locked_ptr对象,此时就不再是临时对象了,而是一个具名对象,它的生存期是由作用域决定的。
2024-12-16 16:48:19
652
原创 巧用临时对象之二
对于id来说,因为是基本类型的数据,这点开销并不大,尚可接受,而对于name成员,它是string类型,因为不同库的实现不同,在构造时有的库实现可能需要更多的耗时操作。按照临时对象的生存期定义,它的生存期仅存在于lock_guard<mutex>(source.mtx)表达式所在的这一行,即在调用委托构造函数传递实参时,创建lock_guard的临时对象,并同时进行加锁,当调用返回后表达式生存期结束,这时销毁lock_guard临时对象,并同时进行解锁。
2024-12-14 06:00:21
882
原创 巧用临时对象之一
就以类universal_ptr来说,它只有一个数据成员,而且还是指针类型,指针占用内存非常小,在64位环境下仅占用8字节,而是这个类的析构、拷贝构造和移动构造函数都是缺省的,只是简单的指针赋值操作而已,因此,尽管在调用的中间阶段产生了临时对象,但开销非常小。此外,临时对象使用指针形式的数据成员,也无需担心悬挂指针的问题,因为所创建的临时对象,它即时传参即时创建即时销毁,都是发生在同一个表达式中,它的生存期不会晚于创建时所传递的参数的生存期。时,返回值类型被推导为unique_ptr<string>类型?
2024-12-13 20:20:43
827
原创 探索连续调用多个虚函数的优化
在函数B::foo()中,使用placement new操作符在B类型的对象所占用的内存空间中,创建了它的一个派生类D的对象(二者的sizeof相等,完全可以这样做),因为在调用D的构造函数时,要初始化vptr指针,此时vptr被修改为指向D类的虚函数表,尽管此时B所创建的对象的this指针没有变化,但是因为B和D的对象布局完全一样,this所包含的vptr指向的虚函数表已经被修改成了D的虚函数表。原因是为了安全,尽管虚函数表指针是不可见的,但在程序中仍然可以通过非常规的手段对它进行修改。
2024-11-20 21:49:22
639
原创 栈和局部变量
我们知道程序在运行时,它的内存可以分为数据区、代码区、堆区、栈区等,而栈区是一个线程独占内存区。一个函数分配的局部变量就是在所在线程的栈区分配的,当调用一个函数时,会为它建立一个栈桢,所有函数用到的局部变量都是在这个栈帧中分配的,局部变量所占用的内存空间分配和释放都是自动的,调用函数时随着栈桢的创建,局部变量的空间也就分配好了,当函数退出时释放栈桢,局部变得空间也就释放了,所有的这一切都是自动进行的,无需程序员参与。先看一下栈的运行机制,以x86 32 位 CPU 为例。在刚开始工作在实模式时,CPU中
2024-11-01 10:59:37
770
原创 派生类重载的delete操作符调用时可以动态绑定吗
该函数先调用了dog类的析构函数,然后再调用dog类的重载的operator delete(),正好符合delete操作符的语义,也就是说在这里,编译器使用了一个独立的函数来封装了这个delete操作符的功能。因此,派生类中重载的delete操作符在使用基类指针析构堆上对象时,也是动态绑定来调用的,只不过它并不是使用传统的方式,定义成虚函数来动态绑定的,而是被封装在一个编译器自动生成的虚析构函数中,通过动态绑定虚析构函数来间接的动态绑定。
2024-10-31 15:35:49
913
原创 C++中的CRTP
我们知道,要定义派生类肯定需要知道基类的类型,而在使用类模板来实例化一个模板类时又得需要知道模板的实参类型,在这里实例化基类时的实参类型又是派生类的类型。CRTP基类是类模板,基类实例化之后,和派生类是一一对应的,即一个基类只能派生一个子类,而模板参数化this指针的实现方式是基类只有一个,而且是非模板类,它的派生类可以有多个,但是每定义一个派生类,在该基类中都会使用派生类类型实例化出一个对应的interface()函数,也就是在基类中会有多个interface()成员函数和它的派生类一一对应。
2024-10-18 22:48:33
1951
原创 一种条件语句的编译优化方式
我们知道,指令是以指令流的形式在CPU流水线中解码并执行的,如果指令流的流程没有发生跳转的话,它就会按照指令流中的指令顺序依次执行,并且在前面指令的执行过程中,CPU的解码单元还会预取后面的指令并预先进行译码操作,前面的指令执行完毕紧接着执行已经译码完成的后面的指令,一环扣一环的向前推进。=的比较时,编译器会认为不相等是一个大概率的事件,显然int型的取值范围是0-4G,因此如果a和b的值是均匀分布的话,它们相等的可能性是1/4G,在编译器看来是极低的概率。=b),编译器都生成更有利于a!
2024-10-05 20:38:41
806
1
原创 C++ string类能否被继承?
对于像string这样没有虚函数的类,如果要复用它的功能,首先应该考虑使用对象组合的机制,让string对象是派生类的一个成员,在派生类中通过转发的方式使用string的成员函数。原因是在堆上创建的MyString对象是通过它的基类string类型来delete的,然而string的析构函数并不是virtual函数,这样在delete pStr时,调用的是string的析构函数,并没有调用派生类MyString 的析构函数,从而导致了MyString的数据成员prefix所分配的内存没有被释放。
2024-10-05 16:52:07
1458
原创 当C++遇到空指针异常......
在Java语言中,有空指针异常,在编程时为了代码安全,在遇到空指针时,防止程序崩溃,会捕捉空指针异常,即NullPointerException异常类。比如下面就是一段捕获NullPointerException异常的Java代码片段:try { 。。。。} catch (NullPointerException e) { e.printStackTrace();}当try语句块中的代码访问到空指针后,会抛出NullPointerException,随后在catch语句捕获这个异常,并
2022-04-28 18:24:49
10671
1
原创 C++11实现一个读写自旋锁-3(顺序锁 )
是一种特殊的读写锁,它也是一种乐观锁,我们知道,在读写锁中读写操作之间是互斥的,然而在顺序锁中,读写之间没有锁,写操作的时候无视读者的存在,但是读者在读数据时,要进行校验,验证在读数据期间数据没有被修改过,如果修改了,就放弃已经获取的数据,重新获取数据,也就是说读者总是假定它所读取的数据是正确的,是一种读乐观锁。同lock-free相比,它的关注点是读,只要在读的那一时刻,没有写操作,就认为能够都成功,读完之后再判断在读的过程中是否有写操作发生,如果有,则回滚,重新读。而lock-free更常见的是写操作,
2022-04-28 09:10:11
1422
原创 C++中调用虚函数都是动态绑定吗
多态在面向对象编程中是一个重要的概念,一般是使用虚函数来实现的,原理就是通过虚函数表保存了一个类的虚成员函数的指针,在调用虚函数时,可以通过对象的虚函数表指针来从虚函数表中得到函数指针,因为函数地址是通过函数指针来访问的,所以在编译时是不知道函数入口地址的,只能在运行时通过访问虚函数表来定位。为了方便分析说明,先定义几个有继承关系的类:// 基类class base {public: base() {} base(int) { foo(); } virtual void f
2022-03-17 17:36:19
1435
原创 C++中值语义的函数参数和返回值的背后
我们知道,在C++中对象作为数据传递时,如函数的参数传递和返回值,有值语义和引用语义的形式之说。当使用基本数据类型(如int、short、bool、指针、引用)进行传递时,实现非常简单,因为它们的值可以直接放入寄存器中,把寄存器作为参数传递和返回值的载体,就可以了,从一些函数调用约定就知道这些情况。可是,如果要传递的数据不是基本类型,而是用户定义的类型,比如class、struct等复合数据类型,这些类型的数据是无法放入一个寄存器的,那么在C++中是如果实现的呢。比如,我们有一个类,如下:class.
2022-03-17 11:26:32
1153
空空如也
空空如也
TA创建的收藏夹 TA关注的收藏夹
TA关注的人
RSS订阅