C++11中的线程库

在涉及到多线程问题时,不同的平台下都有不同的接口,这样使得代码的可移植性变的很差,C++11中一个比较重要的特性就是支持了线程,使得C++在并行编程时并不需要依赖第三方库。在使用时,只需包含 < thread > 头文件。

C++thread库

线程创建

构造线程时需要注意:

①:默认构造的线程,即没有启动函数的,那么此时的线程就不会启动,也没有线程id
 
②:初始化构造,即线程对象有启动函数,并且也有参数传递,该线程是joinable状态的
 
③:C++11中线程库的线程不支持拷贝构造,该函数被delete
 
④:移动构造,构造一个线程对象,将一个线程对象关联的线程的状态转移给其它线程对象,转移期间不影响线程的执行

示例:

void FunC(int a)
{
	cout << a << endl;
}

int main()
{
	thread t1;//该线程为默认构造,没有启动函数,也就没有线程id
	thread t2(FunC, 10);//初始化构造,有启动函数,则有线程id

	cout << t1.get_id() << endl;//0
	cout << t2.get_id() << endl;//线程id

	thread t3(move(t2));//移动构造,此时线程对象t2资源就被移动到线程对象t3中了

	cout << t2.get_id() << endl;// 被移动构造过,不再是执行线程,为0
	cout << t3.get_id() << endl;// 同上面没有被移动构造时的 t2线程id

	//t1.join();// 不是执行线程,不用等待
	//t2.join();// 不是执行线程,不用等待
	t3.join();// 线程等待,必须等待
	return 0;
}

结果:

C++11中的线程库_第1张图片

线程关联函数可按照三种方式给出:函数指针,lambda表达式,函数对象;

注意:可joinable的线程在销毁之前,应被线程等待或线程分离。

可通过join able函数判断线程是否有效。

线程的参数传递

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,则即使线程参数为引用类型,在线程中修改后也并不能修改外部实参,因为实际引用的线程栈中的拷贝,而不是外部实参

如果想修改实参可使用ref函数,该函数返回一个reference_wrapper对象,可用于对元素的访问和修改;
初始构造时,不访问元素,但是返回的对象可用于访问或修改它。
拷贝构造时,访问元素,返回一个对象,该对象可用于访问或修改其引用的元素。

或者使用指针传递,也可修改实参。

void FunC(int& a)
{
	++a;
}

int main()
{
	int a = 10;

	thread t1(FunC, ref(a));

	cout << a << endl;
	t1.join();

	return 0;
}

线程等待与分离

线程等待:C++11中使用join函数,该函数会让主线程阻塞,当工作线程退出时,join会清理线程资源,然后返回,主线程再继续往下执行,一个线程对象只能使用一个join,否则程序会崩溃。

线程等待始终存在着问题,如果线程抛异常退出了,那么线程的资源就不会被清理。

线程分离:工作线程会线程对象进行分离,不再被线程对象所关联,就不能通过线程对象控制线程,工作线程会在后台运行,并将所有权和控制权交给C++库,当线程退出时,其相关资源会被自动回收。

原子性操作库atomic

多线程会引起线程安全问题,当一个或多个线程访问同一块临界资源时,就会造成很多麻烦。

传统的方式:对临界资源进行加锁,但加锁会导致,同一时间只能由一个线程对临界资源进行访问,而其它线程会被阻塞,就会影响程序的运行效率,并且如果锁的控制不好救会造成死锁。

库文件为< atomic >;那么在访问该原子类型变量时,就可以不必对该变量进行加锁访问,线程能够保证对原子类型变量的互斥访问。

atomic<int> a = 100;

int main()
{
	vector<thread> vt;
	for (int i = 0; i < 5; ++i)
	{
		vt.push_back(thread([&]() {
			for (int i = 0; i < 10; ++i)
				++a;
		}));
	}

	cout << a << endl;//a = 150,并且不会出错

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

	return 0;
}

互斥锁

C++11中,使用< mutex >进行加锁操作。
常见的加锁操作:

函数名 功能
lock() 上锁,锁住互斥量,若锁被其它线程拿着,会阻塞
unlock() 解锁,释放互斥量
try_lock() 尝试加锁,该操作不会使线程阻塞

守卫锁:lock_guard

基于RAII思想设计的守卫锁,防止异常安全导致的死锁问题:

template<class T = mutex>
class LockGuard
{
private:
	T& _mutex;
public:
	LockGuard(T& mtx)
		:_mutex(mtx)
	{
		_mutex.lock();
	}

	~LockGuard()
	{
		_mutex.unlock();
	}

	LockGuard(const T&) = delete;
	T& operator=(const T&) = delete;
};

唯一锁:unique_lock

与守卫锁类型,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且是以独占所有权的方式管理mutex对象的上锁和解锁操作,即对象之间不能进行拷贝和赋值操作

在移动构造/赋值时,unique_lock对象需要传递一个mutex对象作为参数,新建的unique_lock对象负责对传入的mutex对象进行上锁和解锁;

实例化对象时自动调用构造函数上锁,对象销毁时自动调用析构函数解锁,可以很方便的防止死锁现象。

和防卫锁不同的是,唯一锁更加灵活,提供了更多的成员函数:

功能 成员函数
上锁操作和解锁 try,try_lock,try_lock_for,try_lock_until和unlock
修改操作 移动赋值、交换(swap:与另一个唯一锁对象互换所管理的互斥量所有权)、释放(release:返回所管理的互斥量对象的指针,并释放所有权)
获取属性 owns_lock(返回当前对象是否已上锁)、operator bool()(与own_lock()功能类似)、mutex(返回当前唯一锁管理的互斥量的指针)

你可能感兴趣的:(C/C++学习,c++,多线程)