多线程(C++11)

多线程(C++)


文章目录

  • 多线程(C++)
  • 前言
  • 一、std::thread类
    • 1.线程的创建
      • 1.1构造函数
      • 1.2代码演示
    • 2.公共成员函数
      • 2.1 get_id()
      • 2.2 join()
      • 2.3 detach()
      • 2.4 joinable()
      • 2.5 operator=
    • 3.静态函数
    • 4.类的成员函数作为子线程的任务函数
  • 二、call_once函数
    • 1.原理和介绍
    • 2.应用-懒汉模式
  • 三、线程同步之互斥锁(互斥量)
    • 1.std::mutex类
      • 1.1 成员函数
      • 1.2 线程同步
    • 2.std::lock_guard类
    • 3.std::recursive_mutex类
    • 4.std::timed_mutex类
  • 四、线程同步之条件变量
    • 1.生产者-消费者模型
    • 2.条件变量
      • 2.1 成员函数
  • 总结


前言

C++11之前,C++语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多的不便。现在C++11中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。


一、std::thread类

1.线程的创建

1.1构造函数

//1. 默认构造函数,构造一个线程对象,在这个线程中不执行任何处理动作
thread() nonexcept;

//2. 移动构造函数,将other的线程所有权转移给新的thread对象,之后的other不再表示执行线程
thread(thread&& other) nonexcept;

//3. 创建线程对象,并在该线程中执行函数f的业务逻辑,args是要传递给函数f的参数(使用最多的,最有用的)
//此处使用右值引用的好处:可以延长临时变量的寿命,等到使用完成后再进行释放
template<class Function,class... Args>
explicit thread(Function&& f,Args&&... args);
/*备注:
任务函数f的类型可以是如下:
1.普通函数、类成员函数、匿名函数、仿函数(这些都是可调用对象类型)
2.可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
*/

//4.使用delet显示删除拷贝构造,不允许线程对象之间的拷贝
thread(const thread&)=delete;

1.2代码演示

#include <iostream>
#include <thread>

void func()
{
	std::cout << "children worker:" <<"xiaodu ,id:"<< std::this_thread::get_id() << std::endl;
}

void func1(std::string name,int id)
{
	std::cout << "children worker:" << name << ",id:" << std::this_thread::get_id() << std::endl;
}

int main() 
{
	std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;
	//1. 创建空的线程对象
	std::thread t1;
	//2. 创建一个可用的子线程
	std::thread t2(func);

	std::thread t3(func1, "xiaoliu", 20);

	std::thread t4([=](int id) 
		{
			std::cout << "arg id :" << id << ",id:" << std::this_thread::get_id() << std::endl;
		}, 18);

	std::thread&& t5 = std::move(t4);

	return 0;
}

报错信息
报错原因分析:程序启动之后,执行main()函数,进程里就有一个主线程(父线程),然后接下来打印主线程id,再接下来创建线程t1、t2、t3、t4、t5,但是子线程创建后就会变成就绪态,和主线程一起抢cpu时间片,谁抢到cpu时间片谁就执行对应的任务函数,主线程大概率抢到cpu时间片,主线程执行完就退出了,随之退出的还有虚拟地址空间,因此,子线程就算抢到cpu时间片但是对应的虚拟地址空间已经没了,所以报错。
报错的解决思路:让主线程等待子线程执行完任务函数再退出。

2.公共成员函数

2.1 get_id()

应用程序启动后默认只有一个线程,这个线程一般称为主线程或父线程,通过线程类创建出来的线程一般称为子线程,每个线程创建出来都对应一个线程ID,这个ID是唯一的,可以通过这个ID来区分和识别各个已经存在的线程实例,这个函数就是get_id(),函数原型如下:

// std::thread::id--长整型数
std::thread::id get_id() const noexcept;

示例程序:

#include <iostream>
#include <thread>

void func(int num,std::string str)
{
	for (int i = 0; i < 10; i++)
	{
		std::cout << "子线程:i=" << i << " num : " << num << ",str: " << str << std::endl;
	}
}

void func1()
{
	for (int i = 0; i < 10; i++)
	{
		std::cout << "子线程:i=" << i << std::endl;
	}
}



int main() 
{
	// 获得线程ID的两种方法:
	// 方法1--在子线程或者主线程函数中调用std::this_thread::get_id()函数,会得到当前线程ID
	std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;
	std::thread t(func,520,"i love you");
	std::thread t1(func1);
	// 方法2--调用thread类的成员函数get_id()
	std::cout << "线程t的线程ID:" << t.get_id()<<std::endl;
	std::cout << "线程t1的线程ID:" << t1.get_id() << std::endl;

	return 0;
}

2.2 join()

join()字面意思是连接一个线程,意味着主动地等待线程的终止(线程阻塞)。在某个线程A中通过子线程对象B调用join()函数,调用这个函数的线程A被阻塞,但是子线程对象B中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程B中的相关资源然后返回,同时,调用该函数的线程A解除阻塞继续向下执行。

// 如果要阻塞主线程的执行,只需要在主线程中通过子线程对象调用这个方法即可,
// 当调用这个方法的子线程对象中的任务函数执行完毕之后,主线程的阻塞也就随之解除了
void std::thread::join();

示例程序1:

#include <iostream>
#include <thread>

