- 调用无参的构造函数
thread() noexcept;
#include
#include
using namespace std;
int main()
{
thread t1;
return 0;
}
pthread_create
函数,并需要传参 线程执行的函数
和该函数需要的参数
等参数,即程序运行时会启动线程。
- 调用带参的构造函数
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
fn是可调用对象,可以是函数指针、lambda表达式、函数对象。
Args是线程调用对象所需要若干个参数。
注意该带参的构造函数用explicit关键字修饰,即该带参的构造函数不支持隐式类型转换。
#include
#include
using namespace std;
void Add(int left, int right)
{
cout << left + right << endl;
}
int main()
{
int x = 10;
int y = 20;
thread t1(Add,x,y);
t1.join();
return 0;
}
- 线程禁用拷贝函数
禁用拷贝函数的作用在于禁止拷贝构造和拷贝赋值,防止多个线程对同一个线程对象进行复制操作,从而避免潜在的并发、数据不一致问题。其作用主要表现为以下:
防止不同线程共享相同的线程对象:如果允许线程对象被复制,那么不同的线程可能会共享相同的线程状态,导致无法确定线程的行为。这可能导致竞态条件和数据竞争等并发问题。、
强制显式管理线程对象的生命周期:禁用拷贝函数迫使程序员显式地管理线程对象的生命周期。这可以确保线程的创建、销毁和使用都是明确的,减少了出现资源泄漏或线程泄漏的可能性。
提高线程安全性:禁用拷贝函数有助于提高线程安全性,因为不同线程之间不会意外地共享线程对象的状态。这有助于减少并发编程中的错误和难以调试的问题。
- 允许使用移动构造函数来构造线程对象
thread (thread&& x) noexcept;
通过使用 std::move
将线程对象从一个地方移到另一个地方,可以避免拷贝线程对象,同时确保线程对象的生命周期正确管理。
#include
#include
using namespace std;
void Add(int left, int right)
{
cout << left + right << endl;
}
int main()
{
int x = 10;
int y = 20;
thread t1=thread(Add,x,y);
t1.join();
return 0;
}
#include
#include
using namespace std;
void Add(int left, int right)
{
cout <<"left+right: " << left + right << endl;
}
class ADD
{
public:
int operator()()
{
int x = 20, y = 10;
return x + y;
}
};
int main()
{
int x = 10;
int y = 20;
thread t1(Add,x,y);//可调用对象为函数指针
thread t2([x, y] {cout <<"x+y: " << x + y << endl; });//可调用对象为lambda表达式
ADD dd;
thread t3(dd);//可调用对象为函数对象
t1.join();
t2.join();
t3.join();
return 0;
}
thread中常用的成员函数如下:
成员函数 | 功能 |
---|---|
join | 对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞 |
joinable | 判断该线程是否已经执行完毕,如果是则返回true,否则返回false |
detach | 将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待 |
get_id | 获取该线程的id |
swap | 将两个线程对象关联线程的状态进行交换 |
另外,joinable
函数可以判断线程是否有效的,是以下任意情况则线程无效:
void func()
{
cout <<"this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
thread t1(func);
cout << "t1 id: "<<t1.get_id() << endl;
t1.join();
return 0;
}
为了能在线程函数中更好的调用线程的成员函数,库中将几个常用的成员函数包装在this_thread命名空间中。可以在线程所关联的线程函数中通过this_thread::xxx调用。
this_thread命名空间内的成员函数有:
get_id | 获取该线程的id |
---|---|
yield | 让出当前线程的执行权,允许系统将执行时间片分配给其他可运行的线程 |
sleep_until | 让当前线程休眠到一个具体时间点 |
sleep_for | 让当前线程休眠一个时间段 |
说明一下:
yield函数作用包括:
yield()
函数会主动放弃当前线程的执行权,告诉操作系统该线程愿意将剩余时间片让给其他线程。这样做可以提高系统的多线程并发性能,避免某个线程长时间占用 CPU 资源而导致其他线程被延迟调度的问题。yield()
函数,您可以以某种方式控制线程的执行优先级。当一个线程通过 yield()
放弃执行权时,操作系统可以按照一定的调度算法重新选择下一个要运行的线程。yield()
可能会导致线程的性能下降,因为线程需要频繁放弃和重新获得执行权。yield()
函数通常用于无锁操作,当当前线程需要切换到另一个线程时,可以调用yield令当前线程放弃时间片,切换上下文,让步給另一个线程占用CPU。yield让当前线程让步CPU时间片,让其他线程占用CPU,通常用于无锁操作。
总结一下:
启动一个线程后,该线程会占用一些资源,当这个线程退出时,需要将线程所占用的资源进行回收,否则会导致内存泄漏问题。因此thread库就为我们两种回收线程资源的策略。
主线程创建新线程后,需要调用
join
函数回收新线程,主线程会阻塞在join
函数处,直到新线程退出,主线程回收新线程资源成功。
join
函数回收完新线程相关资源后,该新线程对象和刚被销毁的栈帧就没有关系了,因此对同一个线程只能join
一次,否则程序会崩溃。void func()
{
cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
thread t1(func);
cout << "t1 id: " << t1.get_id() << endl;
t1.join();
t1.join();
return 0;
}
join
后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可以调用一次join
。void func()
{
cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
thread t1(func);
cout << "t1 id: " << t1.get_id() << endl;
t1.join();
t1 = thread(func);
t1.join();
return 0;
}
join
方法回收线程资源时,可能会出现在join之前因为抛异常或其他原因退出当前栈帧而导致join
失败。void func()
{
cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
thread t1(func);
cout << "t1 id: " << t1.get_id() << endl;
if (3 != 0)
{
return -1;//退出当前栈帧
}
t1.join();
return 0;
}
因此可以采用RAII的方式包装线程对象,利用对象的生命周期控制线程的资源释放,即当退出当前栈帧时自动调用join
方法释放线程资源。
class mythread
{
public:
mythread(thread& t):_t(t)
{
cout << "thread id: " << _t.get_id() << endl;
}
~mythread()
{
cout << "thread id: " << _t.get_id() << " join success" << endl;
_t.join();
}
mythread(mythread const&) = delete;
mythread& operator=(const mythread&) = delete;
private:
thread& _t;
};
void func()
{
cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
thread t1(func);
mythread tt(t1);
return 0;
}
主线程创建新线程后,也可以调用
detach
函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。
detach
的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach
函数。避免在调用detach
之前因为某些原因退出当前栈帧导致线程分离失败,从而导致程序崩溃。线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
void ADD(int& num)
{
num = num + 1;
}
int main()
{
int num = 10;
thread t1(ADD,num);
t1.join();
cout << "after num: " << num << endl;//10 not 11
return 0;
}
如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:
void ADD(int& num)
{
num = num + 1;
}
int main()
{
int num = 10;
thread t1(ADD,ref(num));
t1.join();
cout << "after num: " << num << endl;//11
return 0;
}
void ADD(int* num)
{
*num = *num + 1;
}
int main()
{
int num = 10;
thread t1(ADD,&num);
t1.join();
cout << "after num: " << num << endl;//11
return 0;
}
int main()
{
int num = 10;
thread t1([&num] {num = num + 1; });
t1.join();
cout << "after num: " << num << endl;//11
return 0;
}
在C++11中,Mutex总共包了四个互斥量的种类:
std::mutex
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量,若该互斥量被其他线程所占有,阻塞等待该锁解锁 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果该互斥量已经被其他线程所占有,将直接返回 |
注意一下,当线程函数调用lock()时:
注意一下,当线程函数调用try_lock()时:
死锁概念:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。换言之,一个线程持有资源不释放,向申请不到的资源进行申请,而申请失败处于阻塞等待状态,那么持有的资源将无法释放或者让别的线程使用,该线程就处于死锁状态。
std::recursive_mutex
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock()。
除此之外, std::recursive_mutex 的特性和 std::mutex 大致相同。
std::timed_mutex
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
除此之外,timed_mutex也提供了lock
、try_lock
和unlock
成员函数,其的特性与mutex大致相同。
std::recursive_timed_mutex
recursive_timed_mutex就是recursive_mutex和timed_mutex的结合,recursive_timed_mutex既支持在递归函数中进行加锁操作,也支持定时尝试申请锁。
加锁示例
int gval = 0;
void func1(int val)
{
for (int i = 0; i < val; i++)
{
gval++;
}
}
int main()
{
int m = 1000000;
thread t1(func1,m);
thread t2(func1,2*m);
t1.join();
t2.join();
cout << "val: " << gval << endl;
return 0;
}
因此需要用互斥量保护临界区。
int gval = 0;
mutex mut;
void func1(int val)
{
for (int i = 0; i < val; i++)
{
mut.lock();
gval++;
mut.unlock();//锁放里面相比于锁放外面会多一个是加锁解锁的消耗二个是切换上下文的消耗
}
}
int main()
{
int m = 1000000;
thread t1(func1,m);
thread t2(func1,2*m);
t1.join();
t2.join();
cout << "val: " << gval << endl;
return 0;
}
mut.lock()
这一行时,两个线程会竞争互斥锁,竞争到的线程能够进入临界区执行相关代码,而竞争失败的线程就阻塞等待。当该互斥锁被持有的线程释放时,该竞争失败的线程才能被唤醒,有机会再次竞争到互斥锁。每次只能有一个线程进入临界区。int gval = 0;
mutex mut;
void func1(int val)
{
mut.lock();
for (int i = 0; i < val; i++)
{
gval++;
}
mut.unlock();
}
int main()
{
int m = 1000000;
thread t1(func1,m);
thread t2(func1,2*m);
t1.join();
t2.join();
cout << "val: " << gval << endl;
return 0;
}
在使用互斥锁时,有可能存在加锁后,在解锁前因为线程对象的生命周期结束或其他原因导致退出当前栈帧,没有完成解锁。那么会导致其他线程在申请当前锁时阻塞住造成死锁问题,因此可以以RAII的方式包装互斥锁,需要用到锁的地方实例化一个lock_guard对象,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
lock_guard是C++11中的一个模板类,其定义如下:
template <class Mutex>
class lock_guard;
class Lock_guard
{
public:
Lock_guard(mutex& mut):_mut(mut)
{
_mut.lock();
}
~Lock_guard()
{
_mut.unlock();
}
Lock_guard(const Lock_guard&) = delete;
Lock_guard& operator=(const Lock_guard&) = delete;
private:
mutex& _mut;
};
int gval = 0;
mutex mut;
void func1(int val)
{
{
Lock_guard lg(mut);
for (int i = 0; i < val; i++)
{
gval++;
}
}
}
int main()
{
int m = 1000000;
thread t1(func1,m);
thread t2(func1,2*m);
t1.join();
t2.join();
cout << "val: " << gval << endl;
return 0;
}
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
unique_lock其用法和lock_guard相似
mutex mut;
void func()
{
unique_lock<mutex> ul(mut);//调用构造函数加锁
//......
ul.lock();
func1();
//......
ul.unclock();
}//调用析构函数解锁
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
int gval = 0;
void func1(int val)
{
for (int i = 0; i < val; i++)
{
gval++;
}
}
int main()
{
int m = 1000000;
thread t1(func1, m);
thread t2(func1, 2 * m);
t1.join();
t2.join();
cout << "val: " << gval << endl;
return 0;
}
加加操作分为三步:
load
:将共享变量gval从内存加载到寄存器中。update
:更新寄存器里面的值,执行+1操作。store
:将新值从寄存器写回共享变量n的内存地址。0039310F mov eax,dword ptr [gval (03A03D0h)]
00393114 add eax,1
00393117 mov dword ptr [gval (03A03D0h)],eax
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。以下为原子操作类型的名称及其对应的内置类型名称
注意:需要使用以上原子操作变量时,必须添加头文件
#include
可以通过atomic类模板,定义出对应内置类型的原子类型
atomic<T> t;
//atomic_int gval=0;
atomic<int>gval=0;
void func1(int val)
{
for (int i = 0; i < val; i++)
{
gval++;
}
}
int main()
{
int m = 1000000;
thread t1(func1, m);
thread t2(func1, 2 * m);
t1.join();
t2.join();
cout << "val: " << gval << endl;
return 0;
}
CAS:Compare and Swap,即比较再交换。
例如多个线程对同一个数据进行加加操作,线程将数据放到寄存器中,寄存器保存一份原来的数据,然后放数据到cpu上进行加加,通过比对内存中的数据,当保存的原来的数据与此时内存中的数据相同时,就将加加后的数据放回内存;当保存的原来的数据与此时的内存中的数据不相同时,意味着其他线程已经将改变后的数据放回内存了,那么当前线程就不能将数据放回去了。
cas的缺点:CPU开销大。在并发量比较高的情况下,如果许多线程反复尝试更新某一个值,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
在上层调用线程库,通过条件编译区分调用windows线程库还是linux线程库。