C++高级教程(下)
# 信号处理
信号通常由操作系统的内核在发生检查到异常的时候发出的信号
有些信号可能会直接终中断当前的程序进程,开发人员可以在适当的时机处理对应异常情况
在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断
在移动端通常需要开发人员提前处理,否则可能导致程序的直接奔溃
# 常见异常信号
头文件 csignal 中下表所列信号可以在程序中捕获
信号 | 描述 |
---|---|
SIGABRT | 程序的异常终止,如调用 abort。 |
SIGFPE | 错误的算术运算,比如除以零或导致溢出的操作。 |
SIGILL | 检测非法指令。 |
SIGINT | 接收到交互注意信号。 |
SIGSEGV | 非法访问内存。 |
SIGTERM | 发送到程序的终止请求。 |
# 捕获异常信号
signal() 函数
C++ 信号处理库提供了 signal 函数,用来捕获突发事件:
/// 第一个参数:是一个整数,代表了信号的编号
/// 第二个参数:是一个指向信号处理函数的指针
void (*signal (int sig, void (*func)(int)))(int);
模拟捕获异常信息示例:
#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "捕获到异常信息类型值: (" << signum << ") .\n";
// 终止程序
exit(signum);
}
int main ()
{
// 注册信号处理程序
signal(SIGABRT, signalHandler);
signal(SIGFPE, signalHandler);
signal(SIGILL, signalHandler);
signal(SIGINT, signalHandler);
signal(SIGSEGV, signalHandler);
signal(SIGTERM, signalHandler);
cout << "计算 5/0 = " <<endl;
cout << 5/0 << endl;
return 0;
}
上面的代码分母=0时计算异常结果:
计算 5/0 =
捕获到异常信息 (8) .
main.cpp: In function ‘int main()’:
main.cpp:26:14: warning: division by zero [-Wdiv-by-zero]
26 | cout << 5/0 << endl;
| ~^~
Exited with error status 8
信号SIGFPE:
FPE是floating-point exception(浮点异常)的首字母缩略字。在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。SIGFPE的符号常量在头文件signal.h中定义。
错误的算术操作 SA SIGINFO宏:
FPE INTDIV 整数除以零
FPE INTOVF 整数上溢
FPE FLTDIV 浮点除以零
FPE FLTOVF 浮点上溢
FPE FLTUND 浮点下溢
FPE FLTRES 浮点结果不准
FPE FLTINV 无效浮点操作
FPE FLTSUB 浮点下标越界
# 抛出异常信号
raise() 函数抛出异常信号如下:
int raise (signal sig);
示例:
#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "捕获到异常信息类型值: (" << signum << ") .\n";
// 终止程序
exit(signum);
}
int main ()
{
// 注册算术运算错误 SIGFPE 信号处理程序
signal(SIGFPE, signalHandler);
sleep(1);
// 手动抛出算术运算错误异常
raise(SIGFPE);
return 0;
}
结果:
捕获到异常信息类型值: (8) .
Exited with error status 8
更多异常参考signal 文件。
下面 linux—signal信号:
linux_signal信号 | ||
---|---|---|
SIGHUP | SIGHUP,hong up ,挂断。本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。 登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和 后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也 能继续下载。 此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。 | |
SIGINT | 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。 | |
SIGQUIT | 和SIGINT类似, 但由QUIT字符(通常是Ctrl+)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号 | |
SIGILL | SIGILL,illeage,非法的。执行了非法指令, 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出也有可能产生这个信号。 | |
SIGTRAP | 由断点指令或其它陷阱(trap)指令产生. 由debugger使用 | |
SIGABRT | 调用abort函数生成的信号。 | |
SIGBUS | 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间) | |
SIGFPE | FPE是floating-point exception(浮点异常)的首字母缩略字。在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。SIGFPE的符号常量在头文件signal.h中定义。 在这里插入图片描述 | |
SIGKILL | 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号,终极大招。 | |
SIGUSR1 | 留给用户使用 | |
SIGSEGV | 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据 | |
SIGUSR2 | 留给用户使用 | |
SIGPIPE | 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止 | |
SIGALRM | 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号 | |
SIGTERM | 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。 | |
SIGCHLD | 子进程(child)结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这 时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。 | |
SIGCONT | 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符 | |
SIGSTOP | 暂停(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略 | |
SIGTSTP | 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号 | |
SIGTTIN | 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行. Unix环境下,当一个进程以后台形式启动,但尝试去读写控制台终端时,将会触发SIGTTIN(读)和SIGTTOU(写)信号量,接着,进程将会暂停(linux默认情况下),read/write将会返回错误。这个时候,shell将会发送通知给用户,提醒用户切换此进程为前台进程,以便继续执行。由后台切换至前台的方式是fg命令,前台转为后台则为CTRL+Z快捷键。 那么问题来了,如何才能在不把进程切换至前台的情况下,读写控制器不会被暂停?答案:只要忽略SIGTTIN和SIGTTOU信号量即可:signal(SIGTTOU, SIG_IGN)。 stty stop/-stop命令是用于设置收到SIGTTOU信号量后是否执行暂停,因为有些系统的默认行为不一致,比如mac是默认忽略,而linux是默认启用。stty -a可以查看当前tty的配置参数。 在这里插入图片描述 | |
SIGTTOU | 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到。具体见上面SIGTTIN | |
SIGURG | SIGURG, urgent, 紧急的。有”紧急”数据或out-of-band数据到达socket时产生 | |
SIGXCPU | 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。 | |
SIGXFSZ | 当进程企图扩大文件以至于超过文件大小资源限制 | |
SIGVTALRM | 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间. | |
SIGPROF | 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间 | |
SIGWINCH | Windows Change, 窗口大小改变时发出. | |
SIGIO | 文件描述符准备就绪, 可以开始进行输入/输出操作. | |
SIGPWR | Power failure | |
SIGSYS | 非法的系统调用。 |
1、程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
2、不能恢复至默认动作的信号有:SIGILL,SIGTRAP
3、默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
4、默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
5、默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
6、默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
7、此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞
# 多线程
需要引入头文件 thread
https://www.zhihu.com/question/36236334
# 一些概念
多线程中一些概念词汇容易被误解,这里解释一下
# 核
核 :指的是cpu芯片集成到 运算核心 模块。
单核:就是cpu集成单个运算核心
多核:就是多个运算核心,显然多核的运算能力更强。
# 并发
并发就是一段时间内来回执行不同的任务,同一时刻只能执行一个任务,但是将时间拆分很多分,每个任务轮流执行(如果有优先级,那么优先级高的任务将获取到更多时间,也优先被处理)
# 并行
并行是在多核cpu芯片下,同一时刻同时运行多个任务(并发并非真正多任务,它只是时间拆分细,看着像同时执行)。
cpu集成单个运算核心模块越多,并行任务数越多,能力越强
# 多进程
系统将每个程序都单独分配了独立的内存空间
操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据
通常一个软件分配一个独立的进程,并有唯一的进程ID。除非你杀掉这个进程,否则其他程序无法分配这个进程ID。
# 进程间通讯
软件之间也可能存在业务交互需求,那么软件之间的通讯也就同 进程 之间相互通信一样,由于系统对进程间通信管理严格,通常需要使用系统指定的api。
在进程间的通信,可以使用信号、套接字,文件、管道等方式,这个操作相对比较消耗资源,不适合大量高频任务
# 线程
一个程序至少一个进程,一个进程至少一个线程(主线)
当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,该线程通常叫作程序的主线程(Main Thread)。
任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程。
线程是参与系统调度的最小单位。它被包含在进程中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
其它的线程通常由主线程创建,也称为主线程的子线程。所以主线程通常会在最后结束运行,执行各种清理工作(如为子线程收尸)。
# 多线程
多个系统任务调度逻辑单元,也就是多个任务被分配给多个任务调度员去管理,而不是一个管理员
通常说的多线程都是只在某一个进程中的多个任务调度
# 创建线程
c++ 中引入线程库 thread
示例:
#include <iostream>
#include <thread>
using namespace std;
void thread_call_me(int x)
{
cout<< "子线程调用:" << x <<endl;
}
int main()
{
std::cout << "main:主线程\n";
thread first (thread_call_me,1); // 开启线程, 调用 thread_call_me
thread second (thread_call_me,2); // 开启线程
thread third (thread_call_me,3);
// join:等待启动的线程执行完成,才会继续往下执行。
first.join();
second.join();
third.join();
//必须join完成
std::cout << "子线程结束.\n";
return 0;
}
结果:
main:主线程
子线程调用:1
子线程调用:3
子线程调用:2
子线程结束.
# 编译命令
不用环境和库在编译时都可能存在区别,需要根据情况处理,包括兼容性
本地运行
⚠️⚠️⚠️ 请添加编译参数 -lpthread 执行命令,否则会失败:
$ g++ thread.cpp -lpthread -o thread.o
./thread.o
// 或
g++ test.cc -o test -l pthread
⚠️⚠️⚠️ 如果你是在Mac 电脑系统运行,需要指定c++版本,因为从c++11以后才支持多线程
$ g++ -std=c++11 thread.cpp -lpthread -o thread.o
$ ./thread.o
在线编译: https://www.onlinegdb.com
⚠️⚠️⚠️ 其中设置选择 Command line arguments: 里 填 -lpthread
# join与detach
1、join方式,等待启动的线程完成,才会继续往下执行。
2、detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
如果把上面示例中的相关修改:
first.join();
second.join();
third.join();
删除两行,改为:
second.join();
运行结果:
main:主线程
子线程调用:2
子线程结束.
terminate called without an active exception
显然部分子线程没有设置执行方式,程序出行异常
如果不想等待,也需要写明,使用detach:
first.detach(); // 不需要等待该子线程
second.join(); // 需要等待子线程2执行完成
third.detach(); // 不需要等待该子线程
结果:
main:主线程
子线程调用:3
子线程调用:1
子线程调用:2
��线程结束.
// 再试一次:
main:主线程
子线程调用:2
子线程调用:3
子线程结束.
每次结果可能都不一样。但是至少 second thread 一定会执行完成。
# this_thread
this_thread是一个类,它有4个功能函数,具体如下:
函数 | 使用 | 说明 |
---|---|---|
get_id | std::this_thread::get_id() | 获取线程id |
yield | std::this_thread::yield() | 放弃线程执行,回到就绪状态 |
sleep_for | std::this_thread::sleep_for(std::chrono::seconds(1)); | 暂停1秒 |
sleep_until | 如下 | 一分钟后执行吗,如下 |
使用前面的示例演示获取线程id:
#include <iostream>
#include <thread>
using namespace std;
void thread_call_me(int x)
{
cout<< "子线程调用:" << x << "; 线程id = " << std::this_thread::get_id() <<endl;
}
int main()
{
std::cout << "main:主线程" << "; 线程id = " << std::this_thread::get_id() <<endl;
thread first (thread_call_me,1); // 开启线程, 调用 thread_call_me
thread second (thread_call_me,2); // 开启线程
thread third (thread_call_me,3);
first.join(); // 需要等待该子线程
second.join(); // 需要等待子线程2执行完成
third.join(); // 需要等待该子线程
std::cout << "子线程结束.\n";//必须join完成
return 0;
}
结果(请添加编译参数 -lpthread 执行命令)
main:主线程; 线程id = 140085890111296
子线程调用:2; 线程id = 140085881714240
子线程调用:1; 线程id = 140085890106944
子线程调用:3; 线程id = 140085873321536
子线程结束.
# 锁(多线程)
1、常规互斥锁mutex可以使用lock对某一个子线程进行任务加锁,直到任务执行完成,加锁期间资源不会被其他线程修改,直到unlock。
2、还有一种场景是有多个子线程,这些线程存在依赖关系,比如 一个线程需要等其他某些线程执行完成后再进行其他逻辑,这样单纯lock无法满足。那么可以使用condition_variable 和 condition_variable_any
# 互斥锁mutex
如下表所示。
类型 | 说明 |
---|---|
std::mutex | 最基本的 Mutex 类。 |
std::recursive_mutex | 递归 Mutex 类。 |
std::time_mutex | 定时 Mutex 类。 |
std::recursive_timed_mutex | 定时递归 Mutex 类。 |
std::mutex 是C++11 中最基本的互斥量,供了独占所有权的特性
std::recursive_lock 则可以递归地对互斥量对象上锁。
- lock():资源上锁
- unlock():解锁资源
- trylock():查看是否上锁
# 1、lock与unlock
mutex常用操作:
- lock():资源上锁
- unlock():解锁资源
- trylock():查看是否上锁
示例:
1、多任务不加锁:
#include <iostream>
#include <thread>
void funpri (int n, char c)
{
for (int i=0; i<n; ++i)
{
sleep(0.1); // 模拟耗时操作
std::cout << c;
}
}
int main ()
{
std::thread th1 (funpri,10,'1');//线程1:打印1
std::thread th2 (funpri,10,'0');//线程2:打印0
th1.join();
th2.join();
return 0;
}
结果:(程序并没有先打印10个1,然后10个0,而是乱序的)(请添加编译参数 -lpthread 执行命令)
10101010101010101010
2、多任务加锁:
现在用一个互斥锁,所以第一个调用结束后才会执行下一次任务
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
std::mutex mtx; // 声明互斥锁
void safe_fun (int n, char c)
{
mtx.lock(); // 加锁
for (int i=0; i<n; ++i)
{
sleep(0.1); // 模拟耗时操作
std::cout << c;
}
mtx.unlock(); // 解锁
}
int main ()
{
std::thread th1 (safe_fun,10,'1');//线程1:打印1
std::thread th2 (safe_fun,10,'0');//线程2:打印0
th1.join();
th2.join();
return 0;
}
输出:符合结果,先10个1,再10个0(请添加编译参数 -lpthread 执行命令)
11111111110000000000
# 2、lock_guard
作用域锁:加锁后不能手动unlock解锁,而是要等到加锁代码作用域结束自动解锁
示例:
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex; // protects g_i,用来保护g_i
void safe_fun (int n)
{
const std::lock_guard<std::mutex> lock(g_i_mutex); // 加锁
for (int i=0; i<n; ++i)
{
sleep(0.1); // 模拟耗时操作
std::cout << std::this_thread::get_id() << "; count = " << g_i << '\n';
++g_i;
}
std::cout << '\n';
// 解锁代码不需要,这里加锁作用域结束会自动释放锁
}
int main(){
std::cout << "main id: " <<std::this_thread::get_id()<<std::endl;
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_fun, 5);
std::thread t2(safe_fun, 5);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
Result: (请添加编译参数 -lpthread 执行命令)
main id: 140505467811648
main: 0
140505467807296; count = 0
140505467807296; count = 1
140505467807296; count = 2
140505467807296; count = 3
140505467807296; count = 4
140505459414592; count = 5
140505459414592; count = 6
140505459414592; count = 7
140505459414592; count = 8
140505459414592; count = 9
main: 10
# 3、unique_lock
unique_lock:
1、可加锁
2、可以创建的时候不加锁,稍后再加锁
3、可解锁
4、作用域结束后自动解锁
#include <mutex>
#include <thread>
#include <iostream>
int g_i_1 = 0;
std::mutex g_i_mutex;
void safe_fun(int n)
{
// defer_lock: unlock,默认自动加锁
std::unique_lock<std::mutex> lock1(g_i_mutex, std::defer_lock);
lock1.lock(); // 多个使用 std::lock(lock1, lock2);
for (int i=0; i<n; ++i)
{
sleep(0.1); // 模拟耗时操作
std::cout << std::this_thread::get_id() << "; g_i_1 = " << g_i_1 << '\n';
++g_i_1;
}
}
int main()
{
std::thread t1(safe_fun, 3);
std::thread t2(safe_fun, 3);
t1.join();
t2.join();
return 0;
}
result:(请添加编译参数 -lpthread 执行命令)
139727387924032; g_i_1 = 0
139727387924032; g_i_1 = 1
139727387924032; g_i_1 = 2
139727379531328; g_i_1 = 3
139727379531328; g_i_1 = 4
139727379531328; g_i_1 = 5
# 死锁
1、死锁的形成场景:
1)忘记释放锁:在申请锁和释放锁之间直接return
2)单线程重复申请锁:一个线程,刚出临界区,又去申请资源。
3)多线程多锁申请:两个线程,两个锁,他们都已经申请了一个锁了,都想申请对方的锁
4)环形锁的申请:多个线程申请锁的顺序形成相互依赖的环形
2、产生死锁的必要条件:
1)互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2)请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3)不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
3、解决死锁的基本方法:
1)一次性分配完所有资源,这样就不会再有请求了:(破坏请求条件)
2)当进程阻塞时,释放所持有的资源(破坏请保持条件)
3)资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
# 条件变量(线程)
文件定义:
https://www.apiref.com/cpp-zh/cpp/header/condition_variable.html
condition_variable :
1、必须结合unique_lock使用
2、条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。
3、是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。
condition_variable_any :
condition_variable_any可以使用任何的锁,mutex 即可。
类型 | 说明 |
---|---|
condition_variable | 构建对象 |
析构 | 删除 |
wait | Wait until notified |
wait_for | Wait for timeout or until notified |
wait_until | Wait until notified or time point |
notify_one | 解锁一个线程,如果有多个,则未知哪个线程执行 |
notify_all | 解锁所有线程 |
cv_status | 这是一个类,表示variable 的状态,如下所示 |
enum class cv_status { no_timeout, timeout };
# 1、wait
实现一个功能:任务A 依赖任务B、C、D都完成后再执行, 其中 B、C、D 可乱序
代码示例:
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量(信号)通知能力
int semaphore_num = 3; // 信号量(标记状态, 大于0时阻塞线程)
// 判断当前是否阻塞线程(wait 需要使用此函数作参数)
bool shipment_available()
{
return semaphore_num<=0;
}
// 任务函数,每次执行后信号量-1
void work_s(const char* t)
{
std::this_thread::yield();
std::unique_lock<std::mutex> lck(mtx);
semaphore_num-=1;
std::cout << "执行任务 " << t << '\n';
cv.notify_one();
}
// 任务通知,最后执行的任务,当状态非阻塞时执行任务
void work_notify(const char* t)
{
std::unique_lock<std::mutex> lck(mtx);//自动上锁
//第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
cv.wait(lck,shipment_available);
std::cout << "最后执行的任务:" << t << '\n';
}
int main ()
{
std::cout << "主线程开始执行" << '\n';
// 线程任务,最后执行的,依赖下面其他任务执行完成
std::thread thread_a (work_notify, "A");
// 其他子线程任务
std::thread thread_b (work_s, "B");
std::thread thread_c (work_s, "C");
std::thread thread_d (work_s, "D");
std::cout << "主线程执行的其它同步任务1" << '\n';
thread_a.join(); // join 阻塞当前主线程
thread_b.join();
thread_c.join();
thread_d.join();
std::cout << "主线程结束任务" << '\n';
return 0;
}
结果:(请添加编译参数 -lpthread 执行命令)
主线程开始执行
执行任务 C
主线程执行的其它同步任务1
执行任务 D
执行任务 B
最后执行的任务:A
主线程结束任务
代码解析:
// 简单说就是b,c,d 三个任务执行完成后发送的通知告诉wait(),如果号量=0时,表示不用等待了
1、thread_a 线程要执行work_notify函数时,wait()会先判断当前是否在阻塞中,直到非阻塞才执行任务
2、thread_b、thread_c、thread_d 每次执行任务时,都会将信号量-1,直到信号量=0时,
cv.notify_one(); 会发送一个到thread_a 中的wait() 状态变更的通知,告诉它状态变更
3、信号量=0非阻塞时,执行work_notify函数中任务
# 2、wait_for
前面wait中介绍到,任务B、C、D都完成后再执行A,但是有一种情况,任务B、C、D 中存在某个任务请求超时了,那么就永远无法通知到A执行任务了。
这样就会影响后面的程序功能了。
wait_for
与std::condition_variable::wait() 类似,不过 wait_for 增加了超时判断:
示例1:
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量(信号)通知能力
cv_status
int semaphore_num = 3; // 信号量(标记状态, 大于0时阻塞线程)
// 判断当前是否阻塞线程(wait 需要使用此函数作参数)
bool shipment_available()
{
return semaphore_num<=0;
}
// 任务函数,每次执行后信号量-1
void work_s(const char* t)
{
std::this_thread::yield();
std::unique_lock<std::mutex> lck(mtx);
semaphore_num-=1;
std::cout << "执行任务 " << t << '\n';
sleep(1); // 模拟延时任务
// 子任务执行完成后,如果不需要阻塞,发出通知
if (shipment_available) {
cv.notify_one();
}
}
// 任务通知,最后执行的任务,当状态非阻塞时执行任务
void work_notify(const char* t)
{
std::unique_lock<std::mutex> lck(mtx);//自动上锁
std::cv_status status = cv.wait_for(lck,std::chrono::seconds(5)); // 设置5秒超时,超时后会走 timeout
if (status == std::cv_status::timeout)
{
//表示线程还没执行完,此时已经超时了
std::cout << "子线程任务超时,线程还没有执行完毕,当前任务 " << t << " 被弃" << '\n';
}
else if (status == std::cv_status::no_timeout)
{
//表示线程成功返回
std::cout << "其他子线程全部执行完毕" << '\n';
std::cout << "最后执行的任务:" << t << '\n';
}
}
int main ()
{
// 线程任务,最后执行的,依赖下面其他任务执行完成
std::thread thread_a (work_notify, "A");
// 其他子线程任务
std::thread thread_b (work_s, "B");
std::thread thread_c (work_s, "C");
std::thread thread_d (work_s, "D");
thread_a.join();
thread_b.join();
thread_c.join();
thread_d.join();
return 0;
}
结果:(设置3个任务每次1秒,设置5秒的timeout,所以当前任务正常执行了)
执行任务 C
执行任务 D
执行任务 B
其他子线程全部执行完毕
最后执行的任务:A
修改:
std::cv_status status = cv.wait_for(lck,std::chrono::seconds(2)); // 设置2秒超时,超时后会走 timeout
结果:(设置3个任务每次1秒,设置2秒的timeout,所以当前任务正常执行了)
执行任务 C
执行任务 D
执行任务 B
子线程任务超时,线程�没有执行完毕,当前任务 A 被弃
示例2: 使用future
#include <stdio.h>
#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <list>
#include <mutex>
#include <future>
using namespace std;
int mythread()
{
sleep(4);
return 6;
}
int main()
{
// ⚠️ async 异步函数, 不会阻塞当前主线程
std::future<int> result = std::async(mythread);
// std::future<int> result = std::async(std::launch::deferred, mythread);
// ⚠️ wait_for (未设置async第一参数状态)阻塞当前主线程
std::future_status status = result.wait_for(std::chrono::seconds(3));
if (status == std::future_status::timeout)
{
cout << "任务超时,线程还没有执行完毕" << endl;
}
else if (status == std::future_status::ready)
{
cout << "线程成功执行完毕" << endl;
}
else if (status == std::future_status::deferred)
{
//如果async的第一个参数被设置为std::launch::deferred,立即执行此次,表示默认状态已设置
cout << "任务延迟执行!" << endl;
}
cout << "main全部任务结束!" << endl;
return 0;
}
结果
任务超时,线程还没有执行完毕
main全部任务结束!
# 线程池
# 概念
在一个程序中,如果我们需要多次使用线程,这就意味着,需要多次的创建并销毁线程。而创建并销毁线程的过程势必会消耗内存,线程过多会带来调动的开销,进而影响缓存局部性和整体性能。线程的创建并销毁有以下一些缺点:
- 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
- 销毁太多线程,将导致之后浪费时间再次创建它们。
- 创建线程太慢,将会导致长时间的等待,性能变差。
- 销毁线程太慢,导致其它线程资源饥饿。
线程池维护着多个线程,这避免了在处理短时间任务时,创建与销毁线程的代价。
# 线程池的实现
⚠️ 程序开发过程中,池 的概念很重要,程序要需要在在有限的资源中 考虑 预加载、重复利用、优化性能等
线程池:程序边运行边创建线程是比较耗时的,线程被使用前提前创建适量的数量线程备用,这样,程序在运行时,只需要从线程池中拿来用就可以了.大大提高了程序运行效率.一般线程池都会有以下几个部分构成:
- 线程池管理器(ThreadPoolManager):用于创建并管理线程池,也就是线程池类
- 工作线程(WorkThread): 线程池中线程
- 任务队列task: 用于存放没有处理的任务。提供一种缓冲机制。
- append:用于添加任务的接口
线程池实现代码:线程池代码链接,实测运行正常 (opens new window)
#include <vector>
#include <queue>
#include <thread>
#include <iostream>
#include <condition_variable>
using namespace std;
const int MAX_THREADS = 6; //最大线程数目
template <typename T>
class threadPool
{
public:
threadPool(int number = 1);
~threadPool();
bool append(T *task);
//工作线程需要运行的函数,不断的从任务队列中取出并执行
static void *worker(void *arg);
void run();
private:
//工作线程
vector<thread> workThread;
//任务队列
queue<T *> taskQueue;
mutex mt;
condition_variable condition;
bool stop;
};
template <typename T>
threadPool<T>::threadPool(int number) : stop(false)
{
if (number <= 0 || number > MAX_THREADS)
throw exception();
for (int i = 0; i < number; i++)
{
cout << "create thread:" << i << endl;
workThread.emplace_back(worker, this);
}
}
template <typename T>
inline threadPool<T>::~threadPool()
{
{
unique_lock<mutex> unique(mt);
stop = true;
}
condition.notify_all();
for (auto &wt : workThread)
wt.join();
}
template <typename T>
bool threadPool<T>::append(T *task)
{
//往任务队列添加任务的时候,要加锁,因为这是线程池,肯定有很多线程
unique_lock<mutex> unique(mt);
taskQueue.push(task);
unique.unlock();
//任务添加完之后,通知阻塞线程过来消费任务,有点像生产消费者模型
condition.notify_one();
return true;
}
template <typename T>
void *threadPool<T>::worker(void *arg)
{
threadPool *pool = (threadPool *)arg;
pool->run();
return pool;
}
template <typename T>
void threadPool<T>::run()
{
while (!stop)
{
unique_lock<mutex> unique(this->mt);
//如果任务队列为空,就停下来等待唤醒,等待另一个线程发来的唤醒请求
while (this->taskQueue.empty())
this->condition.wait(unique);
T *task = this->taskQueue.front();
this->taskQueue.pop();
if (task)
task->process();
}
}
/**
* 以下是实现部分
* */
class Task
{
private:
int total = 0;
public:
void process();
};
// 任务
void Task::process()
{
std::cout << "任务执行完成 !" << std::endl;
this_thread::sleep_for(chrono::seconds(1));
}
template class std::queue<Task>;
int main(void)
{
threadPool<Task> pool(1);
std::string str;
while (1)
{
Task *task = new Task();
pool.append(task);
delete task;
}
}
运行:
create thread:0
任务执行完成 !
任务执行完成 !
任务执行完成 !
任务执行完成 !
任务执行完成 !
任务执行完成 !
^C
...Program finished with exit code 0
Press ENTER to exit console.
# CGI
基于CGI 您可以用于 客户端 动态 请求 服务器端资源 (如,HTML页面/二进制文件)
比如GET、POST等的实现
在CGI的基础上作了进一步包装的 CSP/ASP/JSP/PHP/PERL
等等
# 什么是CGI
CGI 是通用网关接口(Common Gateway Interface)的缩写. 它主要用于服务器端动态输出客户端的请求(如,HTML页面/二进制文件).
客户端请求参数不同, 服务器端会给出不同的应答结果
CGI 标准将这个接口定义的非常简单 (即: WEB 服务器收到客户端的请求后通过环境变量和标准输入(stdin)将数据传递给CGI程序, CGI程序通过标准输出(stdout) 将数据返回给客户端). 所以只要能操作标准输入/输出的程序语言都可以CGI程序, Perl/C++/JAVA/VB等.
CSP/ASP/JSP/PHP/PERL 与CGI程序的关系? 它们大都是CGI的变种, 因为它们的操作原理都是CGI的基础上作了进一步的包装, 屏蔽了CGI的与程序语言相关的接口.
为什么还直接用CGI呢? 1、高效率:
C/C++ 不像PERL/VBS/JS等解释执行语言运行时解释执行源文件中的语句. 同时这一点仍非JAVA/PHP等所能及. 所以C/C++仍是许多WEB应用的首选, 特点是大型WEB应用中.
2、兼容性:
嵌入式设备(如PDA/数码产品/通信产品)WEB应用的首选, 目前几乎所有的嵌入式设备都直接用C语言开发, 而CPU/内存/外存等的限制几乎根本不可能安装如PERL/ASP/JSP的运行环境, 所以嵌入式设备上C开发CGI几乎仍是唯一选择.
将 C 直接嵌入到HTML中叫CSP吗? 是的, C 语言天然好的"移植性/高效性/灵活性", 一直以来都是最受程序员青睐的语言, 现在用CSP 技术我们就可以轻松地将 C 语句直接嵌入到 HTML 源文件中了, 它编程过程跟ASP/JSP/PHP 几乎一样. 甚至有些时候, 就可以直接拿 JSP/PHP 的源文件作为 CSP 的源文件了, 因为它们都用 <% 和 %> 进行标记.