void func()
{
	std::cout << "children worker:" <<"xiaodu ,id:"<< std::this_thread::get_id() << std::endl;
}

void func1(std::string name,int id)
{
	std::cout << "children worker:" << name << ",id:" << std::this_thread::get_id() << std::endl;
}

int main() 
{
	std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;
	//1. 创建空的线程对象
	std::thread t1;
	//2. 创建一个可用的子线程
	std::thread t2(func);

	std::thread t3(func1, "xiaoliu", 20);

	std::thread t4([=](int id) 
		{
			std::cout << "arg id :" << id << ",id:" << std::this_thread::get_id() << std::endl;
		}, 18);

	std::thread&& t5 = std::move(t4);
	
	t2.join();
	t3.join();
	t5.join();		// t4线程的所有权转交给t5,t4对象失效

	return 0;
}

当程序运行到thread::join()时存在两种情况:

  • 如果任务函数func()还没执行完毕,主线程阻塞,知道任务执行完毕,主线程解除阻塞,继续向下运行
  • 如果任务函数func()已经执行完毕,主线程不会阻塞,继续向下执行

上述的bug通过join()函数成功解决。

示例程序2:
需求:程序中一共有三个线程,其中两个子线程负责分段下载同一个文件,下载完毕之后,由主线程对这个文件进行下一步处理

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void download1()
{
    // 模拟下载, 总共耗时500ms,阻塞线程500ms
    this_thread::sleep_for(chrono::milliseconds(500));
    cout << "子线程1: " << this_thread::get_id() << ", 下载1....完成" << endl;
}

void download2()
{
    // 模拟下载, 总共耗时300ms,阻塞线程300ms
    this_thread::sleep_for(chrono::milliseconds(300));
    cout << "子线程2: " << this_thread::get_id() << ", 下载2....完成" << endl;
}

void doSomething()
{
    cout << "完成...." << endl;
}

int main()
{
    thread t1(download1);
    thread t2(download2);
    // 阻塞主线程,等待所有子线程任务执行完毕再继续向下执行
    t1.join();
    t2.join();
    doSomething();
}

最核心的处理是在主线程调用doSomething();之前在第35、36行通过子线程对象调用了join()方法,这样就能够保证两个子线程的任务都执行完毕了,也就是文件内容已经全部下载完成,主线程再对文件进行后续处理,如果子线程的文件没有下载完毕,主线程就去处理文件,很显然从逻辑上讲是有问题的。

2.3 detach()

detach()函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。(唯一好处)(比如孩子(子线程)翅膀硬了,和家里(父线程)断绝关系(detach),自己外出闯荡(脱离主线程执行任务函数),死了家里也不会给买棺材(执行完毕后,自己释放系统资源),如果家里被诛九族(主线程退出)还是会受到牵连(子线程还是会退出))。
示例程序:

#include <iostream>
#include <thread>

void func()
{
	std::cout << "children worker:" << "xiaodu ,id:" << std::this_thread::get_id() << std::endl;
}

void func1(std::string name, int id)
{
	std::cout << "children worker:" << name << ",id:" << std::this_thread::get_id() << std::endl;
}

int main()
{
	std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;
	//1. 创建空的线程对象
	std::thread t1;
	//2. 创建一个可用的子线程
	std::thread t2(func);

	std::thread t3(func1, "xiaoliu", 20);

	std::thread t4([=](int id)
		{
			std::cout << "arg id :" << id << ",id:" << std::this_thread::get_id() << std::endl;
		}, 18);

	std::thread&& t5 = std::move(t4);

	t2.join();
	t3.join();
	// 子线程t5在主线程中调用detach()函数,子线程和主线程分离,
	//从此再也不能在主线程中子线程调用成员函数进行任何有效操作,否则会报错
	t5.detach();
	// 不能得到真正的线程ID
	t5.get_id();
	//  失效,报错
	//t5.join();		

	return 0;
}

注意事项:线程分离函数detach()不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了,比如:通过join()阻塞主线程等待子线程中的任务执行完毕,或者调用get_id()获取子线程的线程ID。有利就有弊,鱼和熊掌不可兼得,建议使用join(),一般不用detach()。

2.4 joinable()

joinable()函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

  • 返回值为true:主线程和子线程之间有关联(连接)关系
  • 返回值为false:主线程和子线程之间没有关联(连接)关系
bool joinable() const noexcept;

示例程序:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void foo()
{
	std::this_thread::sleep_for(chrono::seconds(1));
}

int main()
{
	// 创建一个空的线程对象,如果子线程未关联任务函数,则二者也是未关联
	thread t;
	cout << "before starting,joinable:" << t.joinable() << endl;	//false

	// 匿名对象作为临时变量,使用右值引用拷贝构造函数
	t = thread(foo);
	cout << "after starting,joinable:" << t.joinable() << endl;		//true

	// 线程t调用jion函数,阻塞主线程,执行任务函数完成后,退出子线程
	t.join();
	cout << "after starting,joinable:" << t.joinable() << endl;		//false

	// 创建线程t1 
	thread t1(foo);
	cout << "after starting,joinable:" << t1.joinable() << endl;	//true
	// 线程分离
	t1.detach();
	cout << "after starting,joinable:" << t1.joinable() << endl;		//false

	return 0;
}

2.5 operator=

