1.thread
在c++11中,c++标准委员会开发出了thread库;接下来我们先来看看这个库的使用吧;
1.1 thread类接口介绍
1.1.1 thread类构造函数
我们thread库中的thread类的构造函数可以通过直接传递回调函数与函数的参数来构造线程:
cpp
int sub(int x, int y)
{
std::cout << x - y << std::endl;
return x - y;
}
void add(int x, int y)
{
std::cout << x + y << std::endl;
}
void test1()
{
int x = 1, y = 2;
std::thread t1(add, x, y);
std::thread t2(sub, x, y);
t1.join();
t2.join();
}
现象:
相比较与linux底层的pthread_create,这里C++thread的使用更加方便,我们不需要固定线程函数的返回值,也不需要通过结构体来传递函数的多个参数,只需要通过函数的不定参数列表来传递多个不同的参数即可;
此外,线程函数删除了拷贝构造,防止了线程的拷贝,但是我们可以使用thread()空构造来创建空线程,之后再通过移动构造的方式让线程接收到线程函数:
cpp
void testfun2()
{
std::cout << "我是线程函数" <<std::endl;
}
void test2()
{
std::vector<std::thread> v;
for (int i = 0; i < 5; i++)
{
v.push_back(std::thread());
}
for (auto& it : v)
{
it = std::thread(testfun2);
}
for (auto& it : v)
{
it.join();
}
}
现象:
linux下的pthread-create,是通过存储pthread_t的tid,来达到存储线程的效果,而C++这里的线程想存储起来,可以通过上面创建空线程的方式存储,最后通过移动构造来创建出带有线程函数的线程;
1.1.2 thread的各种接口
其实thread类的接口并不多,底层都是封装了linux下的线程代码(windows环境下自然就是封装windows的),我们接下来直接通过使用来加深理解:
joinable :
cpp
void testfun3()
{
std::cout << "我是线程函数" << std::endl;
}
void test3()
{
std::thread t1(testfun3);
Sleep(10);//只是让现象更明显,忽略即可
std::cout << t1.joinable() << std::endl;
t1.join();
std::cout << t1.joinable() << std::endl;
}
现象:
一个线程接收到线程函数时运行起来,没有被join时joinable返回值才为true;如果一个线程被join或者detach又或者只是利用thread()默认构造没有连接线程函数,joinable返回值返回为false;总之,joinable可以用来判断一个线程可不可以被join或者detach;
detach:
cpp
void testfun4()
{
std::cout << "我是线程函数" << std::endl;
}
void test4()
{
std::thread t1(testfun4);
t1.detach();
}
在主线程不关心子进程的返回值时直接让子线程与主线程分离,主线程可以去做自己的事情,不需要阻塞的等待子线程;
get_id:
cpp
void testfun5()
{
std::cout << "在线程函数中获取线程id:"<<std::this_thread::get_id()<< std::endl;
}
void test5()
{
std::thread t1(testfun5);
std::cout <<"t1的线程id:" << t1.get_id() << std::endl;
t1.join();
}
现象:
可以通过get_id获取C++自己维护的线程id,在线程外直接通过线程对象即可获取,在线程内需要通过this_thread作用域调用域中函数get_id才可以获取到线程id;
1.2 thread函数的参数
1.2.1 传递给thread的方法
thread函数的第一个参数一定是一个回调方法,这个方法可以是函数指针,仿函数lambda,还可以是function包装器,使用方式是各不相同的;
cpp
void testfun6()
{
std::cout << "我是函数指针" << std::endl;
}
class fun6 {
public:
void operator()()
{
std::cout << "我是仿函数" << std::endl;
}
};
void test6()
{
std::thread t1(testfun6);//函数指针
std::thread t2([]()->void {std::cout << "我是lambda方法" << std::endl; });//lambda
fun6 f;
std::thread t3(f);//仿函数
t1.join();
t2.join();
t3.join();
}
现象:
成功的传递了三种不同的方法(函数);
1.2.1 传递的参数为引用时要带上ref()
cpp
void testfun7(int x,int y,int & z)
{
z = x + y;
}
void test7()
{
int x = 1, y = 2, z=0;
std::thread t1(testfun7,x,y,z);
//std::thread t1(testfun7,x,y,std::ref(z));
Sleep(10);//为了防止主线程跑太快,创建线程还没有运行
std::cout << z << std::endl;
t1.join();
}
当我们写出上面这样的代码时,我们想要让z成为我们希望的值 ;
但是我们编译代码是会出现这样的报错:
当出现这样的报错时,我们就会知道是我们的参数传递出现了问题,那么为什么出现了这样的错误呢?
其实这是和thread对参数传递的处理有关;在C++中thread是被封装成为了一个类的,而这个类的构造函数接收了我们传递的参数,而构造函数会通过参数的类型来推理出模板的类型,从而将参数转化为相应的类型(和我们前面的完美转发有些类型),如果我们想要传递的是一个参数的引用,我们就需要显式的声明这个参数是引用,使得这个参数保持引用的状态一直向下传递;
将z声明为ref(z)即可
cpp
std::thread t1(testfun7,x,y,std::ref(z));
此时再运行:
在我们的linux线程部分,似乎从来没有出现过这样的问题,这其实是C++类模板的推理所出现的问题;而在linux下的线程是通过指针转递的,所以不会出现这样的推理的问题;
介绍完线程我们接下来介绍一下与线程息息相关的mutex
2.mutex
2.1 mutex对象
mutex头文件中的mutex类是一个无法被拷贝的类,并且他的构造函数是无参的构造函数
下面是它的接口:
这些接口的使用其实和linux下线程的使用是一摸一样的,不相同的只是需要通过mutex类来调用而已;(面向对象与面向过程的区别);其中的native_handle这个接口是开放的一个底层的接口,可以通过其去设置pthread_mutex_t类型指针指向的内容,这样就可以设置锁的某些属性,这个接口我也没用过,这里只是作为介绍,也让我自己有所了解;
此外mutex的头文件中基本的锁对象还有有这些锁对象:
2.2 mutex头文件中的其他对象
2.2.1 recursive_mutex
这个锁对象一般是用在递归函数中,用来解决递归调用时使用普通锁出现的死锁问题,在递归中,我们一般需要使用recursive_mutex而不是普通的mutex锁;
2.2.2 timed_mutex
这个锁提供了一种尝试获取锁并在一定时间后自动放弃的机制。这对于需要防止死锁或者提高线程响应性的应用场景非常有用
接下来我们在来说一说对基本锁对象封装的锁:
2.2 对基本锁的封装
锁可以保证线程的安全,但在抛异常出现时,执行流的跳转会有致命的死锁出现,那么为了解决这样的问题,RAII风格的锁就出现了:
2.2.1 lock_guard
C++封装了这样的锁,可以让锁上锁后,在这个栈帧被析构时随着栈帧自动释放锁,lock_guard就是这样的封装;
lock_guard的封装很简单没有其他的接口;
2.2.2 unique_lock
unique_lock可以说是lock_guard的提升版,不仅可以手动释放锁,还增加了时间锁的功能:
上面是对锁的介绍,接下来我们介绍一下条件变量condition_variable;
3. condition_variable
这个头文件中有这两个基本类:
其中condition_variable:
condition_variable只能与std::unique_lock<std::mutex>结合使用,如果想配合其他类型的锁使用费可以使用condition_variable_any类;
下面我们使用 condition_variable类来实现,两个不同线程分别打印1到10的奇数与偶数:
cpp
两个线程分别打印奇偶数
std::mutex mtx;
std::condition_variable cond;
int count = 1;
void printOdd()
{
while (true)
{
std::unique_lock<std::mutex>lck(mtx);
if (count > 10)
{
lck.unlock();
break;
}
while (count % 2 == 0)cond.wait(lck);
if(count<=10)std::cout<<std::this_thread::get_id()<<": " << count << std::endl;
count++;
//lck.unlock();
cond.notify_one();
}
}
void printEven()
{
while (true)
{
std::unique_lock<std::mutex>lck(mtx);
if (count > 10)
{
if (count % 2 == 1)lck.unlock();
break;
}
while (count % 2 == 1)cond.wait(lck);
if (count <= 10)std::cout << std::this_thread::get_id() << ": " << count << std::endl;
count++;
//lck.unlock();
cond.notify_one();
}
}
int main()
{
std::thread t1(printOdd);
std::thread t2(printEven);
t1.join();
t2.join();
return 0;
}
现象:
condition_variable的使用与linux下cond条件变量的使用是一样的,只不过上层语言层对其进行了封装使得其具有了跨平台性;
4.atomic
4.1 atomic的基本使用
线程安全问题出现时,我们不仅可以使用锁来控制线程安全,还可以使用atomic原子操作库中的接口来保护线程安全;
我们用这样一个场景来举例:当我们有100个线程时,每个线程对count计数器加加一万次,如果线程安全,那么最后的结果应该是100*10000=1000000,一百万,我们用下面的代码验证;
cpp
int count = 0;
void add()
{
for (int i = 0; i < 10000; i++)
{
count++;
}
}
void testAtomic()
{
std::vector<std::thread> v;
for (int i = 0; i < 100; i++)
{
v.push_back(std::thread());
}
for (auto& it : v)
{
it = std::thread(add);
}
for (auto& it : v)
{
it.join();
}
std::cout << "count :" << count << std::endl;
}
测试结果:
此时,如果按照我们平常的操作,我们会使用锁来保证线程安全,但atomic库的出现,我们可以不使用锁,我们可以这样操作:
cpp
std::atomic<int> count = 0;
仅仅只需要将我们的count变量用atomic类来包装,就可以使得count的操作是原子的;
4.2 atomic的底层
那么为什么要有这样的原子库呢?我们的锁不是已经可以解决线程安全的问题了吗,为什么还要加一个这样的atomic库呢?我们就得看看atomic的底层CAS无锁编程了;
我们可以将对atomic变量的操作看成下面这样的操作:
上面的atomic变量++操作中与平常的++操作不相同的地方是使用CAS函数来判断count位置内存中的数据是否发生了改变,如果count位置的数据发生了改变就代表有其他线程在我们进行++操作时对count进行修改,所以我们刚刚进行的++操作是无效的需要重新进行;
在atomic库中CAS检查函数的原型是这个:
cpp
template< class T >
bool atomic_compare_exchange_weak( std::atomic* obj,
T* expected, T desired );
template< class T >
bool atomic_compare_exchange_weak( volatile std::atomic* obj,
T* expected, T desired );
而实际实现我们可以看成这样:
cpp
bool compare_and_swap (int *addr, int oldval, int newval)
{
if ( *addr != oldval ) {
return false;
}
*addr = newval;
return true;
}
如果还需要更多的了解可以看陈皓大佬的博客网站coolshell:
4.3 atomic的优点
我们看了上面的CAS操作我们可以发现这样的保证线程安全的原子操作没有是锁的,这意味着我们的线程不需要对锁资源进行竞争的获取,性能优越,开销小;避免了锁的上下文切换,适合高并发的场景;但是atomic变量对于复杂的线程场景可能会出现问题,我们还是得使用锁来进行复杂逻辑的编程;
5.智能指针引用计数线程安全
在今天之前我们只会使用加锁的方式来保护引用计数的线程安全,不过今天之后,我们就可以使用atomic库来保护线程安全;
cpp
class smartPtr{
public:
smartPtr(T* ptr = nullptr)
:_ptr(ptr), _pcount(new std::atomic<int>(1))
{}
template<class D>
smartPtr(T* ptr,D del)
:_ptr(ptr), _pcount(new std::atomic<int>(1)),_del(del)
{}
......
private:
T* _ptr;
//int* _pcount;
std::atomic<int>* _pcount;
};
我们只需要将_pcount引用计数更改为atomic类型的变量的指针即可,在构造时,将_pcount初始化为指向堆区的atomic变量的指针;这里的保护很简单,我们还需要注意智能指针本身是线程安全的,但是智能指针保护的资源并不是线程安全的;
6.单例模式最简单实现方式
cpp
//单例模式最简单实现方式(懒汉)
class singleClass {
public:
static singleClass& getClass()
{
static singleClass s;
return s;
}
//删除构造和拷贝构造
singleClass(const singleClass&) = delete;
singleClass& operator=(const singleClass&) = delete;
private:
singleClass()
{
cout << "singleClass()" << endl;
}
};
int main()
{
singleClass::getClass();
singleClass::getClass();
singleClass::getClass();
singleClass::getClass();
singleClass::getClass();
return 0;
}
现象:
在C++11后我们可以这样实现单例模式,因为局部的静态变量只会在第一次初始化,保护了线程安全;但如果是在C++11之前无法保证线程是安全的;
其更本原因是:
C++11后的局部静态变量:
如果多个线程同时尝试初始化同一个局部静态变量,只有一个线程会成功初始化该变量,其他线程会被阻塞,直到初始化完成。
这一机制是通过在内部实现中使用适当的锁或原子操作来确保的。这样可以有效地避免线程间的竞争条件,确保了程序的安全性和一致性。
C++11后对局部静态变量做了处理,防止多线程竞争变量时出现的线程安全问题;