C++11 thread,mutex,condition_variable,atomic,原子操作CAS,智能指针线程安全,单例模式最简单实现方式

1.thread

在c++11中,c++标准委员会开发出了thread库;接下来我们先来看看这个库的使用吧;

1.1 thread类接口介绍

1.1.1 thread类构造函数

我们thread库中的thread类的构造函数可以通过直接传递回调函数与函数的参数来构造线程:

cpp 复制代码
int sub(int x, int y)
{
	std::cout << x - y << std::endl;
	return x - y;
}

void add(int x, int y)
{
	std::cout << x + y << std::endl;
}
void test1()
{
	int x = 1, y = 2;
	std::thread t1(add, x, y);
	std::thread t2(sub, x, y);
	t1.join();
	t2.join();
}

现象:

相比较与linux底层的pthread_create,这里C++thread的使用更加方便,我们不需要固定线程函数的返回值,也不需要通过结构体来传递函数的多个参数,只需要通过函数的不定参数列表来传递多个不同的参数即可;

此外,线程函数删除了拷贝构造,防止了线程的拷贝,但是我们可以使用thread()空构造来创建空线程,之后再通过移动构造的方式让线程接收到线程函数:

cpp 复制代码
void testfun2()
{
	std::cout << "我是线程函数" <<std::endl;
}

void test2()
{
	std::vector<std::thread> v;
	for (int i = 0; i < 5; i++)
	{
		v.push_back(std::thread());
	}
	for (auto& it : v)
	{
		it = std::thread(testfun2);
	}
	for (auto& it : v)
	{
		it.join();
	}
}

现象:

linux下的pthread-create,是通过存储pthread_t的tid,来达到存储线程的效果,而C++这里的线程想存储起来,可以通过上面创建空线程的方式存储,最后通过移动构造来创建出带有线程函数的线程;

1.1.2 thread的各种接口

其实thread类的接口并不多,底层都是封装了linux下的线程代码(windows环境下自然就是封装windows的),我们接下来直接通过使用来加深理解:

joinable :

cpp 复制代码
void testfun3()
{
	std::cout << "我是线程函数" << std::endl;
}
void test3()
{
	std::thread t1(testfun3);
	Sleep(10);//只是让现象更明显,忽略即可
	std::cout << t1.joinable() << std::endl;
	t1.join();
	std::cout << t1.joinable() << std::endl;
}

现象:

一个线程接收到线程函数时运行起来,没有被join时joinable返回值才为true;如果一个线程被join或者detach又或者只是利用thread()默认构造没有连接线程函数,joinable返回值返回为false;总之,joinable可以用来判断一个线程可不可以被join或者detach;

detach:

cpp 复制代码
void testfun4()
{
	std::cout << "我是线程函数" << std::endl;
}
void test4()
{
	std::thread t1(testfun4);
	t1.detach();
}

在主线程不关心子进程的返回值时直接让子线程与主线程分离,主线程可以去做自己的事情,不需要阻塞的等待子线程;

get_id:

cpp 复制代码
void testfun5()
{
	std::cout << "在线程函数中获取线程id:"<<std::this_thread::get_id()<< std::endl;
}
void test5()
{
	std::thread t1(testfun5);
	std::cout <<"t1的线程id:" << t1.get_id() << std::endl;
	t1.join();
}

现象:

可以通过get_id获取C++自己维护的线程id,在线程外直接通过线程对象即可获取,在线程内需要通过this_thread作用域调用域中函数get_id才可以获取到线程id;

1.2 thread函数的参数

1.2.1 传递给thread的方法

thread函数的第一个参数一定是一个回调方法,这个方法可以是函数指针,仿函数lambda,还可以是function包装器,使用方式是各不相同的;

cpp 复制代码
void testfun6()
{
	std::cout << "我是函数指针" << std::endl;
}
class fun6 {
public:
	void operator()()
	{
		std::cout << "我是仿函数" << std::endl;
	}
};
void test6()
{
	std::thread t1(testfun6);//函数指针
	std::thread t2([]()->void {std::cout << "我是lambda方法" << std::endl; });//lambda
	fun6 f;
	std::thread t3(f);//仿函数
	t1.join();
	t2.join();
	t3.join();
}

现象:

成功的传递了三种不同的方法(函数);

1.2.1 传递的参数为引用时要带上ref()

cpp 复制代码
void testfun7(int x,int y,int & z)
{
	z = x + y;
}
void test7()
{
	int x = 1, y = 2, z=0;
	std::thread t1(testfun7,x,y,z);
	//std::thread t1(testfun7,x,y,std::ref(z));
	Sleep(10);//为了防止主线程跑太快,创建线程还没有运行
	std::cout << z << std::endl;
	t1.join();
}

当我们写出上面这样的代码时,我们想要让z成为我们希望的值 ;

但是我们编译代码是会出现这样的报错:

当出现这样的报错时,我们就会知道是我们的参数传递出现了问题,那么为什么出现了这样的错误呢?

