• C++11之线程库


    thread类的简单介绍

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

    线程对象的构造方式

    1.调用无参的构造函数

    thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程。比如:

    thread t1;
    
    • 1

    thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象。

    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		std::cout << i << std::endl;
    	}
    }
    int main()
    {
    	std::thread t1;
    	t1 = std::thread(Func, 2);
    
    	t1.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    场景: 实现线程池的时候就是需要先创建一批线程,但一开始这些线程什么也不做,当有任务到来时再让这些线程来处理这些任务。

    2.调用带参的构造函数

    thread的带参的构造函数的定义如下:

    template <class Fn, class... Args>
    explicit thread (Fn&& fn, Args&&... args);
    
    
    • 1
    • 2
    • 3
    • fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。
    • args...:调用可调用对象fn时所需要的若干参数。
    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		std::cout << i << std::endl;
    	}
    }
    int main()
    {
    	std::thread t1(Func, 2);
    
    	t1.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.调用移动构造函数

    thread提供了移动构造函数,能够用一个右值线程对象来构造一个线程对象。

    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		std::cout << i << std::endl;
    	}
    }
    int main()
    {
    	std::thread t1 = std::thread(Func, 2);
    
    	t1.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意:thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。

    thread提供的成员函数

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

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

    此外,joinable函数还可以用于判定线程是否是有效的,如果是以下任意情况,则线程无效:

    • 采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程);
    • 线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理);
    • 线程已经调用join或detach结束。(线程已经结束)。

    获取线程的id的方式

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

    void Func(int n)
    {
    	std::cout << std::this_thread::get_id() << std::endl;
    }
    int main()
    {
    	std::thread t1(Func, 1);
    
    	t1.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    this_thread命名空间中还提供了以下三个函数:

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

    互斥量库(mutex)

    对于下面这段程序,看似没什么问题,但是当我们打印出来就会发现,每一次x的值都是不一样的,因为产生了线程安全的问题,那么如何解决这个问题呢?就需要用到我们的mutex了。

    int x = 0;
    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		x++;
    	}
    }
    int main()
    {
    	int n = 10000;
    
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在C++11中,mutex总共包了四个互斥量的种类:

    1.std::mutex

    mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。mutex中常用的成员函数如下:

    成员函数功能
    lock对互斥量进行加锁
    try_lock尝试对互斥量进行加锁
    unlock对互斥量进行解锁,释放互斥量的所有权

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

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

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

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

    我们此时对上面的代码进程修改:

    int x = 0;
    std::mutex mtx;
    
    void Func(int n)
    {
    	
    	for (int i = 0; i < n; i++)
    	{
    		mtx.lock();
    		x++;
    		mtx.unlock();
    	}
    }
    int main()
    {
    	int n = 10000;
    
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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

    此时我们就会发现,打印出来的x的值一直都是20000,并不会发生任何改变。

    2. std::recursive_mutex

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

    我们来看下面这段代码,我们发现运行他以后程序就崩溃了,那是因为如果在递归函数中使用mutex互斥锁进行加锁,那么在线程进行递归调用时,可能会重复申请已经申请到但自己还未释放的锁,进而导致死锁问题。

    int x = 0;
    std::mutex mtx;
    
    void Func(int n)
    {
    	if (n == 0)
    		return;
    
    	mtx.lock();
    	x++;
    
    	Func(n - 1);
    
    	mtx.unlock();
    }
    int main()
    {
    	int n = 10000;
    
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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

    此时就可以使用我们的std::recursive_mutex了。

    int x = 0;
    //std::mutex mtx;
    std::recursive_timed_mutex mtx;
    
    void Func(int n)
    {
    	if (n == 0)
    		return;
    
    	mtx.lock();
    	x++;
    
    	Func(n - 1);
    
    	mtx.unlock();
    }
    int main()
    {
    	int n = 10000;
    
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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

    我们为你会发现,上面的代码在Debug版本下就崩了,但在Realse版本下就可以正常运行,那是因为每个线程都拥有自己的独立栈空间,但是又不是很大,递归的次数太多就会导致溢出,程序就崩了。

    3.std::timed_mutex

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

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

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

    4.std::recursive_timed_mutex

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

    其实加锁的方式有很多种,可以在for循环内加锁,也可以在for循环外加锁,接下来我们来看一看在for循环外进行加锁。

    int x = 0;
    std::mutex mtx;
    
    void Func(int n)
    {
    	mtx.lock();
    	for (int i = 0; i < n; i++)
    	{
    		x++;
    	}
    	mtx.unlock();
    }
    int main()
    {
    	int n = 10000;
    
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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

    那么他们有什么区别呢?

    • 此处在for循环体外加锁比在for循环体内加锁更高效,因为在for循环体内加锁会导致线程打印数字时频繁进行加锁解锁操作,而如果在for循环体外加锁,那么这两个线程只需要在开始打印1之前进行一次加锁,在打印完10000后进行一次解锁就行了。
    • 在for循环体外加锁也就意味着两个线程的打印过程变成了串行的,即一个线程打印完1-10000后另一个线程再打印,但这时打印效率提高了,因为避免了这两个线程间的频繁切换。
    • 为了保证两个线程使用的是同一个互斥锁,线程函数必须以引用的方式接收传入的互斥锁,并且在传参时需要使用ref函数保持对互斥锁的引用。
    • 此外,也可以将互斥锁定义为全局变量,或是用lambda表达式定义线程函数,然后以引用的方式将局部的互斥锁进行捕捉,这两种方法也能保证两个线程使用的是同一个互斥锁。
    int main()
    {
    	int n = 10000;
    	int x = 0;
    	std::mutex mtx;
    
    	std::thread t1([&, n]()
    		{
    			mtx.lock();
    			for (int i = 0; i < n; i++)
    			{
    				x++;
    			}
    			mtx.unlock();
    		});
    
    	std::thread t2([&, n]()
    		{
    			mtx.lock();
    			for (int i = 0; i < n; i++)
    			{
    				x++;
    			}
    			mtx.unlock();
    		});
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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

    lock_guard和unique_lock

    使用互斥锁时,如果加锁的范围太大,那么极有可能在中途返回时忘记了解锁,此后申请这个互斥锁的线程就会被阻塞住,也就是造成了死锁问题。比如出现抛异常的时候:

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

    我们会发现,此时加锁以后就没有解锁,抛异常后直接跳跃到catch位置,没有解锁,也就造成是死锁问题。

    因此C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

    lock_guard

    template <class Mutex>
    class lock_guard;
    
    • 1
    • 2

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

    • 在需要加锁的地方,用互斥锁实例化一个lock_guard对象,在lock_guard的构造函数中会调用lock进行加锁。
    • 当lock_guard对象出作用域前会调用析构函数,在lock_guard的析构函数中会调用unlock自动解锁。

    通过这种构造对象时加锁,析构对象时自动解锁的方式就有效的避免了死锁问题。

    模拟实现lock_guard:

    • lock_guard类中包含一个锁成员变量(引用类型),这个锁就是每个lock_guard对象管理的互斥锁。
    • 调用lock_guard的构造函数时需要传入一个被管理互斥锁,用该互斥锁来初始化锁成员变量后,调用互斥锁的lock函数进行加锁。
    • lock_guard的析构函数中调用互斥锁的unlock进行解锁。
    • 需要删除lock_guard类的拷贝构造和拷贝赋值,因为lock_guard类中的锁成员变量本身也是不支持拷贝的。
    template<class lock>
    class lockGuard
    {
    public:
    	lockGuard(lock& lk)
    		:_lk(lk)
    	{
    		_lk.lock();
    	}
    
    	~lockGuard()
    	{
    		_lk.unlock();
    	}
    
    	lockGuard(const lockGuard&) = delete;
    	lockGuard& operator=(const lockGuard&) = delete;
    private:
    	lock& _lk;
    };
    
    int x = 0;
    std::mutex mtx;
    
    void Func(int n)
    {
    	try
    	{
    		lockGuard<std::mutex> lock(mtx);
    		for (int i = 0; i < n; i++)
    		{
    			x++;
    			if (i % 3 == 0)
    			{
    				throw std::exception("抛异常");
    			}
    		}
    	}
    	catch (const std::exception& e)
    	{
    		std::cout << e.what() << std::endl;
    	}
    }
    int main()
    {
    	int n = 10000;
    
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    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。
    • 修改操作:移动赋值、swap、release(返回它所管理的互斥量对象的指针,并释放所有权)。
    • 获取属性:owns_lock(返回当前对象是否上了锁)、operator
      bool(与owns_lock的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

    比如如下场景就适合使用unique_lock:

    • 要用互斥锁保护函数1的大部分代码,但是中间有一小块代码调用了函数2,而调用函数2时不需要用函数1中的互斥锁进行保护,函数2内部的代码由其他互斥锁进行保护。
    • 因此在调用函数2之前需要对当前互斥锁进行解锁,当函数2调用返回后再进行加锁,这样当调用函数2时其他线程调用函数1就能够获取到这个锁。

    在这里插入图片描述

    原子性操作库(atomic)

    线程安全问题

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

    int x = 0;
    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		x++;
    	}
    }
    int main()
    {
    	int n = 10000;
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::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++的操作并不是原子的,他的步骤分为三步:

    • load:将共享变量n从内存加载到寄存器中。
    • update:更新寄存器里面的值,执行+1操作。
    • store:将新值从寄存器写回共享变量n的内存地址。

    对应汇编代码如下:
    在这里插入图片描述
    有可能线程1在执行刚进入函数就被切走了,并没有执行++操作,此时就切换到线程2,线程可能执行的时间长一点,执行完所有操作才切换为线程1,此时线程1会执行刚才未完成的操作,最终就会导致两个线程分别对共享变量n进行了一次++操作,但最终n的值却只被++了一次。

    我们在上面已经谈到可以用加锁解决线程安全问题,但是无论是for循环里面还是外面加锁,都多多少少会产生一些问题,for循环里面加锁就会频繁的进行加锁和解锁操作,for循环外面加锁会使两个线程变成串行操作,操作不当就会造成死锁问题。

    原子类解决线程安全问题

    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

    注意: 需要用大括号对原子类型的变量进行初始化。

    std::atomic_int x = { 0 };
    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		x++;
    	}
    }
    int main()
    {
    	int n = 10000;
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    除此之外,也可以使用atomic类模板定义出任意原子类型:

    std::atomic<int> x = 0;
    void Func(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		x++;
    	}
    }
    int main()
    {
    	int n = 10000;
    	std::thread t1(Func, n);
    	std::thread t2(Func, n);
    
    	t1.join();
    	t2.join();
    
    	std::cout << x << std::endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 原子类型通常属于“资源类型”数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等。
    • 为了防止意外,标准库已经将atomic模板类中的拷贝构造、移动构造、operator=默认删除掉了。
    • 原子类型不仅仅支持原子的++操作,还支持原子的–、加一个值、减一个值、与、或、异或操作。

    条件变量库(condition_variable)

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

    wait系列成员函数

    wait系列成员函数的作用就是让调用线程进行阻塞等待,包括wait、wait_for和wait_until。
    下面先以wait为例进行介绍,wait函数提供了两个不同版本的接口:

    //版本一
    void wait(unique_lock<mutex>& lck);
    //版本二
    template<class Predicate>
    void wait(unique_lock<mutex>& lck, Predicate pred);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    函数说明:

    • 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。
    • 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

    为什么调用wait系列函数时需要传入一个互斥锁?

    • 因为wait系列函数一般是在临界区中调用的,为了让当前线程调用wait阻塞时其他线程能够获取到锁,因此调用wait系列函数时需要传入一个互斥锁,当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
    • 因此wait系列函数实际上有两个功能,一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。

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

    • wait_for函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个时间段,表示让线程在该时间段内进行阻塞等待,如果超过这个时间段则线程被自动唤醒。
    • wait_until函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个具体的时间点,表示让线程在该时间点之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒。
    • 线程调用wait_for或wait_until函数在阻塞等待期间,其他线程调用notify系列函数也可以将其唤醒。此外,如果调用的是wait_for或wait_until函数的第二个版本的接口,那么当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么当前线程还需要继续被阻塞。

    注意: 调用wait系列函数时,传入互斥锁的类型必须是unique_lock。

    notify系列成员函数

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

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

    注意: 条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队。

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

    尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

    该题目主要考察的就是线程的同步和互斥。

    • 互斥:两个线程都在向控制台打印数据,为了保证两个线程的打印数据不会相互影响,因此需要对线程的打印过程进行加锁保护。
    • 同步:两个线程必须交替进行打印,因此需要用到条件变量让两个线程进行同步,当一个线程打印完再唤醒另一个线程进行打印。

    我们还需要考虑下面这两个问题:

    1. 我们不知道那个线程会先打印,那个线程会后打印,因为不一定我们先创建的线程就是先打印的,有可能后创建的线程也会先打印;
    2. 我们也不能保证一个线程就打印一次,不会连续打印,因为如果线程1打印完成以后解锁,此时有可能线程1和线程2同时竞争一个锁,可能线程1又竞争到这个锁,继续进行下面操作。

    所以我们在这儿就需要控制住这两个条件:

    我们假设有两个线程t1,t2,线程t1打印奇数,线程t2打印偶数,当程序第一次运行时,我们就必须保证线程t1是先运行的,会出现两种情况:

    1. t1比t2先抢到锁,此时t1就先运行,t2就阻塞,t1运用完毕以后唤醒线程t2;
    2. t2比t1先抢到锁,此时就需要将t2阻塞,然后调转过来运行t1。
      在这里插入图片描述
      上述程序虽然保证了t1比t2先运行,但是并没有解决某个线程连续多次打印的情况,所以我们在这儿还需要,而且我们还需要保证两个线程是交替进行打印的。

    我们可以利用线程t1打印奇数,t2打印偶数的条件对等待条件进行限制,如果x为奇数,就调用t1进行打印,阻塞t2,完成操作后唤醒t2,如果x为偶数,就调用t2进行打印,阻塞t1,完成操作后唤醒t1,由此就完成了两个线程是交替进行打印,并且不会出现某个线程连续多次打印的情况。

    在这里插入图片描述
    整体代码如下:

    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
    	int x = 1;
    	int n = 100;
    	std::mutex mtx;
    	std::condition_variable cv;
    
    	std::thread t1([&, n]() {
    		while (1)
    		{
    			if (x > 100)
    				break;
    
    			std::unique_lock<std::mutex> lock(mtx);  //调用构造函数加锁
    
    			if (x % 2 == 0)
    			{
    				cv.wait(lock);//如果为偶数就阻塞
    			}
    
    			std::cout << std::this_thread::get_id() << ":" << x << std::endl;
    			x++;
    
    			cv.notify_one();
    		}
    	});
    
    	std::thread t2([&, n]() {
    		while (1)
    		{
    			if (x > 100)
    				break;
    
    			std::unique_lock<std::mutex> lock(mtx);  //调用构造函数加锁
    			if (x % 2 != 0)
    			{
    				cv.wait(lock);//如果为奇数就阻塞
    			}
    
    			std::cout << std::this_thread::get_id() << ":" << x << std::endl;
    			x++;
    
    			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
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
  • 相关阅读:
    API 管理调研
    bean的配置和实例化以及生命周期
    分布式.RPC-WebService入门案例(java实现,注解实现,xsd文件解析,wsdl文件解析)
    手把手教你10分钟入门微服务开发
    [附源码]计算机毕业设计JAVA校园共享单车系统
    【算法】插入排序
    如何使用Python将PDF转为图片
    【Python笔记-设计模式】装饰器模式
    常用的国外邮箱服务有哪些?
    笔记本电脑怎样重新安装系统
  • 原文地址:https://blog.csdn.net/2303_77100822/article/details/133760954