来聊聊C++中头疼的线程、并发

2023-12-18 12:48
文章标签 c++ 线程 并发 聊聊 头疼

本文主要是介绍来聊聊C++中头疼的线程、并发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

点击上方“AI算法修炼营”,选择加星标或“置顶”

标题以下,全是干货

1. 线程

在一个应用程序(进程)中同时执行多个小的部分(线程),这就是多线程。多个线程虽然共享一样的数据,但是却执行不同的任务。

线程启动、创建、结束

主线程执行完了,就代表整个进程执行完毕了,此时,一般情况下如果其他子线程没有执行完毕,那么这些子线程也会被操作系统终止。所以为了让子线程执行完就需要让主线程保持住。但是也有意外,不一定需要保持住

detach():传统多线程,主线程要等待子线程执行完毕,然后自己再最后退出, detach分离,也就是主线程和子线程不汇合了,各自独立执行,主线程执行完了,不影响我子线程。为什么引入detach?如果创建了很多子线程,让主线程逐个等待子线程结束,这个编程方法不好。

一旦detach()之后,与主线程的thread对象就会与主线程失去关联,此时子线程就驻留在后台。这个子线程就有运行时库负责清理相关线程的资源(守护线程)。一旦detach()调用后,就不能再用join()了。

joinable()判断是否可以成功使用join()或者detach()。

示例:用类对象创建线程