线程中的资源是不能被复制的,因此通过=操作符进行赋值操作最终并不会得到两个完全的对象。

// move(1)
thread& operator=(thread&& other) noexcept;
// copy[deleted](2)
thread& operator=(thread& other) noexcept;

通过以上=操作符的重载声明可以得知:

  • 如果other是一个右值,会将资源所有权进行转移
  • 如果other不是一个右值,禁止拷贝,该函数显示被删除(delete),不可用

3.静态函数

thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的(并行)。

static unsigned hardware_concurrency() noexcept;

示例代码:

#include <iostream>
#include <thread>
using namespace std;


int main()
{
	unsigned int num = thread::hardware_concurrency();
	cout << "hardware_concurrency = " << num << endl;
	return 0;
}

4.类的成员函数作为子线程的任务函数

待学TODO

二、call_once函数

1.原理和介绍

在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候,需要一个once_flag作为call_once()的传入参数,该函数的原型如下:

// 定义位于<mutex>
#include <mutex>
void call_once(std::once_flag& flag,Callable&& f,Args&&... args)

  • flag:once_flag类型的对象,要保证这个对象能够被多个线程同时访问到
  • f:回调函数,可以传递一个有名函数地址,也可以指定一个匿名函数
  • args:作为实参传递给回调函数
    多线程操作过程中,std::call_once()内部的回调函数只会被执行一次,示例代码如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 全局变量
once_flag flag;

void do_once()
{
	cout << "do once function" << endl;
}

void do_something()
{
	static int num = 1;
	// call_once注意事项:flag要能被多个线程同时访问;传入实参参数的形式类似thread函数;无返回值,void
	// 使用场景:在多线程中,只需要初始化一次对象(new 一个对象)
	call_once(flag, do_once);
	cout << "do something function: " << num++ << endl;
}

