c++11高级用法(建议收藏)

Thread

c++语言级别的多线程网络编程代码可以跨平台Window/Linux.在C++11标准当中对线程支持了跨平台是的C++在并发编程时不需要依赖第三方库。并且还引入了atomic原子类。

创建线程

首先我们来学习一下如何创建这个线程,在C++当中如果我们想要创建线程我们需要包含这个头文件 thread.下面我们演示一下定义线程对象的几种方式

thread t1
thread t2(threadFunc)

下面我们分别用这两种方式启动一个线程吧!。首先了第一种方式并没有绑定任何一个线程函数可以任务没有启动这个线程。此时我们可以利用thread类给我们提供的 移动构造或者 移动赋值。注意thread类是不支持这个 左值引用的拷贝构造和赋值的。在这里我们使用一个匿名对象就可以了

void threadFunc(int time) {
    cout << "hello thread" << endl;
    cout << this_thread::get_id() << endl;
    this_thread::sleep_for(std::chrono::seconds(time));

}
int main()
{
    thread t;
    t = thread(threadFunc, 5);

    t.join();
    return 0;
}

在这里需要注意的是这个主线程运行完毕之后如果当前进程还有未完成的线程那么进程就会异常终止。

2.下面我们来看看这个带参数的构造函数,这个就可以绑定这个线程函数了

void threadFunc(int time) {
    cout << "hello thread" << endl;
    cout << this_thread::get_id() << endl;
    this_thread::sleep_for(std::chrono::seconds(time));

}
int main()
{
    thread t(threadFunc, 3);

    t.join();
    return 0;
}

此时这个线程对象与这个线程函数就绑定起来了。至于线程的概念可以参考这个博主的其他博客。

thread类的方法

下面我们介绍一下这个thread类当中常见的几个方法,并不会全部都介绍一遍。如果感兴趣的老铁可以自行百度。

join
detach
get_id()

下面说一下获取线程id的方式。首先我们可以调用thread类当中的get_id函数获取线程的id,但是这个方法必须是线程对象来进行调用。如果我们不想使用对象并且我们还想要获取这个线程id我们可以使用this_thread命名空间下的get_id()进行获取。下面我们通过一段演示一下。

void threadFunc(int time) {

    cout << this_thread::get_id() << endl;
    this_thread::sleep_for(std::chrono::seconds(time));

}
int main()
{
    thread t(threadFunc, 3);
    cout << t.get_id() << endl;

    t.join();
    return 0;
}

下面再介绍一下this_thread命名空间当中的其他函数

this_thread::yield
this_thread::sleep_until
this_thread::sleep_for

再这里我们需要使用chrono类当前的一些和时间类相关的类进行传递参数

谈谈join&detach

我们再前面说过这个这个创建一个线程之后,创建它的这个线程需要调用join方法进行等待,对线程资源进行回收此时这个线程也会被阻塞再这里。否则我们的程序就会挂掉,如果我们不想使用join那么我们可以使用detach将线程进行分离。

void threadFunc(int time) {

    cout << this_thread::get_id() << endl;
    this_thread::sleep_for(std::chrono::seconds(time));

}
int main()
{
    thread t(threadFunc, 3);
    cout << t.get_id() << endl;

    t.detach();
    return 0;
}

但是需要注意的是主线程执行完成整个进程就结束了,有可能子线程里的任务就执行不了了。这是我们使用detach特别需要注意的地方。

mutex&lock_guard&unique_lock

下面我们来介绍一下c++11当中的互斥量。mutex这种锁是c++11提供的最基本的互斥量,并且 mutex对象之间不能相互拷贝,也不能移动这是特别需要注意的。mutex的主要的成员方法如下:

lock
unlock
try_lock

和我们在Linux下学习的锁一样。如果这把锁已经被这个其他线程占有了,此时当前线程又尝试进行获取锁那么当前线程就会被挂起。如果同一个线程调用了两次lock中间没有使用这个unlock那么就会导致这个死锁。
下面我们来举一个加锁的列子来演示一下:我们创建两个线程让他们的入口函数相同打印1到100

void threadFunc() {

    for (int i = 0; i < 100; i++) {

        cout << "线程:" << this_thread::get_id() << "打印:" << i << endl;
        this_thread::sleep_for(chrono::seconds(1));

    }

}
int main()
{
    thread t1(threadFunc);
    thread t2(threadFunc);
    t1.join();
    t2.join();
}

此时我们运行会发现这个结果是杂乱无章的。此时我们如果想要让一个线程打印完成之后下一个线程在打印我们可以使用这个mutex进行加锁。

void threadFunc(mutex&mtx) {
    mtx.lock();
    for (int i = 0; i < 100; i++) {

        cout << "线程:" << this_thread::get_id() << "打印:" << i << endl;
        this_thread::sleep_for(chrono::seconds(1));

    }
    mtx.unlock();
}
int main()
{
    mutex mtx;
    thread t1(threadFunc,mtx);
    thread t2(threadFunc,mtx);
    t1.join();
    t2.join();
    return 0;
}

