知识导图 知识导图
首页
iOS知识
计算机软件
  • 即时通讯网 (opens new window)
  • 开发常用网站 (opens new window)
首页
iOS知识
计算机软件
  • 即时通讯网 (opens new window)
  • 开发常用网站 (opens new window)
  • MD

  • Vue

  • C语法

  • C++语法

    • C++基础概念(上)
    • C++基础概念(下)
    • C++语法速查
    • C++面向对象(上)
    • C++面向对象(下)
    • C++高级教程(上)
    • C++高级教程(中)
    • C++高级教程(下)
      • 信号处理
        • 常见异常信号
        • 捕获异常信号
        • 抛出异常信号
      • 多线程
        • 一些概念
        • 创建线程
        • 编译命令
        • join与detach
        • this_thread
      • 锁(多线程)
        • 互斥锁mutex
        • 1、lock与unlock
        • 2、lock_guard
        • 3、unique_lock
        • 死锁
      • 条件变量(线程)
        • 1、wait
        • 2、wait_for
      • 线程池
        • 概念
        • 线程池的实现
      • CGI
        • 什么是CGI
  • 汇编语言

  • 软件编程及算法
  • C++语法
2023-07-05
目录

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全部任务结束!

# 线程池

# 概念

在一个程序中,如果我们需要多次使用线程,这就意味着,需要多次的创建并销毁线程。而创建并销毁线程的过程势必会消耗内存,线程过多会带来调动的开销,进而影响缓存局部性和整体性能。线程的创建并销毁有以下一些缺点:

  • 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
  • 销毁太多线程,将导致之后浪费时间再次创建它们。
  • 创建线程太慢,将会导致长时间的等待,性能变差。
  • 销毁线程太慢,导致其它线程资源饥饿。

线程池维护着多个线程,这避免了在处理短时间任务时,创建与销毁线程的代价。

# 线程池的实现

⚠️ 程序开发过程中,池 的概念很重要,程序要需要在在有限的资源中 考虑 预加载、重复利用、优化性能等

线程池:程序边运行边创建线程是比较耗时的,线程被使用前提前创建适量的数量线程备用,这样,程序在运行时,只需要从线程池中拿来用就可以了.大大提高了程序运行效率.一般线程池都会有以下几个部分构成:

  1. 线程池管理器(ThreadPoolManager):用于创建并管理线程池,也就是线程池类
  2. 工作线程(WorkThread): 线程池中线程
  3. 任务队列task: 用于存放没有处理的任务。提供一种缓冲机制。
  4. 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 的源文件了, 因为它们都用 <% 和 %> 进行标记.

C++高级教程(中)
Day1-前言

← C++高级教程(中) Day1-前言→

Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式