int main()
{
	thread t1(do_something);
	thread t2(do_something);
	thread t3(do_something);
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

call_once

2.应用-懒汉模式

单例模式分类:

  • 懒汉式
    系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。这种方式要考虑线程安全
  • 饿汉式
    系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就线程安全,没有多线程的线程安全问题。

编写一个单例模式(分为懒汉式和饿汉式)的类–懒汉模式。
示例程序:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 单例模式--懒汉模式
// 全局变量,保证被多个线程访问
once_flag g_flag;

// 单例类
class Base
{
public:
	// 实现单例模式首先将拷贝构造和赋值操作显示删除
	Base(const Base& obj) = delete;
	Base& operator=(const Base& obj) = delete;

	// 静态成员函数
	static Base* getInstance() 
	{
		// 调用call_once函数保证多个线程访问时只有一个对象创建
		call_once(g_flag, [&]() {
			ptr = new Base;
			cout << "单例创建" << endl;
			});
		return ptr;
	}

	void release()
	{
		delete ptr;
		ptr = nullptr;
	}
	string getName()
	{
		return name;
	}

	void setName(string Name)
	{
		this->name = Name;
	}

private:
	// 构造函数设为private,不能被外界直接访问
	Base() { }
	~Base() { }
	static Base* ptr;
	string name;
};
// 懒汉模式是运行时才会创建对象,所以ptr先设置为nullptr
Base* Base::ptr = nullptr;

void func(string name)
{
	Base::getInstance()->setName(name);
	std::cout<<"name:"<< Base::getInstance()->getName()<<endl;
}


int main()
{
	thread t1(func,"路飞");
	thread t2(func,"埃斯");
	thread t3(func,"萨博");
	t1.join();
	t2.join();
	t3.join();
	Base::getInstance()->release();
	return 0;
}

懒汉模式
此处,很大概率会出现打印信息不对,因为三个线程同时对一块内存进行了读写操作,所以为了让多线程线性访问,需要加锁。
加锁后的代码程序:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 单例模式--懒汉模式
// 全局变量,保证被多个线程访问
once_flag g_flag;

// 单例类
class Base
{
public:
	// 实现单例模式首先将拷贝构造和赋值操作显示删除
	Base(const Base& obj) = delete;
	Base& operator=(const Base& obj) = delete;

	// 静态成员函数
	static Base* getInstance() 
	{
		// 调用call_once函数保证多个线程访问时只有一个对象创建
		call_once(g_flag, [&]() {
			ptr = new Base;
			cout << "单例创建" << endl;
			});
		return ptr;
	}

	void release()
	{
		delete ptr;
		ptr = nullptr;
	}
	string getName()
	{
		return name;
	}

	void setName(string Name)
	{
		
		this->name = Name;
	}

private:
	// 构造函数设为private,不能被外界直接访问
	Base() { }
	~Base() { }
	static Base* ptr;
	string name;
};
// 懒汉模式是运行时才会创建对象,所以ptr先设置为nullptr
Base* Base::ptr = nullptr;

mutex mx;
void func(string name)
{
	// 多个线程的共享资源是Base::getInstance()
	mx.lock();
	Base::getInstance()->setName(name);
	std::cout<<"name:"<< Base::getInstance()->getName()<<endl;
	mx.unlock();
}


int main()
{
	thread t1(func,"路飞");
	thread t2(func,"埃斯");
	thread t3(func,"萨博");
	t1.join();
	t2.join();
	t3.join();
	Base::getInstance()->release();
	return 0;
}

三、线程同步之互斥锁(互斥量)

进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。这是因为线程在运行的时候需要先得到CPU时间片,时间片用完之后需要放弃已获得的CPU资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的。
多线程访问
注意概念:线程同步是指让多线程进行线性有序的进行,而不是同时对一块内存操作。
解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:

  • std::mutex:独占的互斥锁,不能递归使用 (最常用)
  • std::timed_mutex:带超时的独占互斥锁,不能递归使用
  • std::recursive_mutex:递归互斥锁,不带超时功能
  • std::recursive_timed_mutex:带超时的递归互斥锁
    注意事项:
  • 内存可能涉及到多个线程的同时访问(写、读写),此时需要通过互斥锁对内存进行保护
  • 使用互斥锁锁住的是和共享数据相关的一个代码块(临界区)
  • 在程序中一个共享数据对应多个代码块,在锁住这些代码块的时候要用同一把锁,并且程序运行期间不能销毁这把互斥锁

1.std::mutex类

1.1 成员函数

lock()函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞其他线程的作用,函数原型如下:

// 
void lock();

独占互斥锁对象有两种状态:锁定和未锁定。如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权,就会被lock()函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。

除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁,函数原型如下:

bool try_lock();

二者的区别在于try_lock()不会阻塞线程,lock()会阻塞线程:

  • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
  • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false

**当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。**该函数的函数原型如下:

void unlock();

通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:

  • 找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
  • 找到和共享资源有关的上下文代码,也就是临界区(下图中的黄色代码部分)
  • 在临界区的上边调用互斥锁类的lock()方法
  • 在临界区的下边调用互斥锁的unlock()方法
    线程同步
    线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

注意:
当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

1.2 线程同步

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 互斥锁
class Base
{
public:
	// count表示打印的个数
	void increment(int count) 
	{
		for (int i = 0; i < count; i++)
		{
			mx.lock();
			++num;
			cout << "++current number: " << num << endl;
			mx.unlock();
			this_thread::sleep_for(chrono::milliseconds(100));
		}
	}
	
	// count表示打印的个数
	void decrement(int count)
	{
		for (int i = 0; i < count; i++)
		{
			mx.lock();
			--num;
			cout << "--current number: " << num << endl;
			mx.unlock();
			this_thread::yield();
		}
	}

private:
	// 此处为共享资源
	int num=999;
	mutex mx;
};



int main()
{
	Base b;
	thread t1(&Base::increment,&b,10);
	thread t2(&Base::decrement, &b, 10);
	t1.join();
	t2.join();
	return 0;
}

结果

2.std::lock_guard类

lock_guard是C++11新增的一个模板类,使用这个类,可以简化互斥锁lock()和unlock()的写法,同时也更安全。这个模板类的定义和常用的构造函数原型如下:

// 类的定义,定义于头文件 <mutex>
template< class Mutex >
class lock_guard;

// 常用构造函数
explicit lock_guard( mutex_type& m );

lock_guard在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
使用lock_guard对上面的例子进行修改,代码如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 互斥锁
class Base
{
public:
	// count表示打印的个数
	void increment(int count) 
	{
		for (int i = 0; i < count; i++)
		{
			{
			lock_guard<mutex> lock(mx);
			++num;
			cout << "++current number: " << num << endl;
			}
			this_thread::sleep_for(chrono::milliseconds(100));
		}
	}
	
	// count表示打印的个数
	void decrement(int count)
	{
		for (int i = 0; i < count; i++)
		{
			{
			lock_guard<mutex> lock(mx);
			--num;
			cout << "--current number: " << num << endl;
			}
			this_thread::yield();
		}
	}

private:
	// 此处为共享资源
	int num=999;
	mutex mx;
};



int main()
{
	Base b;
	thread t1(&Base::increment,&b,10);
	thread t2(&Base::decrement, &b, 10);
	t1.join();
	t2.join();
	return 0;
}

lock_guard通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,需要根据实际情况选择最优的解决方案。

3.std::recursive_mutex类

递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题,在下面的例子中使用独占非递归互斥量会发生死锁:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 互斥锁
class Base
{
public:
	// count表示打印的个数
	void increment(int count)
	{
		for (int i = 0; i < count; i++)
		{
			mx.lock();
			++num;
			cout << "++current number: " << num << endl;
			mx.unlock();
			this_thread::sleep_for(chrono::milliseconds(100));
		}
	}

	// count表示打印的个数
	void decrement(int count)
	{
		for (int i = 0; i < count; i++)
		{
			{
				lock_guard<mutex> lock(mx);
				// 发生死锁
				increment(2);
				--num;
				cout << "--current number: " << num << endl;
			}
			this_thread::yield();
		}
	}

private:
	// 此处为共享资源
	int num = 999;
	mutex mx;	//递归互斥锁
};



int main()
{
	Base b;
	thread t1(&Base::increment, &b, 10);
	thread t2(&Base::decrement, &b, 10);
	t1.join();
	t2.join();
	return 0;
}

错误信息
上面的程序中执行了decrement( )中对incremen( )调用之后,程序就会发生死锁,在decrement( )中已经对互斥锁加锁了,继续调用incremen( )函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在C++中程序会异常退出,使用C库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。要解决这个死锁的问题,一个简单的办法就是使用递归互斥锁std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。修改之后的代码如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 互斥锁
class Base
{
public:
	// count表示打印的个数
	void increment(int count)
	{
		for (int i = 0; i < count; i++)
		{
			mx.lock();
			++num;
			cout << "++current number: " << num << endl;
			mx.unlock();
			this_thread::sleep_for(chrono::milliseconds(100));
		}
	}

	// count表示打印的个数
	void decrement(int count)
	{
		for (int i = 0; i < count; i++)
		{
			{
				lock_guard<recursive_mutex> lock(mx);
				increment(2);
				--num;
				cout << "--current number: " << num << endl;
			}
			this_thread::yield();
		}
	}

private:
	// 此处为共享资源
	int num = 999;
	recursive_mutex mx;	//递归互斥锁
};



int main()
{
	Base b;
	thread t1(&Base::increment, &b, 10);
	thread t2(&Base::decrement, &b, 10);
	t1.join();
	t2.join();
	return 0;
}

结果
虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:

  • 使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
  • 递归互斥锁比非递归互斥锁效率要低一些。
  • 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。

4.std::timed_mutex类

std::timed_mutex是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。
std::timed_mutex比std::_mutex多了两个成员函数:try_lock_for()和try_lock_until():

void lock();
bool try_lock();
void unlock();

// std::timed_mutex比std::_mutex多出的两个成员函数
template <class Rep, class Period>
  bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);

