多线程请勿使用双重检验锁保护数据初始化

在前些年,双重检验锁据说被广泛的运用。其形式如下:

#include <iostream>
#include <mutex>
using namespace std;
class SomeObject
{
public:
	void Hello()
	{
		cout << "Hello" << endl;
	}
};
class BadAtempt
{
	SomeObject *someDetail;
	mutex m;
public:
	void init()
	{
		if (!someDetail)
		{
			lock_guard<mutex> lk(m);
			if (!someDetail)
			{
				someDetail = new SomeObject;
			}
		}
		someDetail->Hello();
	}
};

首先要明确的是为什么要用双重检验锁:为了保护数据的初始化过程,光这么说你或许还不能了解其真正内涵,那咱们首先这么想,先不管多线程,在单进程下创建一个类的实例,BadAtempt a;就行了,我们要明确的是这个实例中有个类型为someObject的成员变量没有初始化。为什么要用指针,就是为了延迟初始化,因为或许你一辈子都不会在这个实例中使用这个成员变量,那又何必初始化它占用多余的空间。使用指针初始化,就需要考虑多线程所带来的问题了。
多线程是并发执行的,在同一时间对同一个实例,可能有多个线程同时初始化这个someObject类型的成员变量,因为在它们看来,这个变量都还未初始化。

	void init()
	{
		if (!someDetail)//两个线程同时通过了这个判断
		{
			someDetail = new SomeObject;//两个线程同时new,Exception
		}
		someDetail->Hello();
	}

这当然就会导致异常了,那你说好,那我加个锁不就行了。

void init()
	{
		lock_guard<mutex> lk(m);//锁住,仅允许一个线程执行接下来的操作
		if (!someDetail)
		{
			someDetail = new SomeObject;
		}
		someDetail->Hello();
	}//释放锁,允许下个阻塞的线程上锁继续。

这确实行,但你不觉得慢么,会相当慢,因为这种情况下如果有100个线程同时初始化,第一个线程上锁,其他99个就得等。第一个结束了,第二个上锁,其他98个等……这叫做线程的序列化,多线程硬生生变成了好像单进程一样顺序执行。
所以应运而生了双重检验锁

void init()
	{
		if (!someDetail)//如果有两个以上的进程通过了这个判断
		{
			lock_guard<mutex> lk(m);//没有关系,在这锁住
			if (!someDetail)//再判断
			{
				someDetail = new SomeObject;//仅有一个线程能初始化。
			}
		}//ok,释放锁,其他所有线程都不会再序列化的被阻塞。
		someDetail->Hello();
	}

看我的注释,你会感觉这种方式,哎哟不错哦。但它其实内涵一个恶劣的race condition。你要注意一个关键点,这个关键点也是避免序列化的关键:第一个判断没有上锁,你再注意第二个关键点:判断如果是false,接下来的语句是someDetail指针调用Hello函数。仔细想想,在多线程并发情况下,如果一个线程已经进行到了给指针赋予指向的对象的语句并且正在赋值还没赋值完(正在执行第8行),其他线程是可以进行第一个判断的因为第一个判断没有上锁,但是它判断的是false!它以为指针已经指向了正确的对象,然后它就继续调用Hello(),自然而然,未定义的情况就出现了。这确实是个不容易发现但很致命的race condition。
你说那怎么办?初始化后不直接调用指针?总不是个长久之计(甚至连个计都算不上)。

C++中提供了一个call_once()方法与once_flag变量助你一臂之力。**

class A
{
	SomeObject *someDetail;
	mutex m;
	once_flag o; //整个once_flag
public:
	void init()//整个初始化函数
	{
		someDetail = new SomeObject;//啥也不用担心,就像单进程那样就行
	}
	void someDetailHello()
	{
		call_once(o,&A::init);//调用这个函数,注意第二参数写法。
		someDetail->Hello();
	}
};

simple and easy.

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在C++中,可以使用多线程处理for循环的方式,其中需要使用线程保护共享变量。下面是一个简单的示例代码,演示如何使用多线程处理for循环并创建线程保护变量: ```c++ #include <iostream> #include <thread> #include <mutex> using namespace std; // 定义一个互斥 mutex mtx; void threadFunction(int id, int& sum) { for (int i = 0; i < 1000000; i++) { // 加保护共享变量 sum mtx.lock(); sum++; mtx.unlock(); } } int main() { int sum = 0; const int numThreads = 10; thread threads[numThreads]; // 创建多个线程并启动 for (int i = 0; i < numThreads; i++) { threads[i] = thread(threadFunction, i, ref(sum)); } // 等待所有线程结束 for (int i = 0; i < numThreads; i++) { threads[i].join(); } cout << "Sum = " << sum << endl; return 0; } ``` 在这个示例代码中,我们定义了一个互斥 `mtx`,并将其用于保护共享变量 `sum`。在函数 `threadFunction` 中,我们使用 for 循环对变量 `sum` 进行累加操作,并在操作前加、操作后解。在主函数中,我们创建了多个线程,每个线程执行函数 `threadFunction`,并将变量 `sum` 作为参数传递给线程。最后,我们等待所有线程结束后输出变量 `sum` 的值。 需要注意的是,在使用互斥时,应该尽可能地减少加的时间,以避免线程间的阻塞。在上面的示例代码中,我们将累加操作放在了加和解之间,这样可以尽可能地减少加时间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值