其实这是和thread对参数传递的处理有关;在C++中thread是被封装成为了一个类的,而这个类的构造函数接收了我们传递的参数,而构造函数会通过参数的类型来推理出模板的类型,从而将参数转化为相应的类型(和我们前面的完美转发有些类型),如果我们想要传递的是一个参数的引用,我们就需要显式的声明这个参数是引用,使得这个参数保持引用的状态一直向下传递;
将z声明为ref(z)即可

cpp 复制代码
	std::thread t1(testfun7,x,y,std::ref(z));

此时再运行:

在我们的linux线程部分,似乎从来没有出现过这样的问题,这其实是C++类模板的推理所出现的问题;而在linux下的线程是通过指针转递的,所以不会出现这样的推理的问题;

介绍完线程我们接下来介绍一下与线程息息相关的mutex

2.mutex

2.1 mutex对象

mutex头文件中的mutex类是一个无法被拷贝的类,并且他的构造函数是无参的构造函数

下面是它的接口:

这些接口的使用其实和linux下线程的使用是一摸一样的,不相同的只是需要通过mutex类来调用而已;(面向对象与面向过程的区别);其中的native_handle这个接口是开放的一个底层的接口,可以通过其去设置pthread_mutex_t类型指针指向的内容,这样就可以设置锁的某些属性,这个接口我也没用过,这里只是作为介绍,也让我自己有所了解;

此外mutex的头文件中基本的锁对象还有有这些锁对象:

2.2 mutex头文件中的其他对象

2.2.1 recursive_mutex

这个锁对象一般是用在递归函数中,用来解决递归调用时使用普通锁出现的死锁问题,在递归中,我们一般需要使用recursive_mutex而不是普通的mutex锁;

2.2.2 timed_mutex

这个锁提供了一种尝试获取锁并在一定时间后自动放弃的机制。这对于需要防止死锁或者提高线程响应性的应用场景非常有用

接下来我们在来说一说对基本锁对象封装的锁:

2.2 对基本锁的封装

锁可以保证线程的安全,但在抛异常出现时,执行流的跳转会有致命的死锁出现,那么为了解决这样的问题,RAII风格的锁就出现了:

2.2.1 lock_guard

C++封装了这样的锁,可以让锁上锁后,在这个栈帧被析构时随着栈帧自动释放锁,lock_guard就是这样的封装;

lock_guard的封装很简单没有其他的接口;

2.2.2 unique_lock

unique_lock可以说是lock_guard的提升版,不仅可以手动释放锁,还增加了时间锁的功能:

上面是对锁的介绍,接下来我们介绍一下条件变量condition_variable;

3. condition_variable

这个头文件中有这两个基本类:

其中condition_variable:

condition_variable只能与std::unique_lock<std::mutex>结合使用,如果想配合其他类型的锁使用费可以使用condition_variable_any类;

下面我们使用 condition_variable类来实现,两个不同线程分别打印1到10的奇数与偶数:

cpp 复制代码
两个线程分别打印奇偶数
std::mutex mtx;
std::condition_variable cond;
int count = 1;

void printOdd()
{
	while (true)
	{
		std::unique_lock<std::mutex>lck(mtx);
		if (count > 10)
		{
			lck.unlock();
			break;
		}
		while (count % 2 == 0)cond.wait(lck);
		if(count<=10)std::cout<<std::this_thread::get_id()<<": " << count << std::endl;
		count++;
		//lck.unlock();
		cond.notify_one();
	}
}

void printEven()
{
	while (true)
	{
		std::unique_lock<std::mutex>lck(mtx);
		if (count > 10)
		{
			if (count % 2 == 1)lck.unlock();
			break;
		}
		while (count % 2 == 1)cond.wait(lck);
		if (count <= 10)std::cout << std::this_thread::get_id() << ": " << count << std::endl;
		count++;
		//lck.unlock();
		cond.notify_one();
	}
}

int main()
{
	std::thread t1(printOdd);
	std::thread t2(printEven);
	t1.join();
	t2.join();
	return 0;
}

现象:

condition_variable的使用与linux下cond条件变量的使用是一样的,只不过上层语言层对其进行了封装使得其具有了跨平台性;

4.atomic

4.1 atomic的基本使用

线程安全问题出现时,我们不仅可以使用锁来控制线程安全,还可以使用atomic原子操作库中的接口来保护线程安全;

我们用这样一个场景来举例:当我们有100个线程时,每个线程对count计数器加加一万次,如果线程安全,那么最后的结果应该是100*10000=1000000,一百万,我们用下面的代码验证;

cpp 复制代码
int count = 0;

void add()
{
	for (int i = 0; i < 10000; i++)
	{
		count++;
	}
}

void testAtomic()
{
	std::vector<std::thread> v;
	for (int i = 0; i < 100; i++)
	{
		v.push_back(std::thread());
	}
	for (auto& it : v)
	{
		it = std::thread(add);
	}
	for (auto& it : v)
	{
		it.join();
	}
	std::cout << "count :" << count << std::endl;
}

测试结果:

此时,如果按照我们平常的操作,我们会使用锁来保证线程安全,但atomic库的出现,我们可以不使用锁,我们可以这样操作:

cpp 复制代码
std::atomic<int> count = 0;