template <class Clock, class Duration>
  bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
  • try_lock_for函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
  • try_lock_until函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
  • 关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
    下面的示例程序中为大家演示了std::timed_mutex的使用:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 互斥锁
class Base
{
public:
	void work()
	{
		while (true) {
			// 锁未锁定--返回值为true
			if (mx.try_lock_for(chrono::milliseconds(100)))
			{
				cout << "count"<<++count<<"  ok 线程ID=" << this_thread::get_id() << endl;
				// 模拟任务使用的时长
				this_thread::sleep_for(chrono::milliseconds(500));
				cout << "count" << count << "  ok 线程ID=" << this_thread::get_id()<<endl;
				// 互斥锁解锁
				mx.unlock();
				break;
			}
			else // 阻塞时间超时--返回false
			{
				// 模拟任务使用的时长
				this_thread::sleep_for(chrono::milliseconds(50));
				cout << "not ok 线程ID=" << this_thread::get_id()<<endl;
			}
		}
	}

private:
	int count = 0;	// 共享资源
	timed_mutex mx;	// 超时独占互斥锁
};



int main()
{
	Base b;
	thread t1(&Base::work, &b);
	thread t2(&Base::work, &b);
	t1.join();
	t2.join();
	return 0;
}

result
**注意:解除阻塞和解锁是两个不同的概念。**从上述的结果,我们可以看出线程38332获得互斥锁的所有权,try_lock_for函数超过阻塞时长,返回false,阻塞的另一个线程1864执行else中的任务,由于这个是while(true)该线程1864重新被阻塞,超过阻塞时长,又重新执行else,直到线程38332解除互斥锁的所有权,线程1864获得互斥锁的所有权,执行接下来的过程。

四、线程同步之条件变量

1.生产者-消费者模型

条件变量通常用于生产者和消费者模型。生产者和消费者模型主要应用的场景是生产者和消费者的速度不匹配。 条件变量能够帮助我们解决某一类线程(生产者或者消费者),互斥锁能够帮助我们解决某一类线程因为多个线程访问共享资源导致的数据混乱问题。
生产者和消费者模型的组成:

  1. 生产者线程->若干个
    生产商品或者任务放入到任务队列中;
    任务队列满了生产者线程就阻塞,不满就工作;
    通过一个生产者的条件变量控制生产者线程阻塞和非阻塞。
  2. 消费者线程->若干个
    读任务队列,将任务或者数据取出;
    任务队列中有数据就有消费者,没有数据就阻塞;
    通过一个消费者的条件变量控制消费者线程阻塞和非阻塞。
  3. 任务队列->存储数据或者任务,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存
    数据结构的类型可以分为数组、链表,也可以是STL容器(queue\stack\list\vector)
    注意:推荐使用STL中的容器,方便增加删除以及动态扩展等。
    生产者消费者模型
    生产者消费者模型任务队列类的代码演示:
#include <queue>
#include <iostream>
#include <thread>
using namespace std;

class WorkQueue
{
public:
	// 添加数据
	void put(const int& task)
	{
		workQueue.push(task);			// 尾部添加数据
		cout << "添加任务:" << task << ",线程ID:" << this_thread::get_id() << endl;
	}

	// 删除数据
	void take()
	{
		int node = workQueue.front();	// 取出头部数据
		workQueue.pop();
		cout << "删除任务:" << node << ",线程ID:" << this_thread::get_id() << endl;
	}

	// 判断任务队列是否满
	bool isFull()
	{
		return workQueue.size() == maxSize;
	}

	// 判断任务队列是否空
	bool isEmpty()
	{
		return workQueue.size() == 0;
	}

	// 当前任务队列的尺寸
	int workSize()
	{
		return workQueue.size();
	}

private:
	int maxSize;	//任务队列的容量
	std::queue<int> workQueue;	// 任务队列的数据结构
};

优点:线程阻塞相比较线程空循环或者线程的重新创建来说效率更高,资源消耗更少。

2.条件变量

