c++11 新特性
新增线程编程相关模块
相关信息
c++11关于并发引入了好多好东西,这里按照如下顺序介绍:
- std::thread相关
- std::mutex相关
- std::lock相关
- std::atomic相关
- std::call_once相关
- std::condition_variable相关
- std::future相关
- async相关
std::thread相关
c++11引入了std::thread来创建线程,支持对线程join或者detach。直接看代码:
#include <iostream>
#include <thread>
using namespace std;
int main() {
auto func = []() {
for (int i = 0; i < 10; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread t(func);
if (t.joinable()) {
t.detach();
}
auto func1 = [](int k) {
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread tt(func1, 20);
if (tt.joinable()) { // 检查线程可否被join
tt.join();
}
return 0;
}
上述代码中,函数func和func1运行在线程对象t和tt中,从刚创建对象开始就会新建一个线程用于执行函数,调用join函数将会阻塞主线程,直到线程函数执行结束,线程函数的返回值将会被忽略。如果不希望线程被阻塞执行,可以调用线程对象的detach函数,表示将线程和线程对象分离。
如果没有调用join或者detach函数,假如线程函数执行时间较长,此时线程对象的生命周期结束调用析构函数清理资源,这时可能会发生错误,这里有两种解决办法,一个是调用join(),保证线程函数的生命周期和线程对象的生命周期相同,另一个是调用detach(),将线程和线程对象分离,这里需要注意,如果线程已经和对象分离,那我们就再也无法控制线程什么时候结束了,不能再通过join来等待线程执行完。
这里可以对thread进行封装,避免没有调用join或者detach可导致程序出错的情况出现:
class ThreadGuard {
public:
enum class DesAction { join, detach };
ThreadGuard(std::thread&& t, DesAction a) : t_(std::move(t)), action_(a){};
~ThreadGuard() {
if (t_.joinable()) {
if (action_ == DesAction::join) {
t_.join();
} else {
t_.detach();
}
}
}
ThreadGuard(ThreadGuard&&) = default;
ThreadGuard& operator=(ThreadGuard&&) = default;
std::thread& get() { return t_; }
private:
std::thread t_;
DesAction action_;
};
int main() {
ThreadGuard t(std::thread([]() {
for (int i = 0; i < 10; ++i) {
std::cout << "thread guard " << i << " ";
}
std::cout << std::endl;}), ThreadGuard::DesAction::join);
return 0;
}
c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能
std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::mutex相关
std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。
mutex分为四种:
- std::mutex:独占的互斥量,不能递归使用,不带超时功能
- std::recursive_mutex:递归互斥量,可重入,不带超时功能
- std::timed_mutex:带超时的互斥量,不能递归
- std::recursive_timed_mutex:带超时的互斥量,可以递归使用
拿一个std::mutex和std::timed_mutex举例吧,别的都是类似的使用方式:
std::mutex:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
std::mutex mutex_;
int main() {
auto func1 = [](int k) {
mutex_.lock();
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::timed_mutex:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
using namespace std;
std::timed_mutex timed_mutex_;
int main() {
auto func1 = [](int k) {
timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
timed_mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::lock相关
这里主要介绍两种RAII方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似,如下:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
using namespace std;
std::mutex mutex_;
int main() {
auto func1 = [](int k) {
// std::lock_guard<std::mutex> lock(mutex_);
std::unique_lock<std::mutex> lock(mutex_);
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::lock_gurad相比于std::unique_lock更加轻量级,少了一些成员函数,std::unique_lock类有unlock函数,可以手动释放锁,所以条件变量都配合std::unique_lock使用,而不是std::lock_guard,因为条件变量在wait时需要有手动释放锁的能力,具体关于条件变量后面会讲到。
std::atomic相关
c++11提供了原子类型 ,理论上这个T可以是任意类型,但是我平时只存放整形,别的还真的没用过,整形有这种原子变量已经足够方便,就不需要使用std::mutex来保护该变量啦。看一个计数器的代码:
struct OriginCounter { // 普通的计数器
int count;
std::mutex mutex_;
void add() {
std::lock_guard<std::mutex> lock(mutex_);
++count;
}
void sub() {
std::lock_guard<std::mutex> lock(mutex_);
--count;
}
int get() {
std::lock_guard<std::mutex> lock(mutex_);
return count;
}
};
struct NewCounter { // 使用原子变量的计数器
std::atomic<int> count;
void add() {
++count;
// count.store(++count);这种方式也可以
}
void sub() {
--count;
// count.store(--count);
}
int get() {
return count.load();
}
};
std::call_once相关
c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用,直接看使用代码吧:
std::once_flag onceflag;
void CallOnce() {
std::call_once(onceflag, []() {
cout << "call once" << endl;
});
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(CallOnce);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::condition_variable相关
条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。
这里使用条件变量实现一个CountDownLatch:
class CountDownLatch {
public:
explicit CountDownLatch(uint32_t count) : count_(count);
void CountDown() {
std::unique_lock<std::mutex> lock(mutex_);
--count_;
if (count_ == 0) {
cv_.notify_all();
}
}
void Await(uint32_t time_ms = 0) {
std::unique_lock<std::mutex> lock(mutex_);
while (count_ > 0) {
if (time_ms > 0) {
cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
} else {
cv_.wait(lock);
}
}
}
uint32_t GetCount() const {
std::unique_lock<std::mutex> lock(mutex_);
return count_;
}
private:
std::condition_variable cv_;
mutable std::mutex mutex_;
uint32_t count_ = 0;
};
关于条件变量其实还涉及到通知丢失和虚假唤醒问题,因为不是本文的主题,这里暂不介绍,大家有需要可以留言。
std::future相关
c++11关于异步操作提供了future相关的类,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。
- std::promise与std::future配合使用
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
void func(std::future<int>& fut) {
int x = fut.get();
cout << "value: " << x << endl;
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(func, std::ref(fut));
prom.set_value(144);
t.join();
return 0;
}
- std::packaged_task与std::future配合使用
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
int func(int in) {
return in + 1;
}
int main() {
std::packaged_task<int(int)> task(func);
std::future<int> fut = task.get_future();
std::thread(std::move(task), 5).detach();
cout << "result " << fut.get() << endl;
return 0;
}
三者之间的关系:
std::future用于访问异步操作的结果,而std::promise和std::packaged_task在future高一层,它们内部都有一个future;
promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。
async相关
async是比future,packaged_task,promise更高级的东西,它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async,看一段使用代码:
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
int func(int in) { return in + 1; }
int main() {
auto res = std::async(func, 5);
// res.wait();
cout << res.get() << endl; // 阻塞直到函数返回
return 0;
}
async具体语法如下:
async(std::launch::async | std::launch::deferred, func, args...);
第一个参数是创建策略:
- std::launch::async表示任务执行在另一线程
- std::launch::deferred表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。
如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。
若从 std::async 获得的 std::future 未被移动或绑定到引用,则在完整表达式结尾, std::future的析构函数将阻塞直至异步计算完成,实际上相当于同步操作:
std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始
注意:关于async启动策略这里网上和各种书籍介绍的五花八门,这里会以cppreference为主。
• 有时候我们如果想真正执行异步操作可以对async进行封装,强制使用std::launch::async策略来调用async。
template <typename F, typename... Args>
inline auto ReallyAsync(F&& f, Args&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
}
总结
- std::thread使线程的创建变得非常简单,还可以获取线程id等信息。
- std::mutex通过多种方式保证了线程安全,互斥量可以独占,也可以重入,还可以设置互斥量的超时时间,避免一直阻塞等锁。
- std::lock通过RAII技术方便了加锁和解锁调用,有std::lock_guard和std::unique_lock。
- std::atomic提供了原子变量,更方便实现实现保护,不需要使用互斥量
- std::call_once保证函数在多线程环境下只调用一次,可用于实现单例。
- std::condition_variable提供等待的同步机制,可阻塞一个或多个线程,等待其它线程通知后唤醒。
- std::future用于异步调用的包装和返回值。
- async更方便的实现了异步调用,异步调用优先使用async取代创建线程。
性能优化
内存管理-智能指针
相关信息
- 智能指针是为没有垃圾回收机制的语言解决可能的内存泄露问题的利器,但是在实际应用中使用智能指针有一些需要注意的地方,好在这些问题都可以解决。
- shared_ptr 和 unqi_ptr 使用时如何选择:如果希望只有一个智能指针管理资源或者管理数组,可以用 uniq_ptr;如果希望多个智能指针管理同一个资源,可以用 shared_ptr。
- weak_ptr 是 shared_ptr 的助手,只是监视 shared_ptr 管理的资源是否被释放,本身并不操作或者管理资源。用于解决 shared_ptr 循环引用和返回 this 指针的问题
- 智能指针线程安全
std::shared_ptr
实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。
- 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
- 每次创建类的新对象时,初始化指针并将引用计数置为1
- 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
- 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
- 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
std::unqi_ptr
unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
std::weak_ptr
weak_ptr:弱引用。引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
右值引用和移动语义
移动构造函数 + std::move: 减少内存拷贝
#include <iostream>
using namespace std;
struct A
{
A(){cout<<"construct"<<endl;}
A(const A& a){cout<<"copy construct" <<endl;}
~A(){cout<<"destruct"<<endl;}
};
A GetA(){ return A();}
int main() {
/*
在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GetA()函数内部创建的对象返回后构造一个临时对象产生的,另一次是在main函数中构造a对象产生的。第二次的destruct是因为临时对象在构造a对象之后就销毁了
*/
A a = GetA();
/*
通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构
*/
//A&& a = GetA();
return 0;
}
[root@centos7 c++]# g++ test.cpp
[root@centos7 c++]# ./a.out
construct
destruct
[root@centos7 c++]# g++ test.cpp -fno-elide-constructors
[root@centos7 c++]# ./a.out
construct
copy construct
destruct
copy construct
destruct
destruct
[root@centos7 c++]# g++ test.cpp -fno-elide-constructors
[root@centos7 c++]# ./a.out
construct
copy construct
destruct
destruct
容器新增 操作减少内存拷贝和移动
#include <vector>
#include <map>
#include <string>
#include <iostream>
using namespace std;
struct Complicated
{
int year;
std::string name;
Complicated(int a, string c):year(a),name(c)
{
cout<<"is constucted"<<endl;
}
Complicated(const Complicated&other):year(other.year),name(std::move(other.name))
{
cout<<"is moved"<<endl;
}
};
int main()
{
std::map<int, Complicated> m;
int anInt = 4;
std::string aString = "C++";
cout<<"—insert--"<<endl;
m.insert(std::make_pair(4, Complicated(anInt, aString)));
cout<<"—emplace--"<<endl;
// should be easier for the optimizer
m.emplace(4, Complicated(anInt, aString));
cout<<"--emplace_back--"<<endl;
vector<Complicated> v;
v.emplace_back(anInt, aString);
cout<<"--push_back--"<<endl;
v.push_back(Complicated(anInt, aString));
return 0;
}
[root@centos7 c++]# ./a.out
—insert--
is constucted
is moved
is moved
—emplace--
is constucted
is moved
--emplace_back--
is constucted
--push_back--
is constucted
is moved
is moved
语法糖
可变模块参数
template<class... Types>
void f(Types... args);
f(); // OK: args contains no arguments
f(1); // OK: args contains one argument: int
f(2, 1.0); // OK: args contains two arguments: int and double
auto 自动推导
auto x = 5; // OK: x has type int
const auto *v = &x, u = 6; // OK: v has type const int*, u has type const int
static auto y = 0.0; // OK: y has type double
for range 容器遍历
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (const int& i : v) // access by const reference
std::cout << i << ' ';
lamada 匿名函数
auto f = [&](int)->void{};
f();
enum
#include <iostream>
int main()
{
enum class Color { red, green = 20, blue };
Color r = Color::blue;
switch(r)
{
case Color::red : std::cout << "red\n"; break;
case Color::green: std::cout << "green\n"; break;
case Color::blue : std::cout << "blue\n"; break;
}
// int n = r; // error: no implicit conversion from scoped enum to int
int n = static_cast<int>(r); // OK, n = 21
std::cout << n << '\n'; // prints 21
}