• 【C++】线程库


    线程的基本概念

    • 在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差
    • C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念

    线程的使用

    常用接口:

    函数名功能
    thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
    thread(fn, args1, args2, …)构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的 参数
    get_id()获取线程id
    jionable()判断线程是否还在执行
    jion()该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行
    detach()分离线程
    swap(thread& x)交换两个线程

    线程是操作系统中的一个概念,是进程中的一个执行分支,线程对象可以关联一个线程,用来控制线程以及获取线程的状态

    当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

    #include <thread>
    int main(){
        std::thread t1;//空线程
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行

    线程函数就是可调用对象,可以有以下形式:

    • 函数指针
    • lambda表达式
    • 函数对象(仿函数)
    • function包装器

    当创建一个空线程时,thread是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行

    #include <iostream>
    #include <thread>
    using namespace std;
    void ThreadFunc(int a) {
    	cout << "Thread1" <<" " << a << endl;
    }
    
    void func(double i){
    	cout << i / 2 << endl;
    }
    class TOB {
    public:
    	void operator()(){
    		cout << "Thread3" << endl;
    	}
    };
    int main(){
    
    	//函数指针移动赋值给t1
    	thread t1;
    	t1 = thread(ThreadFunc, 998);
    
    	//线程函数为lambda表达式
    	thread t2([]() {
    		cout << "Thread2" << endl;
    		});
    
    	//线程函数为仿函数对象
    	TOB tb;
    	thread t3(tb);
    
    	//线程函数为包装器
    	function<void(double)> f1 = func;
    	thread t4(f1, 999);
    
    	t1.join();
    	t2.join();
    	t3.join();
    	t4.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述

    可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

    • 采用无参构造函数构造线程对象
    • 线程对象的状态已经转移给其他线程对象
    • 线程已经调用jion或者detach结束

    线程函数的参数

    线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

    #include <iostream>
    #include <thread>
    using namespace std;
    void Func1(int& x){
    	x += 10;
    	return;
    }
    void Func2(int* x){
    	*x += 10;
    	return;
    }
    int main(){
    	int a = 10;
    	// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
    	// 如果线程函数的接收是引用接收,vs2022会报错
    	/*thread t1(Func1, a);
    	t1.join();
    	cout << a << endl;*/
    
    	// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
    	thread t2(Func1, ref(a));
    	t2.join();
    	cout << a << endl;
    
    	// 也可以拷贝地址再解引用,实现对a的更改
    	thread t3(Func2, &a);
    	t3.join();
    	cout << a << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    如果是类成员函数作为线程参数时,必须将this指针作为线程函数参数

    #include <iostream>
    #include <thread>
    using namespace std;
    class A{
    public:
    	void Func1(int x){
    		cout << x << endl;
    	}
    	static void Func2(int x){
    		cout << x << endl;
    	}
    };
    int main(){
    	A* a = new A;
    	//非静态函数需要传入类的实例或者指针
    	thread t1(&A::Func1, a, 10);
    	//thread t1(&A::Func1, *a, 10);
    	t1.join();
    
    	delete a;
    
    	//静态成员函数只需传入函数指针和参数
    	thread t2(&A::Func2, 10);
    	t2.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    线程安全

    多线程的程序有多个执行流,那么如果一份资源可以被多个线程访问到,那么这个资源就是临界资源,如果多个线程同时对临界资源进行修改,就会出现线程安全问题

    比如用两个线程同时对一个全局变量进行++操作

    int main() {
    
    	vector<thread> vthreads;
    	vthreads.resize(2);
    	int N = 100000;
    	int x = 0;
    
    	for (auto& td : vthreads) {
    		td = thread([&N, &x] {
    			for (int i = 0; i < N; ++i)
    			{
    				++x;
    			}
    			});
    	}
    	for (auto& td : vthreads) {
    		td.join();
    	}
    	cout << x << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    按道理来说两个线程都对x进行++,++了100000次,那么x的最终值会是2

    在这里插入图片描述

    但是实际却少了几万次,是因为**++操作不是原子操作**,当一个线程对x进行++时,另一个线程也在执行++操作,就会导致两次++操作只有一次有效,就有了线程安全问题

    针对这个问题,要对临界区进行保护,那么可以在临界区加入互斥锁,保证线程安全

    int main() {
    
    	vector<thread> vthreads;
    	vthreads.resize(2);
    	mutex mtx;
    	int N = 100000;
    	int x = 0;
    
    	for (auto& td : vthreads) {
    		td = thread([&mtx, &N, &x] {
    			mtx.lock();
    			for (int i = 0; i < N; ++i)
    			{
    				++x;
    			}
    			mtx.unlock();
    			});
    	}
    	for (auto& td : vthreads) {
    		td.join();
    	}
    	cout << x << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    原子性操作库(atomic)

    虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
    因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效

    在这里插入图片描述

    int main() {
    
    	vector<thread> vthreads;
    	vthreads.resize(2);
    	int N = 100000;
    	//atomic<int> x = 0;
    	atomic_int x = 0;
    
    	for (auto& td : vthreads) {
    		td = thread([&N, &x]{
    			for (int i = 0; i < N; ++i)
    			{
                    //此时对x的++就是原子操作
    				++x;
    			}
    		});
    	}
    	for (auto& td : vthreads) {
    		td.join();
    	}
    	cout << x << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问,更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型

    atmoic<T> t; // 声明一个类型为T的原子类型变量t
    
    • 1

    注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。


    互斥锁(mutex)

    互斥锁的种类

    std::mutex

    C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动

    函数名函数功能
    lock()上锁:锁住互斥量
    unlock()解锁:释放对互斥量的所有权
    try_lock()尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞

    线程函数调用lock()时可能会发生以下三种情况

    • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
    • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
    • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

    线程函数调用try_lock()时可能会发生以下三种情况:

    • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
    • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
    • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

    std::recursive_mutex

    允许递归程序加锁

    • 其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock()
    • 除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同

    std::timed_mutex
    • 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() , try_lock_for()
    • 接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false
    • try_lock_until()接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false

    std::recursive_timed_mutex

    recursive_mutex和timed_mutex的结合


    互斥锁的异常安全

    锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guardunique_lock

    lock_guard

    std::lock_gurad 是 C++11 中定义的模板类

    源码:

    template <class _Mutex>
    class _NODISCARD lock_guard { // class with destructor that unlocks a mutex
    public:
        using mutex_type = _Mutex;
    	//引用的方式传入互斥量,在析构函数中加锁
        explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
            _MyMutex.lock();
        }
    
        lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
    	
        //析构函数中解锁
        ~lock_guard() noexcept {
            _MyMutex.unlock();
        }
    
        lock_guard(const lock_guard&) = delete;
        lock_guard& operator=(const lock_guard&) = delete;
    
    private:
        //接收外面锁的引用
        _Mutex& _MyMutex;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用:

    int main() {
    
    	vector<thread> vthreads;
    	vthreads.resize(2);
    	mutex mtx;
    	int N = 100000;
    	int x = 0;
    
    	for (auto& td : vthreads) {
    		td = thread([&mtx, &N, &x] {
    			//在需要加锁的地方声明
    			//如果想要控制加锁的范围,可以使用局部域
    			//可以使用局部域控制对象的生命周期,实现对加锁范围的控制
    			{
    				lock_guard<mutex> lg(mtx);
    				for (int i = 0; i < N; ++i)
    				{
    					++x;
    				}
    			}
    			///
    			}); 
    			//出了作用域,lg对象销毁,就会调用lock_guard的析构函数,解锁mtx
    	}
    	for (auto& td : vthreads) {
    		td.join();
    	}
    	cout << x << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运用了RAII的思想,用对象的生命周期来控制加锁和解锁防止lock和unlock的时候发生异常安全问题

    但是lock_guard并不支持在对象作用域范围内进行手动的解锁和上锁,所以就有了unique_lock

    unique_lock

    与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作

    与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

    • 上锁/解锁操作locktry_locktry_lock_fortry_lock_untilunlock
    • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
    • 获取属性owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)

    两个线程交替打印奇数偶数

    可以使用条件变量实现这个小程序

    在这里插入图片描述

    int main() {
    	mutex mtx;
    	condition_variable cv;
    	int n = 100;
    	bool flag = true;
    
    	//打印奇数
    	thread t1([&] {
    		int i = 1;
    		for (; i < n;) {
    			unique_lock<mutex> lock(mtx);
    			cv.wait(lock, [&flag] {return flag; });
    			cout << i << endl;
    			i += 2;
    			flag = false;
    			cv.notify_one();
    		}
    		});
    
    	//打印偶数
    	thread t2([&] {
    		int j = 2;
    		for (; j < n;) {
    			unique_lock<mutex> lock(mtx);
    			cv.wait(lock, [&flag] {return !flag; });
    			cout << j << endl;
    			j += 2;
    			flag = true;
    			cv.notify_one();
    		}
    		});
    
    	t1.join();
    	t2.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    可以保证两个线程不会连续打印,保证了线程的同步

  • 相关阅读:
    MySQL主从搭建--保姆级教学
    极坐标和直角坐标的雅克比矩阵推导
    【天线】【3】CST一些快捷键
    树形动态规划
    第八章 指针1
    利用干扰源模型确定多通道音频信号盲源分离
    react绑定样式实现切换主题改变组件样式
    内存管理
    elementUI 实现form表单中label文字两端对齐
    Linux XWindow的原理介绍。
  • 原文地址:https://blog.csdn.net/xiaomage1213888/article/details/125630813