condition_variable:需要配合std::unique_lockstd::mutex进行wait操作,也就是阻塞线程的操作。
condition_variable_any:可以和任意带有lock()、unlock()语义的mutex搭配使用,也就是说有四种:

  • std::mutex:独占的非递归互斥锁
  • std::timed_mutex:带超时的独占非递归互斥锁
  • std::recursive_mutex:不带超时功能的递归互斥锁
  • std::recursive_timed_mutex:带超时的递归互斥锁

条件变量通常用于生产者和消费者模型,大致使用过程如下:

  • 拥有条件变量的线程获取互斥量
  • 循环检查某个条件,如果条件不满足阻塞当前线程,否则线程继续向下执行
    产品的数量达到上限,生产者阻塞,否则生产者一直生产。。。
    产品的数量为零,消费者阻塞,否则消费者一直消费。。。
  • 条件满足之后,可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程
    由消费者唤醒被阻塞的生产者,生产者解除阻塞继续生产。。。
    由生产者唤醒被阻塞的消费者,消费者解除阻塞继续消费。。。

2.1 成员函数

condition_variable的成员函数主要分为两部分:线程等待(阻塞)函数 和线程通知(唤醒)函数,这些函数被定义于头文件 <condition_variable>

  • 等待函数
    调用wait()函数的线程会被阻塞
// ①	调用该函数的线程直接被阻塞
void wait (unique_lock<mutex>& lck);
// ②	该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数
// 		该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
//		表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

独占的互斥锁对象不能直接传递给wait()函数,需要通过模板类unique_lock进行二次处理,通过得到的对象仍然可以对独占的互斥锁对象做(lock、try_lock、try_lock_for、try_lock_until、unlock)操作,使用起来更灵活。
如果线程被该函数阻塞,这个线程会释放占有的互斥锁的所有权,当阻塞解除之后这个线程会重新得到互斥锁的所有权,继续向下执行。(wait函数内部实现,其目的是为了避免线程的死锁)
wait_for()函数和wait()的功能是一样的,只不过多了一个阻塞时长,假设阻塞的线程没有被其他线程唤醒,当阻塞时长用完之后,线程就会自动解除阻塞,继续向下执行。

template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
                    const chrono::duration<Rep,Period>& rel_time);
	
template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lck,
               const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_until()函数和wait_for()的功能是一样的,它是指定让线程阻塞到某一个时间点,假设阻塞的线程没有被其他线程唤醒,当到达指定的时间点之后,线程就会自动解除阻塞,继续向下执行。

template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,
                      const chrono::time_point<Clock,Duration>& abs_time);

template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,
                 const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);
  • 通知函数
void notify_one() noexcept;
void notify_all() noexcept;

notify_one():唤醒一个被当前条件变量阻塞的线程
notify_all():唤醒全部被当前条件变量阻塞的线程

生产者线程:当任务队列满的时候,阻塞生产者线程;消费者线程:当任务队列空的时候,阻塞消费者线程。因此,需要两个条件变量控制生产者线程和消费者线程阻塞。

#include <queue>
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

class WorkQueue
{
public:
	// 添加数据
	void put(const int& task)
	{
		unique_lock<mutex> locker(mx);
		// 如果任务队列满了,就需要阻塞生产者线程
		while(workQueue.size()==maxSize)
		{
			// unique_lock对象类似lock_guard,管理mutex的加锁和解锁,当线程访问到这个对象时,未加锁则加锁,反之阻塞
			// 如果locker析构了,自动对mx管理的线程所有权进行解锁
			// 假设现在有线程A,B,C,线程A获得了互斥锁资源,则线程B,C进行阻塞
			notFull.wait(locker);	// 线程A在运行到wait的位置会先释放拥有的互斥锁资源(解锁),此时,线程B,C从阻塞态变成就绪态,线程A再阻塞,所以不会出现死锁
			// 等到线程A从wait位置解除阻塞,会跟其他线程再去抢互斥锁资源
		}
		workQueue.push(task);			// 尾部添加数据
		cout << "添加任务:" << task << ",线程ID:" << this_thread::get_id() << endl;

		// 唤醒消费者
		notEmpty.notify_one();
	}

	// 删除数据
	void take()
	{
		unique_lock<mutex> locker(mx);
		// 如果任务队列空了,就需要阻塞消费者线程
		while(workQueue.size()==0)
		{
			notEmpty.wait(locker);
		}
		int node = workQueue.front();	// 取出头部数据
		workQueue.pop();
		cout << "删除任务:" << node << ",线程ID:" << this_thread::get_id() << endl;

		// 唤醒生产者
		notFull.notify_one();
	}

	// 判断任务队列是否满
	bool isFull()
	{
		// 任务队列的访问所以加锁
		std::lock_guard<std::mutex> locker(mx);
		return workQueue.size() == maxSize;
	}

	// 判断任务队列是否空
	bool isEmpty()
	{
		std::lock_guard<std::mutex> locker(mx);
		return workQueue.size() == 0;
	}

	// 当前任务队列的尺寸
	int workSize()
	{
		std::lock_guard<std::mutex> locker(mx);
		return workQueue.size();
	}

private:
	int maxSize=100;	//任务队列的容量
	std::queue<int> workQueue;	// 任务队列的数据结构
	mutex mx;						// 独占的互斥锁
	condition_variable notFull;		// 控制生产者线程
	condition_variable notEmpty;	// 控制消费者线程
};