仅仅只需要将我们的count变量用atomic类来包装,就可以使得count的操作是原子的;

4.2 atomic的底层

那么为什么要有这样的原子库呢?我们的锁不是已经可以解决线程安全的问题了吗,为什么还要加一个这样的atomic库呢?我们就得看看atomic的底层CAS无锁编程了;

我们可以将对atomic变量的操作看成下面这样的操作:

上面的atomic变量++操作中与平常的++操作不相同的地方是使用CAS函数来判断count位置内存中的数据是否发生了改变,如果count位置的数据发生了改变就代表有其他线程在我们进行++操作时对count进行修改,所以我们刚刚进行的++操作是无效的需要重新进行;

在atomic库中CAS检查函数的原型是这个:

cpp 复制代码
template< class T >
bool atomic_compare_exchange_weak( std::atomic* obj,
                                   T* expected, T desired );
template< class T >
bool atomic_compare_exchange_weak( volatile std::atomic* obj,
                                   T* expected, T desired );

而实际实现我们可以看成这样:

cpp 复制代码
bool compare_and_swap (int *addr, int oldval, int newval)
{
  if ( *addr != oldval ) {
      return false;
  }
  *addr = newval;
  return true;
}

如果还需要更多的了解可以看陈皓大佬的博客网站coolshell:

无锁队列的实现 | 酷 壳 - CoolShell

4.3 atomic的优点

我们看了上面的CAS操作我们可以发现这样的保证线程安全的原子操作没有是锁的,这意味着我们的线程不需要对锁资源进行竞争的获取,性能优越,开销小;避免了锁的上下文切换,适合高并发的场景;但是atomic变量对于复杂的线程场景可能会出现问题,我们还是得使用锁来进行复杂逻辑的编程;

5.智能指针引用计数线程安全

在今天之前我们只会使用加锁的方式来保护引用计数的线程安全,不过今天之后,我们就可以使用atomic库来保护线程安全;

cpp 复制代码
class smartPtr{
public:
	smartPtr(T* ptr = nullptr)
		:_ptr(ptr), _pcount(new std::atomic<int>(1))
	{}

	template<class D>
	smartPtr(T* ptr,D del)
		:_ptr(ptr), _pcount(new std::atomic<int>(1)),_del(del)
	{}

......

private:
	T* _ptr;
	//int* _pcount;
	std::atomic<int>* _pcount;

};

我们只需要将_pcount引用计数更改为atomic类型的变量的指针即可,在构造时,将_pcount初始化为指向堆区的atomic变量的指针;这里的保护很简单,我们还需要注意智能指针本身是线程安全的,但是智能指针保护的资源并不是线程安全的;

6.单例模式最简单实现方式

cpp 复制代码
//单例模式最简单实现方式(懒汉)
class singleClass {
public:
	static singleClass& getClass()
	{
		static singleClass s;
		return s;
	}
    //删除构造和拷贝构造
	singleClass(const singleClass&) = delete;
	singleClass& operator=(const singleClass&) = delete;
private:
	singleClass()
	{
		cout << "singleClass()" << endl;
	}
};

int main()
{
	singleClass::getClass();
	singleClass::getClass();
	singleClass::getClass();
	singleClass::getClass();
	singleClass::getClass();
	return 0;
}

现象:

在C++11后我们可以这样实现单例模式,因为局部的静态变量只会在第一次初始化,保护了线程安全;但如果是在C++11之前无法保证线程是安全的;

其更本原因是:

C++11后的局部静态变量:

如果多个线程同时尝试初始化同一个局部静态变量,只有一个线程会成功初始化该变量,其他线程会被阻塞,直到初始化完成。

这一机制是通过在内部实现中使用适当的锁或原子操作来确保的。这样可以有效地避免线程间的竞争条件,确保了程序的安全性和一致性。

C++11后对局部静态变量做了处理,防止多线程竞争变量时出现的线程安全问题;

相关推荐
hummhumm15 分钟前
Oracle 第13章:事务处理
开发语言·数据库·后端·python·sql·oracle·database
@尘音18 分钟前
QT——记事本项目
开发语言·qt
童先生20 分钟前
python 用于请求chartGpt DEMO request请求方式
开发语言·python
qing_04060321 分钟前
C++——string的模拟实现(上)
开发语言·c++·string
魔道不误砍柴功22 分钟前
Java 中 String str = new String(“hello“); 里面创建了几个对象?
java·开发语言·string·new
我不会JAVA!1 小时前
排序算法(3) C++
c++·算法·排序算法
长潇若雪1 小时前
指针进阶(四)(C 语言)
c语言·开发语言·经验分享·1024程序员节
梦想科研社2 小时前
【无人机设计与控制】红嘴蓝鹊优化器RBMO求解无人机路径规划MATLAB
开发语言·matlab·无人机
混迹网络的权某2 小时前
每天一道C语言精选编程题之求数字的每⼀位之和
c语言·开发语言·考研·算法·改行学it·1024程序员节
一只特立独行的猪6113 小时前
Java面试题——微服务篇
java·开发语言·微服务