进程与线程
进程:如任务管理器中各种程序叫做正在运行的进程。对于操作系统来说,仅仅是一个数据结构,并不真实的执行代码
线程:真实执行代码的
每个进程启动的是时候会同步启动一个主线程即main函数,当main函数结束时,该线程结束并销毁,同时其他线程随之销毁
线程都有一个需要执行的代码块称为线程回调函数
真并发与伪并发
真并发
当cpu是双核或者多核时,并不会一核心一任务,而是由单核心切换转为多核心切换,此时也称作真并发
伪并发
在早期的cpu即单核cpu中,因性能核心各方面较为落后,并发编程实际是一个伪并发编程,即系统中所有进程按照优先级去抢占cpu时间片,也就是系统一会执行这个一会执行哪个。
由于抢占时间片所需时间较短,所以我们并不觉得程序卡顿。但各进程抢占cup时间片是一个很麻烦的事情,cpu虽然提供任务切换的功能即TSS任务段,但Windows并不使用。
因为Windows实现了线程调度,即再线程切换时,上个线程代码执行到的地方的线程的状态,线程上下文,通用寄存器,段寄存器,硬件调试寄存器,EIP(指令指针寄存器),EFLAGS等都会被Windows通过Windows(Context)保存,直到再次切换回来后再加载
并发形式
- 多进程并发:一个可执行程序里只有一个线程,同时启动多个进程执行,如浏览器
- 多线程并发:一个进程内运行多个线程,变量的访问
如:
Value = 100 全局变量
A B A,B两个线程
A,B线程访问Value,访问值都是100
现AB两线程都对Value进行++
操作完成后,Value的值为101,这种情况叫做线程同步问题(后续有讲解)
线程的生存周期
回调函数执行完毕,自然死亡
主线程死亡,被动死亡
并发函数分类实践
如下是一个简单的并发程序描述:
#include <iostream>
#include <thread> 线程库
普通函数
void FirstThreadCallBack() 构建一个新的函数
{
for (size_t i = 0; i < 100000; i++)
{
std::cout << "First:" << i << std::endl;
}
}
int main()
{
std::thread obj(FirstThreadCallBack); 声明线程对象,并在其构造函数中传入要并发的函数地址(也可是类等等其他东西)。在此开始启动一个线程去执行线程回调函数
for (size_t i = 0; i < 100000; i++)
{
std::cout << "main:"<< i << std::endl;
}
System(“pause”); 程序暂停至此,不会死亡
return 0;
}
此时程序会同时进行上述两个函数
仿函数
class Exec 一个类的仿函数
{
public:
void operator()()const
{
std::cout << "Exec" << std::endl;
}
};
int main()
{
Exec e;
std::thread obj(e);
System(“pause”); 程序暂停至此,不会死亡
return 0;
}
打印Exec
Lambda
int main()
{
std::thread obj([] {std::cout << "Lambda" << std::endl; });
System(“pause”); 程序暂停至此,不会死亡
return 0;
}
打印Lambda
综上可知,任何可以调用的类型都可以用于线程对象的构造函数传参
线程死亡
一旦线程启动了,我们就需要知道线程是怎么死的
1.自然死亡 thread析构函数terminate(),主函数执行完毕时,析构函数执行
非自然死亡 thread析构函数执行完毕时,并发的函数即thread传参函数不一定执行完毕
2.等待 绝对的自然死亡 等待函数执行完毕后,程序再往下走
3.不再等待(主线程存活时后台运行)依赖于主线程的存活
4.如果一个线程是Windows原生线程,主线程销毁后其也会死亡
如下我们验证Windows原生线程的死亡:
包含头文件Windows.h
创建一个原生线程需要调用CreateThread()API
利用CreateThread()API中的一个:
DWORD ThreadCallBack(LPVOID lpThreadParameter)
{
for (size_t i = 0; i < 100000; i++)
{
std::cout << "First:" << i << std::endl;
}
return 0;
}
int main()
{
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadCallBack, NULL, NULL, NULL);API函数,创建了一个原生线程
return 0;
}
此时运行程序,发现随着主线程的结束该原生线程死亡
等待
以下讲述等待作用:
1.原生线程等待死亡
int main()
{
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadCallBack, NULL, NULL, NULL);
此处是拿个该原生线程的句柄即控制该线程的启动器
WaitForSingleObject(hThread, -1); -1代表永久等待
return 0;
}
此时发现运行程序原生线程不会死亡,直到它运行完毕
2.C++并发编程等待死亡:
int main()
{
std::thread obj(FirstThreadCallBack);
obj.join(); 阻塞等待,作用是在此处等待函数的返回
return 0;
}
此时运行程序,不会报错也不会死亡
不再等待
int main()
{
std::thread obj(FirstThreadCallBack);
obj.detach(); 不再等待:当其所在命名空间结束时,直接死亡
for (size_t i = 0; i < 100000; i++)
{
std::cout << "main:" << i << std::endl;
}
如在此处加一个循环,程序在执行该循环时,程序没有死亡,并发函数也不会死亡,而是一起执行两个函数
return 0;
}
并发特殊情况
情况一
原本在后台运行的线程,由于各种问题,线程提前崩坏,没有正常返回,等待函数没有接收到返回,抛一个异常,遇到此情况跳过即可
情况二
并发线程不仅可以传函数地址,也可以传其他多个参数,如下:
包含头文件:string.h
void Print(std::string szBuffer,int nCount)
{
for (size_t i = 0; i < nCount; i++)
{
std::cout << szBuffer << ":" << i << std::endl;
}
}
int main()
{
std::thread obj(Print,"rkvir",50);
system(“pause”);
return 0;
}
程序运行,出现一个新现象
在循环执行时,出现
原因:先打印rkvir,然后切片回来,执行system(“pause”),再切片回来打印38
这个现象很形象展示了线程同步问题(后续讲解)