为什么我们不定义这个全局的锁了?这是因为全局变量如果在头文件当中在多个地方包含容易造成这个重定义的问题。

2.下面我们来谈一下这个lock_guard和unique_lock。
首先我们知道,我们在使用互斥锁时。如果加锁的范围太大我们久很有可能忘记解锁了,那么此后申请这个锁的线程就被阻塞住了。也就是死锁了。我们举个列子来说明一下吧。这是一段伪代码

void threadFunc(mutex&mtx) {
    mtx.lock();
    FILE* fp = fopen("a.txt", "w");
    if (fp == nullptr)
    {
        return;
    }

    mtx.unlock();

}

因此在使用互斥锁时如果我们控制不好就很容易导致这个死锁。因此C++11采用这个RAII的思路尝试解决这个问题,使用RAII对这个互斥锁进行封装。出现了这个lock_guard和这个unique_lock.下面我们来谈一下这个lock_guard.

lock_guard是这个C++11当中的类模板。通过RAII的方式对这个互斥锁进行封装。我们只需要加加锁的时候使用互斥锁实例化一个lock_guard对象。在它的构造函数当中会加锁,在这个析构函数里面会自动解锁。

void threadFunc(mutex&mtx) {
    lock_guard<mutex>lock(mtx);
    FILE* fp = fopen("a.txt", "w");
    if (fp == nullptr)
    {
        return;
    }

}

此时就不会出现那么死锁的情况对象的生命周期到了调用析构函数,析构函数自动解锁了。从lock_guard对象的定义出到lock_guard对象的析构处在这段范围内都在互斥锁的保护范围内。

2.unique_lock
相信老铁们都能发现上面那个也太单一了吧,用户不能对锁进行控制,因此C++11又提高了unique_lock.它和lock_guard非常的相似也是采用RAII的思想对锁进行封装。在构造unqiue_lock对象的时候也会进行加锁,unique_lock对象析构的时候也会解锁。但是与lock_guard不同的是unique_lock更加的灵活可以手动的进行解锁。所以了unique_lock也进程配合这个条件变量进行使用。

条件变量(condition_variable)

条件变量condition_variable提供了的成员函数当中,有这个wait系列,notify系列。相信大家在这个Linux当中也应该了解过了这个条件变量。语言当中的条件变量就是对这个系统调用的封装。下面在这里稍微解释一下这两个函数。
1.wait,在这个条件变量下进行等待。使用它时需要将互斥锁传入并且必须是unique_lock。
wait_for和wait_until函数的使用方式与wait函数类似:
2.wait_for函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个时间段,表示让线程在该时间段内进行阻塞等待,如果超过这个时间段则线程被自动唤醒。
3.wait_until函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个具体的时间点,表示让线程在该时间点之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒。
4.线程调用wait_for或wait_until函数在阻塞等待期间,其他线程调用notify系列函数也可以将其唤醒。此外,如果调用的是wait_for或wait_until函数的第二个版本的接口,那么当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么当前线程还需要继续被阻塞。

下面我们通过一个案例来进行演示条件变量的使用:使用两个线程交替打印奇数和偶数

int main()
{
    mutex mtx;
    int num = 1;
    condition_variable odd;
    condition_variable eve;
    thread t1([&]() {
        while (num100)
        {
            unique_lock<mutex>lock(mtx);
            cout << this_thread::get_id() << ":" << num << endl;
            num++;

            eve.notify_one();
            odd.wait(lock);

        }

        eve.notify_one();
    });
    thread t2([&]() {
        while (num100)
        {
            unique_lock<mutex>lock(mtx);
            cout << this_thread::get_id() << ":" << num << endl;
            num++;

            odd.notify_one();
            eve.wait(lock);

        }
        });
     t1.join();
     t2.join();
     return 0;

}

上述就是这个条件变量进行线程间同步与互斥操作。以及这个线程间通信机制。在这里需要注意的是当eve线程将100打印之后。再天条件变量下等待此时奇数线程发现已经不小于100了退出了此时需要通知一下eve线程否则就会造成整个eve线程一直阻塞再那里。

总结:

1.unique_lock 它不紧可以使用在简单的临界区代码的互斥操作当中,还可以用在函数调用过程
所以它可以配合条件变量使用,unique_lock提供了右值引用的拷贝构造和赋值。
2. lock_gurad不可以手动释放,当然不能使用函数参数传递或者返回过程,因为它的拷贝构造被删除了所以只能用于简单的代码段互斥中。
3. cv.notify_all 通知在cv上等待的所有线程,条件成立了可以起来干活了, 其他在cv上等待的线程收到了通知从等待状态=>阻塞状态=>获取互斥锁=>线程继续执行。

包装器function

