资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

C++多线程笔记-创新互联

线程的基本使用 thread

创建线程的函数,头文件thread。参数可以是函数名、仿函数、lambda表达式、类等等。

创新互联是一家专业提供江汉企业网站建设,专注与做网站、网站设计H5高端网站建设、小程序制作等业务。10年已为江汉众多企业、政府机构等服务。创新互联专业网站建设公司优惠进行中。join

等待子线程执行完毕

子线程和子进程的死亡是有些区别的。主进程执行完成后,如果子进程没有执行完会被系统init托管;主线程执行完后,如果子线程还没有执行完,子线程会被强制结束。这就有了join函数,等待子线程执行完后在继续执行。

detach

守护线程,detach的作用是使主线程结束后,子线程不会强制结束

joinable

判断线程是否执行了join或者detach这两个函数,没有调用返回true

线程传递参数注意事项 线程中函数参数
  • 如果线程中的函数参数是引用类型,传递的其实也是拷贝。如果不给参数增加std::ref的话,除了指针都是值传递
  • 如果使用的detach后,使用指针传递要注意参数内存释放的时机,如果主线程结束后,子线程的参数内存肯定是会被释放的
  • 如果想改变主线程中的参数,在传递参数的时候可以加上std::ref,此时要注意内存问题
  • 如果传递int这种简单类型,推荐使用值传递,不要用引用
  • 如果传递类对象,参数需要用const &去接,这样可以少执行一次拷贝构造
  • 拒接使用隐士转换当参数,隐式转换实在子线程中进行转换的,可能出现内存问题,要使用显示转换
mutex互斥量用法、死锁 互斥量的用法

​ 包含#include 头文件

  • lock(),unlock()

    步骤:1.lock(),2.操作共享数据,3.unlock()。
    lock()和unlock()要成对使用
    2.2 lock_guard类模板

  • lock_guard sbguard(myMutex);
    取代lock()和unlock()
    lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()

死锁

​ 两个线程都已经锁了部分资源,还在互相请求对方已经上锁的资源,都不释放资源,就会造成死锁

解决死锁
  • std::lock()函数模板

    • std::lock(mutex1,mutex2……); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
    • 如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会
    • 已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
  • std::lock_guard的std::adopt_lock参数

    • std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock);
    • 加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
    • adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
unique_lock(类模板) unique_lock取代lock_guard
unique_lock比lock_guard灵活很多(多出来很多用法),效率差一点。
 unique_lockmyUniLock(myMutex);
unique_lock的第二个参数
  • std::adopt_lock:
    • 表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
    • 前提:必须提前lock
    • lock_guard中也可以用这个参数
  • std::try_to_lock
    • 尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
    • 使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方前提:不能提前lock();
    • owns_lock()方法判断是否拿到锁,如拿到返回true
  • std::defer_lock:
    • 如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
    • 不给它加锁的目的是以后可以调用unique_lock的一些方法
    • 前提:不能提前lock
unique_lock的成员函数(前三个与std::defer_lock联合使用)
  • lock()
    • 加锁,不用自己调用unlock()
  • unlock()
    • 因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()
  • try_lock()
    • 如果拿不到锁,返回false,否则返回true
  • release()
    • 解除绑定
条件锁 condition_variable

std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。

列举一些他的成员函数

wait()

线程等待,并且会放下锁

wait(线程名,条件函数(可以省略默认true))

调用该方法后会进入堵塞状态,需要等待比额的线程调用notify_one()或者notify_all()来唤醒,唤醒后会尝试继续拿起释放掉的锁

notify_one()

唤醒一个执行过wait还在等待的线程,无法指定唤醒哪一个,是随机唤醒1个

notify_all()

唤醒所有执行了wait还在等待的线程

future、saync

多线程如果函数有返回值,可以使用这两个去接收返回值

std::future result1 = std::async(mythread);

执行futrue的get方法,就可以获得mythread的返回值,如果函数还没有运行完,那么会进入堵塞,一直到有返回值再继续

futrue的wait就是等待返回值,是一个堵塞的方法。

saync的参数

saync有两个参数,第一个参数是如何创建线程,第二个参数是线程函数名字

如果不指定,就是两个随机

std::launch::deferred

该参数线程不会直接创建线程,而且等待执行。

当用户调用get方法的时候,才会执行线程。

注意:他不会创建线程,也是在主线程中执行的方法

std::launch::async

该参数就是直接创建线程直接。是直接执行的

std::packaged_task

类模板,它的模板参数是各种可调用对象,通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用。

std::packaged_taskmypt(mythread);

std::thread t1(std::ref(mypt), 1);

并不会直接调用线程,还是需要使用thread去创建显示,他的作用只是打包

可以通过 get_future(); 获取返回值

std::promise类模板

我们能够在某个线程中给它赋值,然后我们可以在其他线程中,把这个值取出来

std::promise myprom;
std::thread t1(mythread, std::ref(myprom), 180);
t1.join(); //在这里线程已经执行完了
std::future fu1 = myprom.get_future(); //promise和future绑定,用于获取线程返回值

把他当成一个参数传入,在线程中用setValue来改变他的值,在主线程获取参数

std::async和std::thread()区别:

std::thread()如果系统资源紧张可能出现创建线程失败的情况,如果创建线程失败那么程序就可能崩溃,而且不容易拿到函数返回值(不是拿不到)
std::async()创建异步任务。可能创建线程也可能不创建线程,并且容易拿到线程入口函数的返回值;

