多线程与智能指针
C++线程与智能指针
线程
线程,有时被称为轻量进程,是程序执行的最小单元。
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")#这里的 -D 就是define # 将源添加到此项目的可执行文件。 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> pthread_create (thread, attr, start_routine, arg)
参数 | 描述 |
---|---|
thread | 指向线程标识符指针。 |
attr | 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。 |
start_routine | 线程运行函数起始地址,一旦线程被创建就会执行。 |
arg | 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。 |
创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败。
终止线程
使用下面的程序,我们可以用它来终止一个 POSIX 线程:
#include <pthread.h> pthread_exit (status)
实例
#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, ¶m);
线程同步
多线程同时读写同一份共享资源的时候,可能会引起冲突。需要引入线程“同步”机制,即各位线程之间有序地对共享资源进行操作。
#include <pthread.h> #include <queue> 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() { //相对列塞了5条数据 for (size_t i = 0; i < 5; i++) { q.push(i); } //创建10条线程 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) { // 锁,相当于java的synchronized,悲观锁 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以值的形式捕获,其余变量以引用形式捕获 |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!