线程
线程是什么,为什么要用线程
编程是为了实现相应功能的,所以线程可以先从功能去认识。
函数和线程都可运行自身内部函数和代码,线程的不同是——函数运行等待时,不能进行另一个相同函数的操作,因为函数只有一个;线程运行时,若等待,仍可进行其他线程的使用。
所以我们可以把线程看成,自动创建不同函数,运行同一内容,这样就不会阻碍其他线程进行,从而做到能够并行或并发。
线程的两种运行方式:并行、并发
并行,多个线程同时开始;
并发,多个线程同时开始,轮换进行。
注1:不一定同时结束。
注2:单核CPU不能并行,只是轮换的非常快。
线程的状态
就绪 阻塞 运行(新生 死亡)
状态转化
就绪->运行:获得CPU时间片(该进程在CPU上运行时间)
(反)运行->就绪:失去时间片/中断切换/轮换进程/重新开始
运行->阻塞:等待IO操作,主动写出相应操作,使其转化。(操作和命令的输入输出,包括运行某一功能)
(反)阻塞->运行:阻塞条件解除,就绪,运行。
阻塞->就绪:IO操作发生
(反)就绪->阻塞:由于某些原因,线程暂不可运行,处于阻塞状态。
总:当线程需要分配到CPU和进行IO操作都会阻塞
线程的地位
线程是操作系统能运算的最小单位,是进程中的一个执行单元,是分配CPU的基本单位。
线程的组成部分
线程栈:
其中储存了线程所需的局部临时资源。
内核对象:
操作系统可以通过内核对象管理和调度线程核心数据结构,包含有关于此的信息。其中有一个计数器,初始值为1,有进程访问时+1,线程退出时-1,关闭句柄时-1,为0时自动销毁。
如何结束线程
- 自然返回
- ExitThread() 结束调用者
- TerminateThread() 强制杀死线程
进程
线程构成了进程
多个线程的操作的集合就是进程。
一个进程至少有一个线程,被叫做主线程,主线程是我们写的程序的入口点,写入主要逻辑。
进程的地位
进程是一个操作系统的执行实体,是CPU调度和执行的基本单位,是分配资源的一个独立的集合(这里倾向于操作系统角度,线程是CPU角度)
进程对线程的作用
如果线程看为一个函数,进程可以看成一个简化的项目,这个项目是一体的,把线程组合到了一起。
这样可以减少零散的轮换,提升处理效率;共享地址空间和全局变量;访问共享空间。来减少代码定义的复杂度。
进程储存在哪里
进程分32位系统和64位系统两种。
32位系统进程,加载4GB虚拟地址空间,虚拟地址空间是逻辑上的,只有某部分需要时才会映射到物理内存上,本身并非直接占用。
而这个空间的分布为三种模式
空指针模式,
用户模式,
内核模式。
而64位的进程虚拟空间是8TB
而处于空间大小和系统优化原因,64位的已不要求重点记忆空间内容。
关于线程和进程的具体调用约定推荐去官网查询。
程序
程序是什么
程序是一种静态的指令,程序内写了包含进程的一系列代码,进程是一种运行状态。
程序的编译过程
预处理
- 解析#ifdef/ifndef
- 解析#include,将文件加载到当前位置
- 解析#define,简单文本替换,无安全校验检查
- 删除所有注释
- 对文件进行序号标识
- #pragma pack(n)(结构体或联合体的成员以n字节对齐)
编译
1.词法分析(将源代码分割成词法单元,如关键字,标识符,运算符)
2.语法分析(将词法单元转化为抽象语法树,是语法元素的关系的连接结构)
3.语义分析(检查语义正确性,如类型、作用域)
4.中间代码生成(与编译器有关,可能进行)(更换更适合优化的代码来过度)
5.优化(尝试更改程序性能、可读性或其他特定属性,可发生在多个层次,如中间代码优化、指令级优化)
6.得到汇编语言代码(文本文件方式储存)
汇编
代码生成(二进制)(以汇编语言代码生成目标代码(Obj.文件))(若为单模块程序,可直接从源代码转化为机械码,不需要汇编和链接)
链接
连接器进行代码组织和链接(多模块程序的目标代码结合)
线程同步
上面已经从线程的角度介绍了程序,接下来介绍重要的用法。
线程同步可以形容操作,也可以形容状态。
操作:通过使用锁、信号量、条件变量等机制,使线程处于阻塞状态,让线程相互协作,互不干扰。
而这种协作状态也可以叫线程同步。
实现线程同步的方法
为了实现线程同步,线程要保持某一操作,在某一时间只属于自己,来实现相关运算,否则用被其他线程篡改,很容易出现错误。
而这个某时只能运行一个的操作就是原子访问状态。
原子访问
古希腊有一个原子不可分的假说,大概就是这个名字的由来。
原子访问的变量在同一时间,只能由一个线程运行,其他线程运行到这里会发生阻塞,直至唯一的原子变量运行后,再进行对其的原子访问。
C++中参考 std::atomic<int/long/char> VariableName(n),
其中分别为类型、变量名、赋值数。也可以去除括号,用等于号赋值。
关键段
显然,只使一个变量有原子性是不够的。
我们需要创建一个区域,让这个区域的代码有原子性,这样的代码段,才能更有效的实现线程同步,我们把这段代码叫做关键段(也叫临界区)。
如何实现关键段
关键段函数
有一个<windows.h>的关键段函数,可以实现该操作,
CRITICAL_SECTION A(变量名); (用来定义变量)
InitializeCriticalSection(&A); (初始化)
EnterCriticalSection(&A); 进入关键段
LeaveCriticalSection(&A); 退出关键段
Delete CriticalSection(&A); 释放
注:此函数不能跨进程。
同步原语
同步原语分为几种,他们都能实现有关关键段的功能。(它们也可以分在内核对象类,但内核对象侧重于,内核对它们的调度和支持)
头文件依旧<windows.h>
条件变量
CONDITION_VARIABLE condVar(条件变量对象)
(VOID) InitializeConditionVariable(&condVar)(初始化)
(BOOL) SleepConditionVariableCS(&condVar,&cs,INFINITE) (关键段内等待条件变量,使线程处于休眠状态) (cs为定义的关键段)
(VOID) WakeConditionVariable(&condVar) (唤醒某正在等待的线程)
(VOID) WakeAllConditionVariable(&condVar) (唤醒所有在条件变量上的线程)
互斥量
HANDLE hMutex ; //定义
hMutex = CreateMutex(NULL,FALSE,NULL); //创建
(参数为安全属性、初始状态、名称)
(初始状态代表是否锁定第一个创建它的线程)
WaitForSingleObject(hMutex ,INFINITE); //用来等信号,锁被占用时无信号。下面两个同步原语一致
(第二个参数为等待时间,单位毫秒,这里是一直等)
(返回值有WAIT_OBJECT_0成功;WAIT_ABANDONED有互斥量线程终止且未释放;WAIT_TIMEOUT超时;WAIT_FALED失败)
ReleaseMutex(hMutex ); //使状态为0,解锁;
事件
HANDLE hEvent;
CreateEvent(hEvent);
Waitforsingleobject(hEvent);
SetEvent(hEvent);
ResetEvent(hEvent);
(事件不同之处在于,它可以定义是否有信号)
信号量
CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //通常使用NULL,为不可被继承
LONG lInitialCount, //最初可以访问共享资源的线程数量
LONG lMaximumCount, //上面信号量的最大计数值
LPCSTR lpName //信号量名称,可NULL
Waitforsingleobject(); (每使用时,上文信号量值减1)
BOOL ReleaseSemaphore(
HANDLE hSemaphore, //创建的HANDLE类型返回值
LONG lReleaseCount, //释放信号量数(解锁数)
LPLONG lpPreviousCount //接收之前信号量的计数值,可NULL不使用
);
关键段与内核对象的区别
由于同步原语的内核性(被叫做内核对象),它们与关键段(指函数)有一定区别。
1.作用范围
关键段:用户模式;
内核对象:内核模式。
2.灵活性、安全性:
内核对象更好(因为阻塞事件、强制杀死时自动释放)
3.效率:
关键段更好(无需切换状态--内核与用户)