• 【C++】C++多线程库的使用


    在C++11之前,涉及到多线程问题,都是和平台相关的,比如windowslinux下各有自己的接口,这使得代码的可移植性比较差,如果想要多平台能够同时运行就要使用条件编译写两份代码。

    C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含头文件。

    一、线程库(thread)

    使用线程库,必须包含 < thread > 头文件。

    1、线程的id类

    在了解线程库之前我们先讲一个前置知识,我们知道每个线程都要有自己的线程id,在线程库类中有一个内嵌类型id类,此类是用数字表示的是线程id,并且此id类支持比较和流插入运算符

    在这里插入图片描述

    2、线程对象的构造

    线程对象的构造函数如下:

    在这里插入图片描述

    1. thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,内部只有线程对象的相关属性但是没有启动任何线程, 此时线程的id0

    2. thread的带参的构造函数,是一个可变参数模板函数,参数说明如下:

      • fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。
      • args... : 调用可调用对象fn时所需要的若干参数。
    3. 线程是不允许拷贝的,所以其拷贝函数是delete的。

      • 同理线程对象也只允许移动赋值
        在这里插入图片描述
    4. thread提供了移动赋值函数,因此当后续需要让该无参的线程对象与线程关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程转移给该无参的线程对象。

    void func(int n)
    {
    	for (int i = 0; i <= n; i++)
    	{
    		cout << i << endl;
    	}
    }
    int main()
    {
    	// 创建无参的线程对象
    	thread t1;
    	// 移动构造
    	t1 = thread(func, 10);
    
    	t1.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3、thread提供的其他成员函数

    thread中常用的成员函数如下:

    成员函数功能
    join对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞
    joinable判断该线程是否已经执行完毕,如果是则返回true,否则返回false
    detach将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待
    get_id获取该线程的id
    swap将两个线程对象关联线程的状态进行交换

    说明:

    • thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
    • 线程结束以后其线程id会变为0
    • 对同一个线程join两次会导致程序崩溃

    4、this_thread命名空间

    this_threadstd命名空间下的一个子命名空间,此命名空间提供了访问当前线程的一组函数。

    函数功能
    get_id获得当前线程的id
    yield当前线程“放弃”执行,让出时间片,让操作系统调度另一线程继续执行
    sleep_until让当前线程休眠到一个具体时间点
    sleep_for让当前线程休眠一个时间段

    我们调用thread的成员函数get_id可以获取线程的id,但该方法必须通过线程对象来调用get_id函数,如果要在线程对象关联的线程函数中获取线程id,可以调用this_thread命名空间下的get_id函数。

    void func()
    {
     	//获取线程id
    	cout << this_thread::get_id() << endl;
    }
    int main()
    {
    	thread t(func);
    
    	t.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5、线程函数的参数问题

    线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:

    void add(int& num)
    {
    	num = 10;
    }
    
    int main()
    {
    	int num = 0;
    	thread t(add, num);
    	t.join();
    
    	cout << num << endl; //0
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ps :这段代码可能在vs等编译器下报错,不过不用在意,这种写法的使用也不正确

    如果我们想要通过形参改变外部实参时,必须采用以下3种方式:

    1. 如果是引用,传参时可以借助std::ref()函数。
    2. 通过地址的拷贝,利用解引用来修改实参
    3. 利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉
    #include 
    void ThreadFunc1(int& x)
    {
    	x += 10;
    }
    
    void ThreadFunc2(int* x)
    {
    	*x += 10;
    }
    
    int main()
    {
    	int a = 10;
    	// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
    	thread t1(ThreadFunc1, std::ref(a));
    	t1.join();
    	cout << a << endl;
    
    	// 利用地址的拷贝,也能改变外部实参
    	thread t2(ThreadFunc2, &a);
    	t2.join();
    	cout << a << endl;
    
    	// 利用lambda表达式的捕捉列表
    	thread t3([&] {a += 10; });
    	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

    在这里插入图片描述

    二、互斥量库(mutex)

    使用互斥量库,必须包含 < mutex > 头文件。

    1、mutex的种类

    在C++11中,mutex总共包了四个互斥量的种类:
    在这里插入图片描述


    • mutex

    mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝。

    mutex中常用的成员函数如下:

    成员函数功能
    lock对互斥量进行加锁
    try_lock尝试对互斥量进行加锁,如果加锁失败就立即返回,不会阻塞在锁上
    unlock对互斥量进行解锁,释放互斥量的所有权

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

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

    线程调用try_lock时,类似也可能会发生以下三种情况:

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

    • recursive_mutex

    recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。

    • 如果在递归函数中使用mutex互斥锁进行加锁,那么在线程进行递归调用时,可能会重复申请已经申请到但自己还未释放的锁,进而导致死锁问题。

    • recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的unlock

    除此之外,recursive_mutex也提供了lock、try_lockunlock成员函数,其的特性与mutex大致相同。

    我们看到下面的代码能够保证线程在递归时也能一直占有锁,只有当递归完成才会释放锁,两个线程对全局变量进行递归++操作。

    #include 
    #include 
    #include 
    
    std::recursive_mutex mtx;
    int  x = 0;
    void recursiveFunction(int count)
    {
        mtx.lock();
        std::cout << "Thread " << std::this_thread::get_id() << ": Lock acquired, count = "
            << count << std::endl;
    
        if (count > 0) 
        {
            x++;
            recursiveFunction(count - 1);
        }
        
        std::cout << "Thread " << std::this_thread::get_id() << ": Lock released, count = " 
            << count << std::endl;
        mtx.unlock();
    }
    
    int main()
    {
        std::thread t1(recursiveFunction, 3);
        std::thread t2(recursiveFunction, 2);
    
        t1.join();
        t2.join();
        cout << "------------------------------------------" << endl;
        cout << "final result: " << 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
    • 31
    • 32
    • 33
    • 34
    • 35

    在这里插入图片描述


    • timed_mutex

    timed_mutex中提供了以下两个成员函数:

    • try_lock_for:接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间之内还是没有获得锁),则返回false。

    • try_lock_untill:接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间点到来时还是没有获得锁),则返回false。

    • 除此之外,timed_mutex也提供了lock、try_lockunlock成员函数,其的特性与mutex相同。

    利用时间锁可以写一些有趣的程序,下面的运行结果是不能确定的

    #include        // std::cout
    #include          // std::chrono::milliseconds
    #include          // std::thread
    #include           // std::timed_mutex
    
    std::timed_mutex mtx;
    
    void fireworks() 
    {
        // 等待获得锁 : 每个线程每200ms打印"-"
        while (!mtx.try_lock_for(std::chrono::milliseconds(200))) 
        {
            std::cout << "-";
        }
        // 得到一个锁! 等待1秒,然后这个线程打印"*"
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "*\n";
        mtx.unlock();
    }
    
    int main()
    {
        std::thread threads[10];
        // 启动 10 个线程:
        for (int i = 0; i < 10; ++i)
            threads[i] = std::thread(fireworks);
    
        // join 10个线程
        for (auto& th : threads) th.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

    在这里插入图片描述

    • std::recursive_timed_mutex

    recursive_timed_mutex就是recursive_mutex和timed_mutex的结合,recursive_timed_mutex既支持在递归函数中进行加锁操作,也支持定时锁。


    2、lock_guard和unique_lock

    互斥锁的使用很简单,但是有些情况下互斥锁的使用可能会变得很棘手,例如:

    • 如果加锁的范围太大,那么极有可能在中途返回时忘记了解锁,那么以后其他线程申请这个互斥锁的就会被阻塞住,也就是造成了死锁问题。

    • 又或者是如果线程在锁的范围内抛异常,导致没有解锁,也很容易导致死锁问题。

    #include       
    #include         
    #include 
    
    int x = 0;
    mutex mtx;
    void Func(int n)
    {
    	for (size_t i = 0; i < n; i++)
    	{
    		try
    		{
    			mtx.lock();
    			++x;
    			cout << x << endl;
    			if (rand() % 3 == 0)
    			{
    				throw exception("抛异常");
    			}
    			mtx.unlock();
    		}
    		catch (const std::exception& e)
    		{
    			cout << e.what() << endl;
    		}
    	}
    	
    }
    
    int main()
    {
    	thread t1(Func, 10);
    	thread t2(Func, 10);
    
    	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
    • 37
    • 38

    在这里插入图片描述

    为了解决上面的问题,C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guardunique_lock


    lock_guard

    lock_guard是C++11中的一个模板类,其定义如下:

    在这里插入图片描述

    lock_guard类模板主要是通过RAII的方式,对其管理的互斥锁进行了封装。

    • 在需要加锁的地方,用互斥锁实例化一个lock_guard对象,在lock_guard的构造函数中会调用lock()进行加锁。

    • lock_guard对象出作用域前会调用析构函数,在lock_guard的析构函数中会调用unlock()自动解锁。

    • lock_guard对象定义到该对象析构,这段区域的代码都属于互斥锁的保护范围。

    • lock_guard类对象也是也是不支持拷贝的。

    有了这种方法我们就不用害怕出现上面的情况了!

    #include               
    #include      
    #include 
    
    int x = 0;
    mutex mtx;
    void Func(int n)
    {
    	for (size_t i = 0; i < n; i++)
    	{
    		try
    		{
    			lock_guard<mutex> lck(mtx);
    			++x;
    			cout << x << endl;
    			if (rand() % 3 == 0)
    			{
    				throw exception("抛异常");
    			}
    		}
    		catch (const std::exception& e)
    		{
    			cout << e.what() << endl;
    		}
    	}
    	
    }
    
    int main()
    {
    	thread t1(Func, 10);
    	thread t2(Func, 10);
    
    	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
    • 37

    在这里插入图片描述

    lock_guard的模拟实现

    对lock_guard的模拟实现我们只要做到以下几点:

    • 利用构造函数进行加锁,利用析构函数进行解锁,
    • 由于锁不能被拷贝以及所有的线程要看到同一把锁,我们对成员函数必须采用引用
    • 由于lock_guard对象也不能够进行拷贝,我们要对拷贝以及赋值进行delete
    template<class Mutex>
    class lock_guard
    {
    public:
    	lock_guard(Mutex& mtx)
    		:_mtx(mtx)
    	{
    		mtx.lock(); //加锁
    	}
    	~lock_guard()
    	{
    		mtx.unlock(); //解锁
    	}
    	lock_guard(const lock_guard&) = delete;
    	lock_guard& operator=(const lock_guard&) = delete;
    private:
    	Mutex& _mtx;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    unique_lock

    但由于lock_guard太单一,用户没有办法对锁进行控制,因此C++11又提供了unique_lock

    unique_lock与lock_guard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装。在创建unique_lock对象调用构造函数时也会调用lock进行加锁,在unique_lock对象销毁调用析构函数时也会调用unlock进行解锁。

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

    • 加锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
    • 修改操作:移动赋值operator=、swap、release(返回它所管理的互斥量对象的指针,并释放所有权)。
    • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool(与owns_lock的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

    以下场景就适合使用unique_lock:

    1. 需要在锁定期间多次解锁和重新锁定:std::unique_lock 允许在锁定期间多次释放和重新获取锁。这对于需要在锁定期间执行复杂的逻辑或条件判断的情况非常有用。

    2. 需要延迟锁定:std::unique_lock 允许在构造时不立即锁定互斥量,而是在需要时手动调用 lock 函数进行锁定。这对于需要在一段代码中的某个特定位置才需要锁定的情况非常有用。

    三、条件变量库(condition_variable)

    使用条件变量库,必须包含 < condition_variable > 头文件。

    condition_variable中提供的成员函数,可分为wait系列和notify系列两类。


    1、wait系列

    在这里插入图片描述

    • 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。

    • 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,线程在进行wait之前会先判断可调用对象是否为假,如果为假就进行等待,否则就返回。当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

    wait_for和wait_until函数的使用方式与wait函数类似:

    在这里插入图片描述

    • wait_for函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个时间段,表示让线程在该时间段内进行阻塞等待,如果超过这个时间段则线程被自动唤醒。

    • wait_until函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个具体的时间点,表示让线程在该时间点之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒。

    • 线程调用wait_forwait_until函数在阻塞等待期间,其他线程调用notify系列函数也可以将其唤醒。此外,如果调用的是wait_forwait_until函数的第二个版本的接口,那么当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么当前线程还需要继续被阻塞。


    2、notify系列

    notify系列成员函数的作用就是唤醒等待的线程,包括notify_onenotify_all

    notify_one:唤醒等待队列中的任意一个线程,如果等待队列为空则什么也不做。
    notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

    实现两个线程交替打印1-100

    实现这个问题的关键是对同步与互斥的把握,

    1. 怎么让线程1先打印?
    2. 怎么让线程相互交替打印?
    • 对于问题1,我们可以用条件判断判断当前是否是奇数,如果是奇数就打印,如果是偶数就等待,这样线程1不论是先运行还是后运行都会先打印。
    • 对于第二个问题我们可以利用条件变量实现同步功能,一个线程打印完并++以后通知另一个线程打印并++,然后等待另一个线程给自己发通知自己再打印++,如此循环往复便能够达到效果了。
    #include 
    #include 
    #include 
    #include 
    
    int x = 1;
    mutex mtx;
    condition_variable cv;
    
    void Func_1()
    {
    	unique_lock<mutex> lck(mtx);
    	while (x < 100)
    	{
    		if (x % 2 == 0) // 偶数阻塞
    		{
    			cv.wait(lck);
    		}
    		// 或者这样写也行
    		//cv.wait(lck, []() {return x % 2 != 0; });
    
    		cout << this_thread::get_id() << " :" << x++ << endl;
    		cv.notify_one();
    	}
    }
    
    void Func_2()
    {
    	unique_lock<mutex> lck(mtx);
    	while (x <= 100)
    	{
    		if (x % 2 != 0) // 奇数阻塞
    		{
    			cv.wait(lck);
    		}
    		// 或者这样写也行
    		// cv.wait(lck, []() {return x % 2 == 0; });
    
    		cout << this_thread::get_id() << " :" << x++ << endl;
    		cv.notify_one();
    	}
    }
    
    int main()
    {
    	thread t1(Func_1);
    	thread t2(Func_2);
    
    	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
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    四、原子性操作库(atomic)

    使用原子性操作库(atomic),必须包含 < atomic > 头文件。

    1、类型的基本介绍

    多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。

    例如下面的程序,对一个变量进行累加,如果是单线程计算结果一定没有问题,但是对于多线程计算结果就有问题了。

    #include 
    #include 
    
    int g_val_1 = 0;
    int g_val_2 = 0;
    
    void multiThread(int num)
    {
    	for (size_t i = 0; i < num; i++)
    	{
    		g_val_1++;
    	}
    }
    
    void singleThread(int num)
    {
    	for (size_t i = 0; i < num; i++)
    	{
    		g_val_2++;
    	}
    }
    
    int main()
    {
    	thread t1(multiThread, 100000);
    	thread t2(multiThread, 200000);
    	singleThread(300000);
    
    	t1.join();
    	t2.join();
    	cout << "g_val_1 : "<<g_val_1 << endl;
    	cout << "g_val_2 : "<<g_val_2 << 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
    • 32
    • 33
    • 34
    • 35

    当然这里可以通过加锁来进行解决,但是加锁是一件有损于性能的事情。为了解决这样的问题,C++11提供了原子操作类型,对此类型的操作都是原子的,这样我们就不必进行加锁了。

    C++11中引入了原子操作类型,如下:

    原子类型名称对应的内置类型名称
    atomic_boolbool
    atomic_charchar
    atomic_scharsigned char
    atomic_ucharunsigned char
    atomic_intint
    atomic_uintunsigned int
    atomic_shortshort
    atomic_ushortunsigned short
    atomic_longlong
    atomic_ulongunsigned long
    atomic_llonglong long
    atomic_ullongunsigned long long
    atomic_char16_tchar16_t
    atomic_char32_tchar32_t
    atomic_wchar_twchar_t

    将上面的代码进行一点点改变:

    ...
    atomic_int g_val_1 = 0;
    atomic_int g_val_2 = 0;
    ...
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    除此之外,也可以使用atomic类模板定义出任意原子类型,原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝。

    在这里插入图片描述

    因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。

    #include 
    int main()
    {
    	atomic<int> a1(0);
    	//atomic a2(a1); // 编译失败
    	
    	atomic<int> a2(0);
    	//a2 = a1; // 编译失败
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2、成员函数

    • is_lock_free函数

    is_lock_free函数是一个成员函数,is_lock_free()检测是否该类型内部是通过使用锁模拟的,若返回false则表示该原子类型是库或是编译器内部使用一个锁实现的,调用此成员函数不会启动任何数据竞争。

    #include 
    #include 
    #include 
    
    struct A { int a[100]; };
    struct B { int x, y; };
    int main()
    {
        std::cout << std::boolalpha
            << "atomic is lock free? "
            << std::atomic<A>().is_lock_free() << endl;
    
        cout <<"atomic is lock free? "
            << std::atomic<B>{}.is_lock_free() << endl;
    }
    

    在这里插入图片描述

    • store函数

    用于将给定的值存储到原子对象中。

    int main()
    {
    	atomic<int> atInt(0);
    	int a = 10;
    
    	atInt.store(a);
    	cout << atInt << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    由于运算符重载,我们更愿意使用=来进行赋值。(=不能用于对象拷贝)
    在这里插入图片描述

    int main()
    {
    	atomic<int> atInt(0);
    	int a = 10;
    	// 利用了运算符
    	atInt = a;
    	cout << atInt << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • load函数

    load函数用于获取原子变量的当前值,由于下面的函数的存在,我们更愿意隐式使用。
    在这里插入图片描述

    int main()
    {
    	atomic<int> atInt(0);
    	// 显示使用
    	cout << atInt.load() << endl;
    	// 利用了 operator T()
    	cout << atInt << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • exchange函数

    访问和修改包含的值,将包含的值替换并返回它前面的值。

    int main()
    {
    	atomic<int> atInt(0);
    	cout << atInt.exchange(10) << endl;
    	cout << atInt << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    • compare_exchange_weak函数

    这个函数的作用是将 atomic 对象的包含值的内容与预期值进行比较:

    • 如果为true,则用val替换包含的值
    • 如果为false,则用包含的值替换expected
    int main()
    {
    	atomic<int> atInt(0);
    	int a = 1;
    	// 失败后 a = 0
    	cout << atInt.compare_exchange_weak(a, 9) << endl;
    	// 成功!
    	cout << atInt.compare_exchange_weak(a, 9) << endl;
    	//cout << atInt.exchange(10) << endl;
    	cout << atInt << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    注意
    compare_exchange_weak函数是一个弱化版本的原子操作函数,因为在某些平台上它可能会失败并重试。如果需要保证严格的原子性,则应该使用compare_exchange_strong函数。

    • compare_exchange_strong函数
      这个函数的作用和compare_exchange_weak类似,都是比较一个值和一个期望值是否相等,并且在相等时将该值替换成一个新值。不同的是,compare_exchange_strong会保证原子性,并且如果比较失败则会返回当前值。

    • 专业化支持的操作(仅仅支持整形(bool除外)和指针)
    函数名功能
    fetch_add添加到包含的值并返回它在操作之前具有的值
    fetch_sub从包含的值中减去,并返回它在操作之前的值。
    fetch_and读取包含的值,并将其替换为在读取值和之间执行按位 AND 运算的结果。
    fetch_or读取包含的值,并将其替换为在读取值和 之间执行按位 OR 运算的结果。
    fetch_xor读取包含的值,并将其替换为在读取值和 之间执行按位 XOR 运算的结果。

    在这里插入图片描述

    • atomic::operator (comp. assign.)(仅仅支持整形(bool除外)和指针)

    由于运算符的重载,我们可以直接使用运算符
    在这里插入图片描述

    在这里插入图片描述

    3、atomic_flag类

    在这里我们先介绍一个专门的atomic类,atomic_flag是最简单的标准原子类型,他代表一个布尔标识,没有拷贝构造函数和拷贝赋值运算符(=delete)。

    在这里插入图片描述

    • atomic_flag 默认状态不能确定。可以使用 ATOMIC_FLAG_INIT 宏进行初始化,对象使用该宏初始化,那么可以保证该 atomic_flag对象在创建时处于 clear 状态。
    atomic_flag flag = ATOMIC_FLAG_INIT;
    
    • 1
    • atomic_flag 提供了两个成员函数 test_and_set()clear() 来测试和设置标志位。
      • test_and_set() 函数会将标志位置为 true,并返回之前的值;
      • clear() 函数将标志位置为 false
    • atomic_flagtest_and_set() clear() 操作是原子的,可以保证在多线程环境下正确执行。
    • atomic_flag 只能表示两种状态,即 truefalse,不能做其他比较操作。通常情况下,atomic_flag 被用作简单的互斥锁,而不是用来存储信息。
    #include 
    #include 
    int main()
    {
    	// 进行初始化 false
    	atomic_flag flag = ATOMIC_FLAG_INIT;
    	// 返回 0
    	cout << flag.test_and_set() << endl;
    	// 返回 1
    	cout << flag.test_and_set() << endl;
    	// 没有返回值
    	flag.clear();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

  • 相关阅读:
    随机过程理论知识(三)
    【SpringBoot系列】Arthas配合docker轻松实现线上热更新
    熬夜整理的Figma插件合集分享,快码住!
    (10)点云数据处理学习——基于手机视频和工具实现三位点云
    【MongoDB】索引 - 数组字段的多键索引
    Java8实战-总结23
    linux下通过wifi连接网络
    理解Go语言中的GOPATH
    羡慕实时数据看板?来看看Python的交互数据分析可视化工具! ⛵
    论文翻译:2021_LACOPE: Latency-Constrained Pitch Estimation for Speech Enhancement
  • 原文地址:https://blog.csdn.net/qq_65207641/article/details/132301044