在涉及到多线程问题时,不同的平台下都有不同的接口,这样使得代码的可移植性变的很差,C++11中一个比较重要的特性就是支持了线程,使得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;
}
结果:
线程关联函数可按照三种方式给出:函数指针,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(返回当前唯一锁管理的互斥量的指针) |