线程库(类)
在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。
thread的带参的构造函数的定义如下:
cpp
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
线程类的使用:
cpp
void func(int n)
{
for (int i = 0; i <= n; i++)
{
cout << i << endl;
}
}
int main()
{
thread t3 = thread(func, 10);
t3.join();
return 0;
}
- 如果创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行。
- thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
- 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:
方式一:借助std::ref函数
当线程函数的参数类型为引用类型时,如果要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref函数保持对实参的引用。比如:
cpp
void add(int& num)
{
num++;
}
int main()
{
int num = 0;
thread t(add, ref(num));
t.join();
cout << num << endl; //1
return 0;
}
线程回收:join与,ref值引用
启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。
主线程创建新线程后,可以调用join函数等待新线程终止,当新线程终止时join
函数就会自动清理线程相关的资源。join
函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join
,否则程序会崩溃。比如:
cpp
void func(int n)
{
for (int i = 0; i <= n; i++)
{
cout << i << endl;
}
}
int main()
{
thread t(func, 20);
// t.detach(); 主线程直接分离线程,不再阻塞
t.join();
t.join(); //程序崩溃
return 0;
}
因此采用join
方式结束线程时,join
的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,也就是利用对象的生命周期来控制线程资源的释放。比如:
cpp
class myThread
{
public:
myThread(thread& t)
:_t(t)
{}
~myThread()
{
if (_t.joinable())
_t.join();
}
//防拷贝
myThread(myThread const&) = delete;
myThread& operator=(const myThread&) = delete;
private:
thread& _t;
};
互斥量库(mutex)
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。
cpp
void func(int n, mutex& mtx)
{
mtx.lock(); //for循环体外加锁
for (int i = 1; i <= n; i++)
{
//mtx.lock(); //for循环体内加锁
cout << i << endl;
//mtx.unlock();
}
mtx.unlock();
}
int main()
{
mutex mtx;
thread t1(func, 100, ref(mtx));
thread t2(func, 100, ref(mtx));
t1.join();
t2.join();
return 0;
}
锁资源和线程资源都需要我们进行管理。都可以使用RAII风格管理。lock_guard和unique_lock
cpp
mutex mtx;
void func()
{
//...
//匿名局部域
{
lock_guard<mutex> lg(mtx); //调用构造函数加锁
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
{
//...
return; //调用析构函数解锁
}
} //调用析构函数解锁
//...
}
int main()
{
func();
return 0;
}
如下场景就适合使用unique_lock:
- 要用互斥锁保护函数1的大部分代码,但是中间有一小块代码调用了函数2,而调用函数2时不需要用函数1中的互斥锁进行保护,函数2内部的代码由其他互斥锁进行保护。
- 因此在调用函数2之前需要对当前互斥锁进行解锁,当函数2调用返回后再进行加锁,这样当调用函数2时其他线程调用函数1就能够获取到这个锁。
原子类库
原子类解决线程安全问题
C++11中引入了原子操作类型,使得线程间数据的同步变得非常高效。
- 为了防止意外,标准库已经将atomic模板类中的拷贝构造、移动构造、
operator=
默认删除掉了。 - 原子类型不仅仅支持原子的
++
操作,还支持原子的--
、加一个值、减一个值、与、或、异或操作。
如下:两个线程对同一个共享变量进行++操作
cpp
void func(atomic_int& n, int times)
{
for (int i = 0; i < times; i++)
{
n++;
}
}
int main()
{
atomic_int n = { 0 };
int times = 100000; //每个线程对n++的次数
thread t1(func, ref(n), times);
thread t2(func, ref(n), times);
t1.join();
t2.join(); //先join等两个子线程执行完毕
cout << n << endl; //打印n的值
return 0;
}
实现两个线程交替打印1-100
尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。
cpp
int main()
{
int n = 100;
mutex mtx;
condition_variable cv;
bool flag = true;
//奇数
thread t1([&]{
int i = 1;
while (i <= 100)
{
unique_lock<mutex> ul(mtx);
cv.wait(ul, [&flag]()->bool{return flag; }); //等待条件变量满足
cout << this_thread::get_id() << ":" << i << endl;
i += 2;
flag = false;
cv.notify_one(); //唤醒条件变量下等待的一个线程
}
});
//偶数
thread t2([&]{
int j = 2;
while (j <= 100)
{
unique_lock<mutex> ul(mtx);
cv.wait(ul, [&flag]()->bool{return !flag; }); //等待条件变量满足
cout << this_thread::get_id() << ":" << j << endl;
j += 2;
flag = true;
cv.notify_one(); //唤醒条件变量下等待的一个线程
}
});
t1.join();
t2.join();
return 0;
}
C++异常
异常是面向对象语言常用的一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数直接或间接的调用者处理这个错误。
- throw:当程序出现问题时,可以通过throw关键字抛出一个异常。
- try:try块中放置的是可能抛出异常的代码,该代码块在执行时将进行异常错误检测,try块后面通常跟着一个或多个catch块。
- catch:如果try块中发生错误,则可以在catch块中定义对应要执行的代码块。
异常的抛出和捕获的匹配原则:
- 异常是通过throw 对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码,如果抛出的异常对象没有捕获,或是没有匹配类型的捕获,那么程序会终止报错。
- 被选中的处理代码(catch块)是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(类似于函数的传值返回)
- catch(...)可以捕获任意类型的异常,但捕获后无法知道异常错误是什么。
- 实际异常的抛出和捕获的匹配原则有个例外,捕获和抛出的异常类型并不一定要完全匹配,可以抛出派生类对象,使用基类进行捕获,这个在实际中非常有用。
最基础的异常类至少需要包含错误编号和错误描述两个成员变量,甚至还可以包含当前函数栈帧的调用链等信息。该异常类中一般还会提供两个成员函数,分别用来获取错误编号和错误描述。比如:
cpp
class Exception
{
public:
Exception(int errid, const char* errmsg)
:_errid(errid)
, _errmsg(errmsg)
{}
int GetErrid() const
{
return _errid;
}
virtual string what() const
{
return _errmsg;
}
protected:
int _errid; //错误编号
string _errmsg; //错误描述
//...
};
其他模块如果要对这个异常类进行扩展,必须继承这个基础的异常类,可以在继承后的异常类中按需添加某些成员变量,或是对继承下来的虚函数what进行重写,使其能告知程序员更多的异常信息。异常类的成员变量不能设置为私有,因为私有成员在子类中是不可见的,基类Exception中的what成员函数最好定义为虚函数,方便子类对其进行重写,从而达到多态的效果。
智能指针
智能指针的原理
实现智能指针时需要考虑以下三个方面的问题:
- 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
- 对
*
和->
运算符进行重载,使得该对象具有像指针一样的行为。 - 智能指针对象的拷贝问题。浅拷贝析构两次
auto_ptr管理权转移
auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。
unique_ptr防拷贝
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放
shared_ptr 引用计数
类模板,shared_ptr<int> p(new int(0));
cpp
namespace xwy
{
template<class T>
class shared_ptr
{
private:
//++引用计数
void AddRef()
{
_pmutex->lock();
(*_pcount)++;
_pmutex->unlock();
}
//--引用计数
void ReleaseRef()
{
_pmutex->lock();
bool flag = false;
if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmutex->unlock();
if (flag == true)
{
delete _pmutex;
}
}
public:
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
{}
~shared_ptr()
{
ReleaseRef();
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmutex(sp._pmutex)
{
AddRef();
}
shared_ptr& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
{
ReleaseRef(); //将管理的资源对应的引用计数--
_ptr = sp._ptr; //与sp对象一同管理它的资源
_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
_pmutex = sp._pmutex; //获取sp对象管理的资源对应的互斥锁
AddRef(); //新增一个对象来管理该资源,引用计数++
}
return *this;
}
//获取引用计数
int use_count()
{
return *_pcount;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
mutex* _pmutex; //管理的资源对应的互斥锁
};
}
std::shared_ptr的定制删除器
定制删除器的用法
第二个参数传入一个 仿函数,再智能指针析构时调用
cpp
template<class T>
struct DelArr
{
void operator()(const T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
});
return 0;
}
weak_ptr解决循环引用
weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。比如:
cpp
struct ListNode
{
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//...
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
C语言中的类型转换
C语言中有两种形式的类型转换,分别是隐式类型转换和显式类型转换:
- 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
- 显式类型转换:需要用户自己处理,以
(指定类型)变量
的方式进行类型转换。
需要注意的是,只有相近类型之间才能发生隐式类型转换,比如int和double表示的都是数值,只不过它们表示的范围和精度不同。而指针类型表示的是地址编号,因此整型和指针类型之间不会进行隐式类型转换,如果需要转换则只能进行显式类型转换。比如:
cpp
int main()
{
//隐式类型转换
int i = 1;
double d = i;
cout << i << endl;
cout << d << endl;
//显式类型转换
int* p = &i;
int address = (int)p;
cout << p << endl;
cout << address << endl;
return 0;
}
int a = xxx_cast<int> (b);
static_cast
static_cast用于相近类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关类型之间转换。比如:
cpp
int main()
{
double d = 12.34;
int a = static_cast<int>(d);
cout << a << endl;
int* p = &a;
// int address = static_cast<int>(p); //error
return 0;
}
reinterpret_cast
用于两个不相关类型之间的转换。比如指针转整型:
cpp
int main()
{
int a = 10;
int* p = &a;
int address = reinterpret_cast<int>(p);
cout << address << endl;
return 0;
}
const_cast必须转指针
const_cast用于删除变量的const属性,转换后就可以对const变量的值进行修改。提供一种修改const常量的方法,C语言中可以间接修改const常量,C++类型检查更严格。比如:
cpp
int main()
{
const int a = 2; //加valatile 下面变成
int* p = const_cast<int*>(&a);
*p = 3;
cout << a << endl; //2
cout << *p << endl; //3
return 0;
}
dynamic_cast转为子类指针
向上转型: 子类的指针(或引用)→ 父类的指针(或引用)。
向下转型: 父类的指针(或引用)→ 子类的指针(或引用)。
其中,**向上转型就是所说的切割/切片,**是语法天然支持的,不需要进行转换,而向下转型是语法不支持的,需要进行强制类型转换。
**使用dynamic_cast进行向下转型则是安全的,**如果父类的指针(或引用)指向的是子类对象那么dynamic_cast会转换成功,但如果父类的指针(或引用)指向的是父类对象那么dynamic_cast会转换失败并返回一个空指针。
cpp
class A
{
public:
virtual void f()
{}
};
class B : public A
{};
void func(A* pa)
{
B* pb1 = (B*)pa; //不安全
B* pb2 = dynamic_cast<B*>(pa); //安全
cout << "pb1: " << pb1 << endl;
cout << "pb2: " << pb2 << endl;
}
int main()
{
A a;
B b;
func(&a);
func(&b);
return 0;
}
说明一下: dynamic_cast只能用于含有虚函数的类,因为运行时类型检查需要运行时的类型信息,而这个信息是存储在虚函数表中的,只有定义了虚函数的类才有虚函数表。
explicit
explicit用来修饰单参数构造函数,从而禁止单参数构造函数的隐式转换。
RTTI(Run-Time Type Identification)就是运行时类型识别。
C++通过以下几种方式来支持RTTI:
- typeid:在运行时识别出一个对象的类型。
- dynamic_cast:在运行时识别出一个父类的指针(或引用)指向的是父类对象还是子类对象。
- decltype:在运行时推演出一个表达式或函数返回值的类型。