美文网首页
C++线程与智能指针

C++线程与智能指针

作者: Innocencellh | 来源:发表于2019-04-16 15:47 被阅读0次

    线程

    线程,有时被称为轻量进程,是程序执行的最小单元。

    C++11线程

    #include <thread>
    
    void task(int i) {
        cout << "task:" << i << endl;
    }
    
    thread t1(task,100);
    //等待线程结束再继续执行
    t1.join();
    

    POSIX线程

    POSIX 可移植操作系统接口,标准定义了操作系统应该为应用程序提供的接口标准

    Windows上使用 配置: 下载

    cmake_minimum_required (VERSION 3.8)
    include_directories("XXX/pthreads-w32-2-9-1-release/Pre-built.2/include")
    #设置变量为x64 or x86
    if(CMAKE_CL_64)
        set(platform x64)
    else()
        set(platform x86)
    endif()
    link_directories("XXX/pthreads-w32-2-9-1-release/Pre-built.2/lib/${platform}")
    #如果出现 “timespec”:“struct” 类型重定义 设置
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STRUCT_TIMESPEC")
    # 将源添加到此项目的可执行文件。
    add_executable (lsn6example "lsn6_example.cpp" "lsn6_example.h")
    target_link_libraries(lsn6example pthreadVC2)
    

    32位拷贝pthreadVC2.dll 到windows/syswow64目录

    64位拷贝pthreadVC2.dll 到windows/system32目录

    #include <pthread.h>
    void *pthreadTask(void* args) {
        int* i = static_cast<int*>(args);
        cout << "posix线程:" << *i << endl;
        return 0;
    }
    pthread_t pid;
    int pi = 100;
    pthread_create(&pid, 0, pthreadTask, &pi);
    //等待线程的结束
    pthread_join(pid,0);
    

    线程属性

    线程具有属性,用 pthread_attr_t 表示

    pthread_attr_t attr;
    //初始化 attr中为操作系统实现支持的线程所有属性的默认值
    pthread_attr_init(&attr);
    pthread_attr_destroy(&attr);
    
    分离线程

    线程创建默认是非分离的,当pthread_join()函数返回时,创建的线程终止,释放自己占用的系统资源

    分离线程不能被其他线程等待,pthread_join无效,线程自己玩自己的。

    //设置是否为分离线程
    //PTHREAD_CREATE_DETACHED 分离
    //PTHREAD_CREATE_JOINABLE 非分离
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    
    调度策略与优先级

    Windows 无法设置成功

    //设置调度策略 
    //返回0 设置成功
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    // SCHED_FIFO 
    //  实时调度策略,先到先服务 一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。
    // SCHED_RR
    //  实时调度策略,时间轮转 系统分配一个时间段,在时间段内执行本线程
    
    
    //设置优先级
    //获得对应策略的最小、最大优先级
    int max = sched_get_priority_max(SCHED_FIFO);
    int min = sched_get_priority_min(SCHED_FIFO);
    sched_param param;
    param.sched_priority = max;
    pthread_attr_setschedparam(&attr, &param);
    

    线程同步

    多线程同时读写同一份共享资源的时候,可能会引起冲突。需要引入线程“同步”机制,即各位线程之间有序地对共享资源进行操作。

    #include <pthread.h>
    using namespace std;
    
    queue<int> q;
    void *pop(void* args) {
        //线程未同步导致的多线程安全问题
        // 会有重复的数据取出并出现异常
        if (!q.empty())
        {
            printf("取出数据:%d\n", q.front());
            q.pop();
        }
        else {
            printf("无数据\n");
        }
        return 0;
    }
    
    int main()
    {
        for (size_t i = 0; i < 5; i++)
        {
            q.push(i);
        }
        pthread_t pid[10];
        for (size_t i = 0; i < 10; i++)
        {
            pthread_create(&pid[i], 0, pop, &q);
        }
        system("pause");
        return 0;
    }
    

    加入互斥锁

    queue<int> q;
    pthread_mutex_t mutex;
    void *pop(void* args) {
        // 锁
        pthread_mutex_lock(&mutex);
        if (!q.empty())
        {
            printf("取出数据:%d\n", q.front());
            q.pop();
        }
        else {
            printf("无数据\n");
        }
        // 放
        pthread_mutex_unlock(&mutex);
        return 0;
    }
    
    int main()
    {
        //初始化互斥锁
        pthread_mutex_init(&mutex, 0);
        for (size_t i = 0; i < 5; i++)
        {
            q.push(i);
        }
        pthread_t pid[10];
        for (size_t i = 0; i < 10; i++)
        {
            pthread_create(&pid[i], 0, pop, &q);
        }
        //需要释放
        for (size_t i = 0; i < 10; i++)
        {
            pthread_join(pid[i], 0);
        }
        pthread_mutex_destroy(&mutex);
        system("pause");
        return 0;
    }
    

    条件变量

    条件变量是线程间进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立",从而唤醒挂起线程

    template <class T>
    class SafeQueue {
    public:
        SafeQueue() {
            pthread_mutex_init(&mutex,0);
        }
        ~SafeQueue() {
            pthread_mutex_destory(&mutex);
        }
        void enqueue(T t) {
            pthread_mutex_lock(&mutex);
            q.push(t);
            pthread_mutex_unlock(&mutex);
        }
        int dequeue(T& t) {
            pthread_mutex_lock(&mutex);
            if (!q.empty())
            {
                t = q.front();
                q.pop();
                pthread_mutex_unlock(&mutex);
                return 1;
            }
            pthread_mutex_unlock(&mutex);
            return 0;
        }
    
    private:
        queue<T> q;
        pthread_mutex_t mutex;
    };
    

    上面的模板类存放数据T,并使用互斥锁保证对queue的操作是线程安全的。这就是一个生产/消费模式。

    如果在取出数据的时候,queue为空,则一直等待,直到下一次enqueue加入数据。

    这就是一个典型的生产/消费模式, 加入条件变量使 “dequeue” 挂起,直到由其他地方唤醒

    #pragma once
    #include <queue>
    using namespace std;
    
    template <class T>
    class SafeQueue {
    public:
        SafeQueue() {
            pthread_mutex_init(&mutex,0);
            pthread_cond_init(&cond, 0);
        }
        ~SafeQueue() {
            pthread_mutex_destory(&mutex);
            pthread_cond_destory(&cond);
        }
        void enqueue(T t) {
            pthread_mutex_lock(&mutex);
            q.push(t);
            //发出信号 通知挂起线程
            //由系统唤醒一个线程
            //pthread_cond_signal(&cond);
            // 广播 对应多个消费者的时候 多个线程等待唤醒所有
            pthread_cond_broadcast(&cond);
            pthread_mutex_unlock(&mutex);
        }
        int dequeue(T& t) {
            pthread_mutex_lock(&mutex);
            //可能被意外唤醒 所以while循环
            while (q.empty())
            {
                pthread_cond_wait(&cond, &mutex);
            }
            t = q.front();
            q.pop();
            pthread_mutex_unlock(&mutex);
            return 1;
        }
    
    private:
        queue<T> q;
        pthread_mutex_t mutex;
        pthread_cond_t cond;
    };
    
    #include "lsn6_example.h"
    #include <thread>
    #include <pthread.h>
    
    using namespace std;
    #include "safe_queue.h"
    
    SafeQueue<int> q;
    
    void *get(void* args) {
        while (1) {
            int i;
            q.dequeue(i);
            cout << "消费:"<< i << endl;
        }
        return 0;
    }
    void *put(void* args) {
        while (1)
        {
            int i;
            cin >> i;
            q.enqueue(i);
        }
        return 0;
    }
    int main()
    {
        pthread_t pid1, pid2;
        pthread_create(&pid1, 0, get, &q);
        pthread_create(&pid2, 0, put, &q);
        pthread_join(pid2,0);
        system("pause");
        return 0;
    }
    
    

    智能指针

    自C++11起,C++标准库提供了两大类型的智能指针

    shared_ptr

    操作引用计数实现共享式拥有的概念。多个智能指针可以指向相同的对象,这个对象和其相关资源会在最后一个被销毁时释放。

    class A {
    public:
        ~A() {
            cout << "释放A" << endl;
        }
    };
    
    void test() {
        //自动释放 引用计数为1
        shared_ptr<A> a(new A());
        //退出方法 shared_ptr a本身释放,对内部的 A 对象引用计数减1 则为0 释放new 出来的A 对象 
    }
    

    虽然使用shared_ptr能够非常方便的为我们自动释放对象,但是还是会出现一些问题。最典型的就是循环引用问题。

    class B;
    class A {
    public:
        ~A() {
            cout << "释放A" << endl;
        }
        shared_ptr<B> b;
    };
    
    class B {
    public:
        ~B() {
            cout << "释放B" << endl;
        }
        shared_ptr<A> a;
    };
    void test() {
        //自动释放
        shared_ptr<A> a(new A()); //A引用计数为1
        shared_ptr<B> b(new B()); //B引用计数为1
        cout << a.use_count() << endl; //查看内部对象引用计数
        a->b = b;           //A 引用计数为2
        b->a = a;           //B 引用计数为2
        //退出方法,a释放,A引用计数-1结果为1 不会释放 B也一样
    }
    

    weak_ptr

    weak_ptr是为配合shared_ptr而引入的一种智能指针。主要用于观测资源的引用情况。

    它的构造和析构不会引起引用记数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。

    配合shared_ptr解决循环引用问题

    class B;
    class A {
    public:
        ~A() {
            cout << "释放A" << endl;
        }
        weak_ptr<B> b;
    };
    class B {
    public:
        ~B() {
            cout << "释放B" << endl;
        }
        weak_ptr<A> a;
    };
    
    void test() {
        //自动释放
        shared_ptr<A> a(new A()); //A引用计数为1
        shared_ptr<B> b(new B()); //B引用计数为1
    
        a->b = b;           //weak_ptr 引用计数不增加
        b->a = a;           //weak_ptr 引用计数不增加
        //退出方法,A B释放
    }
    

    weak_ptr 提供expired 方法等价于 use_count == 0,当expired为true时,lock返回一个存储空指针的shared_ptr

    unique_ptr

    实现独占式引用,保证同一时间只有一个智能指针指向内部对象。

    unique_ptr<A> a(new A());
    

    auto_ptr已经不推荐使用

    自定义智能指针

    template <typename T>
    class Ptr {
    public:
        Ptr() {
            count = new int(1);
            t = 0;
        }
        Ptr(T *t):t(t) {
            //引用计数为1
            count = new int(1);
        }
        ~Ptr() {
            //引用计数-1 为0表示可以释放T了
            if (--(*count) == 0)
            {
                if (t) {
                    delete t;
                }
                delete count;
                t = 0;
                count = 0;
            }
        }
        //拷贝构造函数
        Ptr(const Ptr<T> &p) {
            //引用计数+1
            ++(*p.count);
            t = p.t;
            count = p.count;
        }
    
        Ptr<T>& operator=(const Ptr<T>& p) {
            ++(*p.count);
            //检查老的数据是否需要删除
            if (--(*count) == 0) {
                if (t) {
                    delete t;
                }
                delete count;
            }
            t = p.t;
            count = p.count;
            return *this;
        }
        //重载-> 操作T 类
        T* operator->() { return t; }
    
    private:
        T *t;
        int *count;
    };
    

    重载=为什么返回引用,而不是对象?

    return *this后马上就调用拷贝构造函数,将*this拷贝给一个匿名临时对象,然后在把临时对象拷贝给外部的左值(a=b,a为左值),再释放临时对象。这样首先会造成不必要的开销。

    同时如果没有自定义的拷贝函数,则会使用默认的浅拷贝。如果类中存在指向堆空间的成员指针,需要进行深拷贝(重新申请内存),避免相同内存地址被其他地方释放导致的问题。

    如果此处返回对象不会导致出现问题:

    第四节课中提到:

    引用类型(Test1&) 没有复制对象 返回的是 t 对象本身 t会被释放 所以会出现问题(数据释放不彻底就不一定)

    这里的t作用域是函数内我们自己创建的一个临时对象,因此会被释放,而在此处返回 *this,作用范围和t不一样。其次,在类中定义了拷贝函数,虽然未进行深拷贝,但小心的维护了堆内存指针(t、count),不会出现堆内存释放导致的悬空指针情况。

    部分C++11、14特性

    nullptr

    nullptr 出现的目的是为了替代 NULL。 C++11之前直接将NULL定义为 0。

    void test(int* i){
        
    }
    void test(int i){
        
    }
    //现在调用哪一个test? test(int)
    test(NULL);
    //调用test(int* i)
    test(nullptr); 
    

    类型推导

    C++11 重新定义了auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。

    auto i = 5;             // i 被推导为 int
    auto p = new auto(10) // arr 被推导为 int *
    //但是auto不能作用在数组上
    auto arr1[10] = { 0 }; //错误
    auto arr2= {0}; //正确
    typeid(arr2).name() //获得类型名为 initializer_list(后续介绍)
    // int  j
    decltype(i) j = 10;
    

    基于范围的 for 循环

    实际上就是foreach

    vector<int> vec = { 1,2,3,4,5 };
    //配合auto使用
    for(auto i : vec)
    {
        cout << i << endl;
    }
    

    Lambda

    匿名函数,即没有函数名的函数

    完整形式:

    [捕获外部变量列表 ] (参数列表) mutable exception->返回类型 { 函数体 }

    mutable:在外部变量列表以值来捕获时,无法修改变量的值,加上mutable表示可修改(不会影响外部变量)

    auto i = 5;
    // [&] 表示外部变量都以引用的形式在lambda中使用,函数内部修改i的值会影响外部
    // 这里的 -> auto 自动推导在c++11不支持,c++14中对auto进行了扩展
    thread t1([&] () -> auto {
        i = 100;
        cout << "线程:" << i  << endl;
    });
    _sleep(10);
    cout << i << endl;
    
    捕获形式 说明
    [] 不捕获任何外部变量
    [i, …] 以值得形式捕获指定的多个外部变量(用逗号分隔);如果引用捕获,需要显示声明&
    [this] 以值的形式捕获this指针
    [=] 以值的形式捕获所有外部变量
    [&] 以引用形式捕获所有外部变量
    [=, &x] 变量x以引用形式捕获,其余变量以传值形式捕获
    [&, x] 变量x以值的形式捕获,其余变量以引用形式捕获

    相关文章

      网友评论

          本文标题:C++线程与智能指针

          本文链接:https://www.haomeiwen.com/subject/minuwqtx.html