int main()
{
	thread t1[5];
	thread t2[5];
	WorkQueue workQ;

	for (int i = 0; i < 5; i++)
	{
		t1[i] = thread(&WorkQueue::put, &workQ, 100 + i);
		t2[i] = thread(&WorkQueue::take, &workQ);
	}

	for (int i = 0; i < 5; i++)
	{
		t1[i].join();
		t2[i].join();
	}

	return 0;
}

在这里插入图片描述
条件变量condition_variable类的wait()还有一个重载的方法,可以接受一个条件,这个条件也可以是一个返回值为布尔类型的函数,条件变量会先检查判断这个条件是否满足,如果满足条件(布尔值为true),则当前线程重新获得互斥锁的所有权,结束阻塞,继续向下执行;如果不满足条件(布尔值为false),当前线程会释放互斥锁(解锁)同时被阻塞,等待被唤醒。
优化后的代码:

#include <queue>
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

class WorkQueue
{
public:
	// 添加数据
	void put(const int& task)
	{
		unique_lock<mutex> locker(mx);
		notFull.wait(locker, [&]() {
			return !(workQueue.size() == maxSize);
			});

		workQueue.push(task);			// 尾部添加数据
		cout << "添加任务:" << task << ",线程ID:" << this_thread::get_id() << endl;

		// 唤醒消费者
		notEmpty.notify_one();
	}

	// 删除数据
	void take()
	{
		unique_lock<mutex> locker(mx);
		notEmpty.wait(locker, [&]() {
			return !workQueue.size() == 0;
			});
		int node = workQueue.front();	// 取出头部数据
		workQueue.pop();
		cout << "删除任务:" << node << ",线程ID:" << this_thread::get_id() << endl;

		// 唤醒生产者
		notFull.notify_one();
	}

	// 判断任务队列是否满
	bool isFull()
	{
		// 任务队列的访问所以加锁
		std::lock_guard<std::mutex> locker(mx);
		return workQueue.size() == maxSize;
	}

	// 判断任务队列是否空
	bool isEmpty()
	{
		std::lock_guard<std::mutex> locker(mx);
		return workQueue.size() == 0;
	}

	// 当前任务队列的尺寸
	int workSize()
	{
		std::lock_guard<std::mutex> locker(mx);
		return workQueue.size();
	}

private:
	int maxSize=100;	//任务队列的容量
	std::queue<int> workQueue;	// 任务队列的数据结构
	mutex mx;						// 独占的互斥锁
	condition_variable notFull;		// 控制生产者线程
	condition_variable notEmpty;	// 控制消费者线程
};

int main()
{
	thread t1[50];
	thread t2[50];
	WorkQueue workQ;

	for (int i = 0; i < 50; i++)
	{
		t2[i] = thread(&WorkQueue::take, &workQ);
		t1[i] = thread(&WorkQueue::put, &workQ, 100 + i);
		
	}

	for (int i = 0; i < 50; i++)
	{
		t1[i].join();
		t2[i].join();
	}

	return 0;
}

总结

本文详细介绍了C++11多线程中thread线程类、mutex等互斥锁、以及线程同步和生产者消费者模型,并提供了对应的示例,本文主要是参考爱编程的大丙,欢迎大家阅读大丙老师的文章,本文作个人学习之用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/641461.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AOP编程

AOP编程 AOP&#xff0c;面向切面编程&#xff0c;一种编程范式&#xff0c;指导开发者如何组织程序结构。 OOP&#xff0c;面向对象编程&#xff0c;一种编程思想。 AOP&#xff0c;提供了一种机制,可以将一些横切系统中多个模块的共同逻辑(如日志记录、事务管理、安全控制等…

SQL面试题练习 —— 波峰波谷

来源&#xff1a;字节今日头条 目录 1 题目2 建表语句3 题解 1 题目 有如下数据&#xff0c;记录每天每只股票的收盘价格&#xff0c;请查出每只股票的波峰和波谷的日期和价格&#xff1b; 波峰定义&#xff1a;股票价格高于前一天和后一天价格时为波峰 波谷定义&#xff1a;股…

MoE 系列论文解读:Gshard、FastMoE、Tutel、MegaBlocks 等

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备面试攻略、面试常考点等热门话题进行了深入的讨论。 总结链接…

Unity在Windows平台播放HEVC/H.265格式视频的底层原理

相关术语、概念 HEVC/H.265 HEVC&#xff08;High Efficiency Video Coding&#xff09;是一种视频压缩标准&#xff0c;也被称为H.265。它是一种高效的视频编码标准&#xff0c;可以提供比之前的标准&#xff08;如H.264&#xff09;更高的压缩率&#xff0c;同时保持较高的…

力扣HOT100 - 31. 下一个排列

解题思路&#xff1a; 数字是逐步增大的 步骤如下&#xff1a; class Solution {public void nextPermutation(int[] nums) {int i nums.length - 2;while (i > 0 && nums[i] > nums[i 1]) i--;if (i > 0) {int j nums.length - 1;while (j > 0 &&…

015_表驱动编程思想(c实现)