由于系统资源限制:
①如果用std::thread创建的线程太多,则可能创建失败,系统报告异常,崩溃。

②如果用std::async,一般就不会报异常,因为如果系统资源紧张,无法创建新线程的时候,async不加额外参数的调用方式就不会创建新线程。而是在后续调用get()请求结果时执行在这个调用get()的线程上。

如果你强制async一定要创建新线程就要使用 std::launch::async 标记。承受的代价是,系统资源紧张时可能崩溃。

③根据经验,一个程序中线程数量 不宜超过100~200 。

std::atomic

原子类,执行不会被中断

只能使用++ – 或者+=类似的运算符才可以保证原子性

Windows临界区

临界区跟上锁类似,有进入临界区,离开临界区,作用和和锁是一样的。

创建临界区

CRITICAL_SECTION my_winsec;

初始化临界区

InitializeCriticalSection(&my_winsec)

进入临界区

EnterCriticalSection(&my_winsec);

是可以重复进入的,mutex是无法重复lock的

离开临界区

LeaveCriticalSection(&my_winsec);

手写自动自动离开的临界区
class CWinLock {public:
    CWinLock(CRITICAL_SECTION *pCritmp)
    {my_winsec =pCritmp;
        EnterCriticalSection(my_winsec);
    }
    ~CWinLock()
    {LeaveCriticalSection(my_winsec)
    };
private:
    CRITICAL_SECTION *my_winsec;
};
递归锁 独占互斥量 std::recursive_mutex

std::mutex 独占式互斥量

std::recursive_mutex:允许在同一个线程中同一个互斥量多次被 lock() ,(但是递归加锁的次数是有限制的,太多可能会报异常),效率要比mutex低。

如果你真的用了 recursive_mutex 要考虑代码是否有优化空间,如果能调用一次 lock()就不要调用多次。

带超时的互斥量 std::timed_mutex 和 std::recursive_timed_mutexstd::timed_mutex:是待超时的独占互斥量
  • try_lock_for():

  • 等待一段时间,如果拿到了锁,或者超时了未拿到锁,就继续执行(有选择执行)

  • try_lock_until():

    • 参数是一个未来的时间点,在这个未来的时间没到的时间内,如果拿到了锁头,流程就走下来,如果时间到了没拿到锁,流程也可以走下来。

    两者的区别就是一个参数是时间段,一个参数是时间点

std::recursive_timed_mutex:是待超时的递归独占互斥量自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。即不断的消耗cpu

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

class CAS
{private:
	std::atomicflag;
public:
	CAS():flag(false) {}
	~CAS() {}
	CAS(const CAS& s) = delete;
	CAS& operator=(const CAS&) = delete;
	void lock()
	{bool expect = false;
		while (!flag.compare_exchange_strong(expect, true))
		{	expect = false;
		}
	}
	void unlock()
	{flag.store(false);
	}
};
compare_exchange_strong

当前值与期望值(expect)相等时,修改当前值为设定值(desired,第二个参数),返回true
当前值与期望值(expect)不等时,将期望值(expect)修改为当前值,返回false

总结

自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。

自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。

自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。

自旋锁本身无法保证公平性,同时也无法保证可重入性。

基于自旋锁,可以实现具备公平性和可重入性质的锁

读写锁

相比互斥锁,读写锁允许更高的并行性,互斥量要么锁住状态要么不加锁,而且一次只有一个线程可以加锁。
读写锁可以有三种状态:

  • 读模式加锁状态;
  • 写模式加锁状态;
  • 不加锁状态;
排他性锁定 lock

锁定互斥。若另一线程已锁定互斥,则lock的调用线程将阻塞执行,直至获得锁。
若已以任何模式(共享或排他性)占有 mutex 的线程调用 lock ,则行为未定义。也就是说,已经获得读模式锁或者写模式锁的线程再次调用lock的话,行为是未定义的。
注意:通常不直接使用std::shared_mutex::lock(),而是通过unique_lock或者lock_guard进行管理。

unlock

解锁互斥。
互斥必须为当前执行线程所锁定,否则行为未定义。如果当前线程不拥有该互斥还去调用unlock,那么就不知道去unlock谁,行为是未定义的。
注意:通常不直接调用 unlock() 而是用 std::unique_lock 与 std::lock_guard 管理排他性锁定。

共享锁定 std::shared_mutex::lock_shared

相比mutex,shared_mutex还拥有lock_shared函数。
该函数获得互斥的共享所有权。若另一线程以排他性所有权保有互斥,则lock_shared的调用者将阻塞执行,直到能取得共享所有权。
若已以任何模式(排他性或共享)占有 mutex 的线程调用 lock_shared ,则行为未定义。即:当以读模式或者写模式拥有锁的线程再次调用lock_shared时,行为是未定义的,可能产生死锁。
若多于实现定义大数量的共享所有者已以共享模式锁定互斥,则 lock_shared 阻塞执行,直至共享所有者的数量减少。所有者的大数量保证至少为 10000。
注意:通常不直接调用 lock_shared() 而是用 std::shared_lock 管理共享锁定。
shared_lock与unique_lock的使用方法类似、

std::shared_mutex::unlock_shared

将互斥从调用方线程的共享所有权释放。
当前执行线程必须以共享模式锁定互斥,否则行为未定义。
通常不直接调用 unlock_shared() 而是用 std::shared_lock 管理共享锁定。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


网站标题:C++多线程笔记-创新互联
当前地址:http://cdkjz.cn/article/cciecc.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220