class Ta{public:void operator()() //需要有这个函数,且不能带参数{//你需要执行的操作}
};
int main(){Ta ta;thread thread(ta);//这样线程就会执行类的operator()//ta.join()ta.detach()//如果 ta中有引用或指针主线程的数据,就会出现不可意料的值。因为一旦主线程执行完,相应的资源就被释放了。//但是对象本身ta还在吗?不在了。那为什么thread还能正常运行?因为创建thread时创建的副本在子线程中运行。不行你可以显示实现一个拷贝构造函数看看。}

线程传参

传递临时对象作线程参数

如果会对线程进行detach()。那么在创建线程时传参要特别注意:

  • 若传递int这种简单类型参数,建议都是值传递,不要用引用,防止节外生枝

  • 如果传递类对象,避免隐式转换。全部都在创建线程这一行就构建出临时对象来,然后在函数参数里,用引用来接,否则系统还会多构造一次对象。

所以建议不要轻易使用detach(),尽量使用join()。这样就不存在局部变量失效导致线程对内存的非法引用问题。

2. 并发的概念

两个或者多个任务(独立的活动)同时的进行:一个程序执行多个独立任务。

以往计算机,单核cpu:某一时刻只能执行一个任务,由任务系统调度,每秒钟进行多次所谓的任务切换。这是一种并发的假象,不是真正的并发,这种切换(上下文切换)是有时间开销的。比如操作系统要保存你操作状态,执行进度等信息,都需要时间,一会切换回来,恢复这些信息也需要时间。

多核cpu才是真正的并发(硬件并发)

使用并发的原因,主要是同时可以干多个事,提高效率

多线程并发

C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程:<atomic>/<thread>/<mutex>/<condition_variable>/<future>

#include <atomic>   // C++11 原子操作,限制并发程序对共享数据的使用,避免数据竞争#include <thread>   // 该头文件主要声明了std::thread类,另外std::this_thread命名空间也在该头文件中#include <mutex>    // C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题#include <condition_variable>  // C++11 并发编程 主要包含了与条件变量相关的类和函数

线程并不是越多越好,每个线程,都需要一个独立的堆栈空间(1M),线程之间的切换要保存很多中间状态;

创建的线程数量不建议超过200-300个,在实际项目中可以不断调整和优化。

多线程通讯共享内存,全局变量,指针,引用等都可以实现。

共享内存带来问题:数据一致性问题,可以用信号量技术来解决。

多进程并发比多进程好, 启动速度快,更轻量级 系统资源开销更好,速度快,共享内存这种通讯方式比任何其他方式都快。缺点有一定难度,要小心处理数据的一致性问题。

主线程等待所有子线程运行结束,最后主线程结束。这样更容易写出稳定的程序。

vector<thread>  threads;  //用容器管理多线程

线程函数内,调用 std::this_thrad::get_id()可以获取线程id。

临界区,互斥量等以往多线程代码不能跨平台, poxi  可以跨平台,但是开发麻烦。

并发实现的常用框架

3. std::mutex 互斥访问

<mutex>是C++标准程序库中的一个头文件,定义了C++11标准中一些互斥访问的类与方法

其中std::mutex表示普通互斥锁,可以与std::unique_lock配合使用,把std::mutex放到unique_lock中时,mutex会自动上锁,unique_lock析构时,同时把mutex解锁。因此std::mutex可以保护同时被多个线程访问的共享数据,并且它独占对象所有权,不支持对对象递归上锁。

可以这样理解:各个线程在对共享资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。(下图来自网络)

常用的成员函数有:

  1. 构造函数:std::mutex不支持copy和move操作,最初的mutex对象处于unlocked状态。

  2. lock函数:互斥锁被锁定。如果线程申请该互斥锁,但未能获得该互斥锁,则申请调用的线程将阻塞(block)在该互斥锁上;如果成功获得该互诉锁,则该线程一直拥有互斥锁直到调用unlock解锁;如果该互斥锁已经被当前调用线程锁住,则产生死锁(deadlock)。

  3. unlock函数:互斥锁解锁,释放调用线程对该互斥锁的所有权。

死锁问题

死锁问题,是至少由两个锁头也就是两个互斥量才能产生。线程A锁了金锁,需要去锁住银锁;然而线程B锁了银锁,需要去锁住金锁,这样A和B都在等待对方释放锁。就成了死锁。

死锁的一般解决方案:只要保证两个互斥量上锁的顺序一致就不会死锁。std::lock()函数模板 能力:一次锁住两个或者两个以上的互斥量。它不存这种因为在多个线层中,因为锁的顺序问题导致死锁的风险问题

std::lock() 如果互斥量中有一个没锁住,它就在那里等着。其情况就是多个锁要么都锁住,要么都没锁住。如果只锁了一个,另外一个没锁住,它立即把已经锁上的释放掉。

但是需要调用mutex.unlock()区分别释放锁。为了避免遗忘unlock,这里可以借用lock_guard和adopt_lock去解决这个问题。

 std::lock(my_mutex1,my_mutex2);std::lock_guard<std::mutex> mylock(my_mutex1,std::adopt_lock);std::lock_guard<std::mutex> mylock(my_mutex2,std::adopt_lock);

std::adopt_lock是一个结构体对象,起一个标记作用,作用就是表示这个互斥量已经lock了,在lock_guard()构造函数中不需要进行lock了,这样就不用去手动释放unlock了,借用lock_guard()的析构函数去unloc。

4. std::unique_lock 锁管理模板类

std::unique_lock为锁管理模板类,是对通用mutex的封装。

std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。

其常用的成员函数为:

  1. unique_lock构造函数:禁止拷贝构造,允许移动构造;

  2. lock函数:调用所管理的mutex对象的lock函数;

  3. unlock函数:调用所管理的mutex对象的unlock函数;

在使用条件变量std::condition_variable时需要使用std::unique_lock而不能使用std::lock_guard。

unique_lock是个类模板,工作中,一般推荐使用lock_guard;

unique_lock比lock_guard灵活的多,但是效率要比lock_guard差一点,内存占用多一点。

unique_lock第二个参数:

  • std::adopt_lock

表示互斥量已经被lock了(你必须要把互斥量lock,否则会报异常 std::adopt_lock标记的效果就是“假设调用方线程已经拥有了互斥的所有权(已经lock成功了),lock_guard和unique_lock都可以调用这个标记。

  • std::try_to_lock

表示我们会尝试用mutex的lock去锁定这个mutex,如果没有锁定成功,就立即返回,并不会阻塞在哪里。用try_to_lock的前提是你自己不能先去lock.

std::mutex mymutex;
std::unique_lock<std::mutex> example(my_mutex,std::try_to_lock);
if(example.owns_lock()){
//拿到锁干什么。。。
}else{
// 没拿到锁干什么
}
  • std::defer_lock

前提是不能自己先lock,否则会报异常 defer_lock意思就是并没有给mutex加锁,初始化了一个没有加锁mutex

std::unique_lock<mutex> example(my_mutex,std::defer_lock);//没有加锁
/*用法一
example.lock();//自己手动加锁
//一些共享代码要处理
example.unlock();
//处理一些非共享代码
example.lock();//不需要手动开锁,当然你要手动开锁也可以,只是画蛇添足
*///用法二
if(example.try_lock()==true){
//拿到锁,做事
}else{
//没拿到锁做事
}

unique_lock成员函数

lock() //可以手动加锁,手动加锁后,你可以不用手动开锁,离开作用域后自动放锁。

 unlock() try_lock() 尝试给互斥量加锁,不阻塞。拿到就true,没拿到就false release(),返回它所管理的mutex对象指针,并释放所有权,也就是说这个unique_lock和mutext不再有关系。如果原来的mutex处于加锁状态,你接管过来必须负责开锁!!

无锁编程

为什么有时候需要unlock?因为你lock锁住的代码越少,执行越快,整个程序运行效率越高。有人也把锁头锁住的代码多少称为锁的粒度。粒度一般用粗细来描述. 锁住的代码少,这个粒度叫细,执行效率高. 锁住的代码多,这个粒度就粗,执行效率低. 合适的粒度,粒度太小可能遗漏保护代码.选择合适的粒度,是高级程序员的能力和实力的体现

unique_lock所有权的传递

std::unique_lock<std::mutex> example(my_mutex);
//example可以把自己的mutex所有权转移给其他unique_lock对象,但是不能复制所有权!
std::unique_lock<std::mutex>  example(std::move(example));
return std::unique_lock<std::mutex> //通过函数返回值也是一种所有权转移

5. std::condition_variable 条件变量

<condition_variable>是C++标准程序库中的一个头文件,定义了C++11标准中的一些用于并发编程时表示条件变量的类与方法等。

条件变量的引入是为了作为并发程序设计中的一种控制结构。当多个线程访问同一共享资源时,不但需要用互斥锁实现独享访问以避免并发错误(竞争危害),在获得互斥锁进入临界区后还需要检验特定条件是否成立:

  1. 若不满足该条件,拥有互斥锁的线程应该释放该互斥锁,使用unique_lock函数把自身阻塞(block)并挂到条件变量的线程队列中

  2. 若满足该条件,拥有互斥锁的线程在临界区内访问共享资源,在退出临界区时通知(notify)在条件变量的线程队列中处于阻塞状态的线程,被通知的线程必须重新申请对该互斥锁加锁。

条件变量std::condition_variable用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable需要与std::unique_lock配合使用。

常用成员函数:

(1)构造函数:仅支持默认构造函数。

(2)wait():当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_*唤醒当前线程。当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_*唤醒当前线程),wait()函数自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种:

  1. 无条件被阻塞:调用该函数之前,当前线程应该已经对unique_lock<mutex> lck完成了加锁。所有使用同一个条件变量的线程必须在wait函数中使用同一个unique_lock<mutex>。该wait函数内部会自动调用lck.unlock()对互斥锁解锁,使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知(notified,通过别的线程调用 notify_*系列的函数)而被唤醒后,wait()函数恢复执行并自动调用lck.lock()对互斥锁加锁。

  2. 带条件的被阻塞:wait函数设置了谓词(Predicate),只有当pred条件为false时调用该wait函数才会阻塞当前线程,并且在收到其它线程的通知后只有当pred为true时才会被解除阻塞。因此,等效于while (!pred()) wait(lck).

(3)notify_all: 唤醒所有的wait线程,如果当前没有等待线程,则该函数什么也不做。

(4)notify_one:唤醒某个wait线程,如果当前没有等待线程,则该函数什么也不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。

简单的说就是,当std::condition_variable对象的某个wait函数被调用的时候,它使用std::unique_lock(通过std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的std::condition_variable对象上调用了notification函数来唤醒当前线程。

6. async、future、packaged_task、promise异步编程


std::async & std::future

创建后台任务并返回值 。希望线程返回一个结果 std::async是个函数模板,用来启动一个异步任务,它返回一个std::future对象,std::future是一个类模板.。

异步任务:自动创建一个线程并开始执行对应的线程入口函数,他返回一个std::future对象 这个future对象里面含有线程入口函数所返回的结果,我们可以通过调用future对象的成员函数get()来获取结果.

通过额外向std::async()传递一个参数,该参数类型是std::launch类型(枚举类型),来达到一些目的

  • std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行. 如果wait()或者get()没有调用则不会执行线程.
    eg: std::async(std::launch::deferred,my_thread)可以测试线程id,延迟调用,其实没有创建新线程,是在主线程中调用的线程入口函数.

  • std::launch::async在调用async函数时,就开始创建线程 async函数默认用的就是std::launch::async标记

#include <future>
#include <thread>
#include<iostream>
int my_thread(){return std::this_thread::get_id();
}
int main(){//auto res=std::async(my_thread);std::future<int> res=std::async(my_thread);//创建了线程并执行,也可以成员函数创建.//A a;//std::future<int> res=std::async(&A::memberfunc,&a,&func_para);//cout<<res.get()<<endl; //卡在这里等待mythrea()执行完毕,拿到结果// res.get()只能调用一次!!res.wait();//等待线程返回,但本身并不返回结果.
}

std::packaged_task

打包任务,把任务包装起来。它是一个类模板,它的模板参数是各种可调用对象,通过std::package_task把各种可调用对象包装起来,方便将来作为线程入口函数调用。

packaged_task包装起来的可调用对象还可以直接调用。

int mythread1(int i){}
int my_thread2(){}int main(){std::packaged_task<int(int)>> mypt(mythread1)  //把函数mythread1通过//packaged_task包装起来std::thread t1(std::ref(mypt),1)//线程直接开始执行,第二个参数作为线程入口函数的参数t1.join();std::future<int> res=mypt.get_future();//std::future对象里包含线程入口函数的返回结果cout<<res.get();}

std::promise 类模板

我们能够在某个线程中给他赋值,然后我们可以在其他线程中,把这个值取出来. 总结:通过promise保存一个值,在将来某个时刻我们通过把一个future绑定到promise上,来获取这个绑定值.

void mythread(std::promise<int>& tmpp,int calc){int i=1/calc;int res=i+calc;tmpp.set_value(res)return ;
}int main(){std::promise<int> myprom;std::thread t1(mythread,std::ref(&myprom),180);t1.join();//获取结果值std::future<int> fu=myprom.get_future();//promise和future绑定,用于获取线程返回值auto result=fu.get();
}

future其他成员函数\shared_future\atomic

std::future成员函数

std::future<int> res=std::async(my_thread);
std::future_status status=res.wait_for(std::chrono::seconds(1)); //等待1秒
if(status==std::future_status::timeout){//超时:表示等待了1秒线程没执行完//do something
}else if(status==std::future_status::ready) {//表示线程成功返回cout<<res.get()<<endl;
}else if(status==std::future_status::deferred){//如果async的第一个参数为std::launch::deferred,则本条件成立//线程被延迟执行res.get();
}
std::shared_future

std::future对象只能get一次,因为get函数的设计是一个移动语义.再次对对象进行get时,自然就没东西可以get了.

std::shared_future是个类模板,它的get()函数是复制数据,可以实现多次get

std::future<int> res=my.get_future();
//std::shared_future<int> res_s(std::move(res));
//或者
std::shared_furture<int> res_S(res.share());//此时res_s有值,res里空了

7. std::atomic 原子操作

<atomic>是C++标准程序库中的一个头文件,定义了C++11标准中一些表示线程、并发控制时进行原子操作的类与方法,主要声明了两大类原子对象:std::atomic和std::atomic_flag。

CAS是Compare And Swap的简称。意味比较并交换。

首先要说的是,为了保证Volatile的原子性操作,引入了三种方法。并且原子操作性能最高。他就是CAS。

那为什么CAS性能最好呢??

我们要来分析一下synchronized低效的原因:首先呢synchronized同步过程大家都有了解。不过这些过程不论是刷新主内存还是本地内存,都需要占用很多资源,所以性能低下。

再来了解CAS的原理:它有三个参数,当前内存值V,旧的预期值A,更新值B,只有当内存值和预期值相同时候,才会修改为B,否则就通过自旋锁的方式再次尝试,直到成功。(显然自旋次数过多也会造成影响)。然而CAS的过程其实没有获取和释放锁。它的运行和JMM内存模型没有关系。而是通过native方法调用本地方法直接和硬件打交道(使用了Unsafe unsafe = Unsafe.getUnsafe()。Unsafe 是CAS的核心类,提供的是硬件级别的原子操作)。因此性能更快。

CAS的不足有哪些?怎么解决的?
  1. 之前说过,CAS通过自旋的方式等待再次尝试,直到成功。因此有可能CAS长时间不成功,那么就需要CPU巨大的开销。

  2. 只能保证一个共享变量的原子操作,如果多个变量就需要使用锁。

  3. ABA问题:ABA问题简单的说就是存在这种可能性,由于我们的CAS会比较当前内存值和旧的预期值,如果相同就操作。如果有一个线程先把A改变成B,之后又变回A,那么另一个线程操作时候显然正确,它感知不到ABA的变化。就好比说,你喜欢一个姑娘很久了,姑娘现在没结婚,于是你就要和她结婚,但是在你回老家拿户口本的时候,这个姑娘和别人结婚,然后又离婚了,你回来的时候他仍然是单身。你品,你细品。问题大吧。

怎么解决ABA问题呢?于是呢就想出了版本控制这一个方法。我们在每一个变量上都加入一个版本号。改变的时候版本号增加,比较的时候版本号一同比较。

原子操作的主要特点是原子对象的并发访问不存在数据竞争,利用原子对象可实现数据结构的无锁设计。在多线程并发执行时,原子操作是线程不会被打断的执行片段。

(1)atomic_flag类

  1. 是一种简单的原子bool类型,只支持两种操作:test_and_set(flag=true)和clear(flag=false)。

  2. 跟std::atomic的其它所有特化类不同,它是锁无关的。

  3. 结合std::atomic_flag::test_and_set()和std::atomic_flag::clear(),std::atomic_flag对象可以当作一个简单的自旋锁(spin lock)使用。

  4. atomic_flag只有默认构造函数,禁用拷贝构造函数,移动构造函数实际上也禁用。

  5. 如果在初始化时没有明确使用宏ATOMIC_FLAG_INIT初始化,那么新创建的std::atomic_flag对象的状态是未指定的(unspecified),既没有被set也没有被clear;如果使用该宏初始化,该std::atomic_flag对象在创建时处于clear状态。

  • test_and_set:返回该std::atomic_flag对象当前状态,检查flag是否被设置,若被设置直接返回true,若没有设置则设置flag为true后再返回false。该函数是原子的。

  • clear:清除std::atomic_flag对象的标志位,即设置atomic_flag的值为false。

(2)std::atomic类

  1. std::atomic提供了针对bool类型、整形(integral)和指针类型的特化实现。每个std::atomic模板的实例化和完全特化定义一个原子类型。

  2. 若一个线程写入原子对象,同时另一个线程从它读取,则行为良好定义。

  3. 原子对象的访问可以按std::memory_order所指定建立线程间同步,并排序非原子的内存访问。

  4. std::atomic可以以任何可平凡复制(Trivially Copyable)的类型T实例化。

  5. std::atomic既不可复制亦不可移动。

  6. ATOMIC_VAR_INIT(val):可以由构造函数直接执行此宏初始化std::atomic对象。

std::atomic 常用的成员函数:

  1. std::atomic::store(val) 函数将参数 val 复制给原子对象所封装的值。

  2. std::atomic::load() 读取被原子对象封装的值。

  3. std::atomic::exchange(val) 读取并修改被封装的值,exchange 会将 val 指定的值替换掉之前该原子对象封装的值,并返回之前该原子对象封装的值,整个过程是原子的.

  4. atomic() 默认构造函数,由默认构造函数创建的 std::atomic 对象处于未初始化(uninitialized)状态,对处于未初始化(uninitialized)状态 std::atomic对象可以由 atomic_init 函数进行初始化。

  5. atomic (T val) 初始化构造函数,由类型 T初始化一个 std::atomic对象。

  6. atomic (const atomic&) 拷贝构造函数被禁用。


参考文献:

1. blog.csdn.net/liuxuejia

2. blog.csdn.net/fengbingc

3. cnblogs.com/wangshaowei

4. blog.csdn.net/fengbingc

5. cnblogs.com/taiyang-li/

6. blog.csdn.net/tanningzh

7.https://zhuanlan.zhihu.com/p/134099301 

8.https://zhuanlan.zhihu.com/p/136861784

本文仅作学术交流,如有侵权,请联系删文

目标检测系列秘籍一:模型加速之轻量化网络秘籍二:非极大值抑制及回归损失优化秘籍三:多尺度检测秘籍四:数据增强秘籍五:解决样本不均衡问题秘籍六:Anchor-Free视觉注意力机制系列Non-local模块与Self-attention之间的关系与区别?视觉注意力机制用于分类网络:SENet、CBAM、SKNetNon-local模块与SENet、CBAM的融合:GCNet、DANetNon-local模块如何改进?来看CCNet、ANN
语义分割系列一篇看完就懂的语义分割综述最新实例分割综述:从Mask RCNN 到 BlendMask超强视频语义分割算法!基于语义流快速而准确的场景解析CVPR2020 | HANet:通过高度驱动的注意力网络改善城市场景语义分割

这篇关于来聊聊C++中头疼的线程、并发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/508423

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给