【背景】 数据压倒一切。如果选择了正确的数据结构并把一切组织的井井有条&#xff0c;正确的算法就不言自明。编程的核心是数据结构&#xff0c;而不是算法。 ——Rob Pike 上面是这个名人说过的话&#xff0c;那么c语言之父 丹尼斯麦卡利斯泰尔里奇 的《c程序设计》里曾经…

【Linux取经路】基于信号量和环形队列的生产消费者模型

文章目录 一、POSIX 信号量二、POSIX 信号量的接口2.1 sem_init——初始化信号量2.2 sem_destroy——销毁信号量2.3 sem_wait——等待信号量2.4 sem_post——发布信号量 三、基于环形队列的生产消费者模型3.1 单生产单消费模型3.2 多生产多消费模型3.3 基于任务的多生产多消费模…

C# 利用Xejen框架源码,我们来开发一个基于Dapper技术的数据库通用的帮助访问类,通过Dapper的增删改查,可以访问Sqlite数据库

Dapper 是一个轻量级的对象关系映射&#xff08;ORM&#xff09;工具&#xff0c;适用于 .NET 平台。它由 Stack Overflow 团队开发&#xff0c;旨在提供简单、高效的数据访问功能。与其他重量级 ORM&#xff08;如 Entity Framework&#xff09;相比&#xff0c;Dapper 更加轻…

用这8种方法在海外媒体推广发稿平台上获得突破-华媒舍

在今天的数字时代&#xff0c;海外媒体推广发稿平台已经成为了许多机构和个人宣传和推广的有效途径。如何在这些平台上获得突破并吸引更多的关注是一个关键问题。本文将介绍8种方法&#xff0c;帮助您在海外媒体推广发稿平台上实现突破。 1. 确定目标受众 在开始使用海外媒体推…

C++语法|虚函数与多态详细讲解(六)|如何解释多态?(面试向)

系列汇总讲解&#xff0c;请移步&#xff1a; C语法&#xff5c;虚函数与多态详细讲解系列&#xff08;包含多重继承内容&#xff09; 多态分为了两种&#xff0c;一种是静态的多态&#xff0c;一种是动态的多态。 静态&#xff08;编译时期&#xff09;的多态 函数重载 boo…

pands使用openpyxl引擎实现EXCEL条件格式

通过python的openpyxl库&#xff0c;实现公式条件格式。 实现内容&#xff1a;D列单元格不等于E列同行单元格时标红。 #重点是formula后面的公式不需要“”号。 from openpyxl.styles import Color, PatternFill, Font, Border from openpyxl.styles.differential import Dif…

【设计模式】JAVA Design Patterns——Bytecode(字节码模式)

&#x1f50d;目的 允许编码行为作为虚拟机的指令 &#x1f50d;解释 真实世界例子 一个团队正在开发一款新的巫师对战游戏。巫师的行为需要经过精心的调整和上百次的游玩测试。每次当游戏设计师想改变巫师行为时都让程序员去修改代码这是不妥的&#xff0c;所以巫师行为以数据…

git push后一直卡在在Writing objects:问题

git push后一直卡在Writing objects: 解决&#xff1a;设置 git config --global http.postBuffer 5242880000在执行git push。 一般设置后就可以成功了&#xff0c;后面不用看。 2. 我这里结果又报错&#xff1a; fatal: protocol error: bad line length 8192 MiB | 107.46 …

【C++】d1

关键字&#xff1a; 运行、前缀、输入输出、换行 运行f10 前缀必须项&#xff1a; #include <iostream> using namespace std; 输入/输出&#xff1a; cin >> 输入 cout << 输出 语句通过>>或<<分开 换行 endl或者"\n"

想当安卓开发工程师?学习路线分享!

安卓开发学习路线 在前几篇文章中,对安卓开发岗位的岗位要求做了一些科普,本节文章将介绍安卓开发岗位的学习路线。 目前,网络上有很多面经、算法题解、算法课等学习资料,如何合理利用这些资料成为技术求职者的一大困惑。笔者整理了一份安卓开发岗位学习路线供大家参考,…

第四课 communcation服务-can配置第二弹

Davinci配置目标: 介绍DBC基本属性,并且配置出一个DBC。 将DBC导入到vector的davinci工具,生成我们想要的代码。 Davinci配置步骤: 1. 编辑DBC文件 DBC文件是一种非常重要的工具,所谓DBC就是Database CAN,CAN网络的数据库文件,定义了CAN网络的节点、消息、信号的所有…

网络安全知识核心20要点

1、什么是SQL注入攻击 概述 攻击者在 HTTP 请求中注入恶意的 SQL 代码&#xff0c;服务器使用参数构建数据库 SQL 命令时&#xff0c;恶意SQL 被一起构造&#xff0c;并在数据库中执行。 注入方法 用户登录&#xff0c;输入用户名 lianggzone&#xff0c;密码‘ or ‘1’’…

axios如何传递数组作为参数,后端又如何接收呢????

前端的参数是一个数组。 前端编写&#xff1a; 后端接收&#xff1a;

git解决版本冲突 -git pull

当在Git中遇到版本冲突时&#xff0c;通常是因为两个或更多的开发者在同一时间修改了同一个文件的相同部分&#xff0c;并将这些修改推送到远程仓库。Git无法自动合并这些修改&#xff0c;因此会产生冲突。以下是解决Git版本冲突的基本步骤&#xff1a; 拉取最新代码&#xff…