接下来我们来看看这个包装器,function是一种函数包装器。它可以对可调用对象进行包装。再C++11当中其实这个function其实就是一个这个类模块而已。
function包装器可以对可调用对象进行包装,包括函数指针,仿函数(函数对象),类的成员函数。下面我们用一下列子来看一下如何来使用这个function

int f(int a, int b)
{
    return a + b;
}
struct Functor
{
public:
    int operator()(int a, int b)
    {
        return a + b;
    }
};
class Plus
{
public:
    static int plusi(int a, int b)
    {
        return a + b;
    }
    double plusd(double a, double b)
    {
        return a + b;
    }
};
int main()
{

    function<int(int, int)> func1 = f;
    cout << func1(1, 2) << endl;

    function<int(int, int)> func2 = Functor();
    cout << func2(1, 2) << endl;

    function<int(int, int)> func3 = [](int a, int b){return a + b; };
    cout << func3(1, 2) << endl;

    function<int(int, int)> func4 = &Plus::plusi;
    cout << func4(1, 2) << endl;

    function<double(Plus, double, double)> func5 = &Plus::plusd;
    cout << func5(Plus(), 1.1, 2.2) << endl;
    return 0;
}

再这里需要注意的是

  • 包装非静态的成员函数时需要注意,非静态成员函数的第一个参数是隐藏this指针,因此在包装时需要指明第一个形参的类型为类的类型。
  • 包装时指明返回值类型和各形参类型,然后将可调用对象赋值给function包装器即可,包装后
  • function对象就可以像普通函数一样使用了。
    取静态成员函数的地址可以不用取地址运算符”&”,但取非静态成员函数的地址必须使用取地址运算符”&”。

下面我们来看看这个包装器再这个底层是如何实现的,其实非常的简单

template<class Fty>
class myfunction
{};

template<class R,class ... A>
class myfunction<R(A...)>
{
public:
    using PFUNC = R(*)(A...);
    myfunction(PFUNC pfunc):_pfunc(pfunc)
    {}
    R operator()(A... arg)
    {
        return _pfunc(arg...);
    }
private:
    PFUNC _pfunc;
};
int main()
{

    myfunction<void(string)>func = Hello;
    func("hello");
    return 0;
}

其底层就是调用了这个operator()然后再调用了包装后的那个函数再这里可能有老铁看不懂这个模块的特化。下面那个类是特例化了一个函数对象类型注意和(函数指针类型进行区别)特例化了一个返回值为这个 R,参数为A…的类。再这里是需要注意的,感觉很牛逼其实底层也不是那么的复杂

总结:

  • 将可调用对象进行统一管理,便于我们对其统一化管理
  • 包装之后明确了可调用对象的返回值和类型让使用者使用起来更加的方便

绑定器bind

bind其实也是一种函数包装器可以接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表,C++当中的bind本质也是一个函数模板。bind绑定器返回的结果函数一个函数对象,下面我们来演示一下bind的使用,其实了一遍它是和function一起使用做这个回调

#include
#include
#include
using namespace std;

void Hello1(string str) {
    cout << str << endl;
}
int sum(int a, int b) { return a + b; }
class Test
{
public:
    int sum(int a, int b) { return a + b; };
};

int main()
{

    bind(Hello1, "hello word");
    cout << bind(sum, 10, 20)() << endl;

    cout << bind(&Test::sum, Test(), 20, 30)() << endl;

    cout<<bind(sum, placeholders::_1, placeholders::_2)(1, 2) << endl;

    return 0;
}

总结
bind包装器的意义在于可以将一个函数的某些参数绑定为固定的值让我们再调用的时候不需要传递这个参数值。可以对函数参数的顺序灵活调整。

make_shared

make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;由于是通过shared_ptr管理内存,因此一种安全分配和使用动态内存的方法。下面我们来看看这个make_shared如何使用


shared_ptr<string> p1 = make_shared<string>(10, '9');

shared_ptr<string> p2 = make_shared<string>("hello");

shared_ptr<string> p3 = make_shared<string>();

C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr, 那与 std::shared_ptr 的构造函数相比它能给我们带来什么好处呢 ?

首先是这个内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.shared_ptr可以需要new两次一次是这个需要管理的资源,第二次是这个底层的引用计数。
而这个make_shared一次性将需要管理的资源和这个引用计数的资源都开辟好了,减少了内存的分配次数。

第二个好处就是防止这个资源泄漏,下面我们来看一段代码


void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) {  }
F(std::shared_ptr<Lhs>(new Lhs("foo")),
std::shared_ptr<Rhs>(new Rhs("bar")));

C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:

1.new Lhs(“foo”))
2.new Rhs(“bar”))
3.std::shared_ptr
4.std::shared_ptr
好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.

此时我们用这个make_shared就非常的舒服:

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

但是这也带来了不好的地方:
1.无法像shared_ptr那样自定义删除器
2.会导致资源延迟释放weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题.

Original: https://blog.csdn.net/qq_56999918/article/details/127819363
Author: 一个山里的少年
Title: c++11高级用法(建议收藏)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/659998/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球