目录
一、thread类
1.1 thread类的构造方法
1.2 其他函数接口
注意
2.3 this_thread命名空间
二、mutex锁
2.1 mutex类
2.2 recursive_mutex
2.3 timed_mutex
三、原子性操作库(atomic)
四、利用RAII机制管理锁
4.1 lock_guard
4.2 unique_lock
五、条件变量
我们可以参考下面文档:C++ thread类
1、支持无参构造。构造一个空线程对象,由于没有关联的线程函数,所以不会直接运行。
2、支持可变参数构造。(最常用) 构造一个线程对象,并关联线程函数fun,args1,args2,...为线程函数的参数。
#include
#include
using namespace std;
void Add(int x, int y)
{
cout << x + y << endl;
}
int main()
{
int a = 10, b = 30;
thread t1(Add, a, b);
t1.join();
return 0;
}
这里join函数的作用是让线程运行完进程进行回收。不然就会造成资源不回收,引发内存泄漏。
3、不支持拷贝构造。
4、支持移动赋值。
get_id:获取线程id,也是线程的唯一标识。get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
join:等待线程回收分配给线程的资源。
joinable:用于判断是否需要回收线程资源。
detach:线程与主线程分离,彼此独立运行。两个线程继续,不会以任何方式阻塞或同步。请注意,当任何一个结束执行时,都会释放其自己的资源。
1. 线程是操作系统中的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的状态。2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
- 函数指针
- lambda表达式
- 函数对象(仿函数)
class temp { public: void operator()() { cout << "thread t3" << endl; } }; int main() { thread t1(Add, 1, 2); thread t2([]() {cout << "thread t2" << endl; }); temp t; thread t3(t); t1.join(); t2.join(); t3.join(); return 0; }
4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
get_id:用于获取线程id。
sleep_for:进程睡眠一段时间。
sleep_until:进程睡眠至某个时间。
由于不会特别常用,这里就不详细介绍,需要用时差文档即可:this_thread - C++ Reference (cplusplus.com)
案例代码:
int ret = 0;
void Func()
{
int n = 10000;
while (n--)
{
ret++;
}
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << ret << endl;
return 0;
}
为了解决这个问题,引入了锁mutex来使得++操作一次完成。
mutex类用到的主要两个函数就是:lock 和 unlock。
mutex m;
int ret = 0;
void Func()
{
int n = 10000;
while (n--)
{
m.lock();
ret++;
m.unlock();
}
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << ret << endl;
return 0;
}
该类提供的函数接口和mutex类一样,但是允许一个线程多次加锁,来获得互斥对象的多个级别的所有权。
相较于上面两种锁,timed_mutex锁增加了两个功能:try_lock_for 和 try_lock_until
try_lock:能够在一定的时间范围内申请锁。如果当前锁未被申请,那么调用的线程就将取走锁;如果当前锁已经被申请了,那么就会返回false。
try_lock_until:尝试申请锁知道某个时间点。
该类的使用需要包含头文件
我们下面看atomic类的构造方法:
可以看到:支持无参构造和列表初始化,但是不能拷贝。
atomic a;
void func()
{
int n = 10000;
while (n--)
{
a++;
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
return 0;
}
这是一个C++中定义的用来管理锁的类,在构造对象时候加锁,析构对象的时候解锁。
实现代码:
template
class lock_guard
{
public:
explicit lock_guard(_Mutex& _Mtx)
:_MyMutex(_Mtx)
{
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t)
:_MyMutex(_Mtx)
{}
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operato = (const lock_guard&) = delete;
private:
_Mutex _MyMutex;
};
通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
案例:
int a = 0;
mutex mx;
void func()
{
int n = 10000;
lock_guard mt(mx);
while (n--)
{
a++;
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
return 0;
}
我们先看一道题:两个线程交替打印0-100的数字,一个打印奇数,一个打印偶数。
我们通常情况下解题是while循环中用if条件判断来判断,一个线程t1判断奇数打印,一个线程t2判断偶数打印,然后打印完++。但是当我们打印t1奇数的时候,此时时间片切到t2,t2会不断的循环判断,直到时间片切回t1。这样就造成了CPU资源的浪费。
这里就要引入我们的条件变量:std::condition_variable、
条件变量中的 wait 和 notify_one 的接口能够实现进程的等待和唤醒。 使得进程避免因为不满足条件而一直循环判断,浪费资源。
需要注意的是:
wait接口的参数是unique_lock类型。
有人会好奇为什么需要传一个锁进来呢?
因为条件变量操作不是原子性的,我们需要加锁保护,但是我们加了锁让线程等待,但是其他线程因为申请不到锁也会进入阻塞,那么不就死循环了吗?
其实并不是的,wait操作之所以需要传一个锁进来,就是因为wait操作的同时,会将锁释放,让其他线程能够申请到锁,直到用notify_one来唤醒线程的时候,才会重新持有锁。
有了条件变量,我们可以让进程在不满足条件的时候进行等待,在满足条件之后再唤醒进程运行。
案例代码:
int main()
{
int a = 0;
condition_variable cv;
mutex mt;
//打印奇数
thread t1([&]()
{
while (a <= 100)
{
unique_lock lock(mt);
if (a % 2 == 0)
{
cv.wait(lock);
}
cout << "t1->" << a << endl;
++a;
cv.notify_one();
}
});
thread t2([&]()
{
while (a <= 100)
{
unique_lock lock(mt);
if (a % 2 == 1)
{
cv.wait(lock);
}
cout << "t2->" << a << endl;
++a;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
我们这里就发现问题了,怎么会打印出101来呢?
原因出在这里:
因此我们只需要把t1时的循环条件<= 改成 < 即可,这样,在100的时候进不去循环了,自然后面的操作也就不会执行了。