Win32
前言
什么是WIN32 API
Win32 API是Windows平台上的一组应用程序接口,可以在C/C++等编程语言中使用,用于访问Windows操作系统的各种功能和资源,如文件、窗口、消息等。Win32 API提供了一系列的函数和数据结构,开发人员可以利用这些API实现Windows应用程序的各种功能,如窗口创建、消息处理、文件操作、进程管理等
API存放在哪里
Win32 API函数是通过DLL(动态链接库)实现的。这些DLL文件通常位于Windows系统目录的System32子目录下,例如C:\Windows\System32
。每个DLL文件包含一个或多个函数的实现,这些函数可以被其他应用程序或者系统程序调用
通常来讲, C:\Windows\System32
中存放的是64位DLL文件,而C:\Windows\SysWOW64
中存放的是32位DLL文件
几个重要的DLL
1.kernel32.dll
kernel32.dll是Windows操作系统的核心动态链接库之一,它包含了大量的系统级函数,用于操作系统管理、内存管理、文件I/O、多线程、进程管理等方面。这些函数为开发人员提供了对底层系统资源的访问,使得开发者可以编写高效、可靠的应用程序。一些常见的函数包括CreateFile、ReadFile、WriteFile、GetModuleHandle、GetLastError等等
2.user32.dll
user32.dll是Windows操作系统的用户界面动态链接库,它包含了一系列用于窗口、消息、菜单、对话框、剪贴板等用户界面组件的函数。这些函数可以帮助开发人员实现各种视觉元素、控件、交互效果等等,提高了应用程序的交互性和易用性。一些常见的函数包括CreateWindowEx、SendMessage、SetWindowText、GetWindowRect等等
3.gdi32.dll
gdi32.dll是Windows操作系统的图形设备接口动态链接库,它包含了一系列用于绘图和图像处理的函数
什么是快照
在Win32系统编程中,快照是指一个静态的系统信息副本。在某些情况下,我们需要获取系统某个时刻的状态信息,如当前运行的进程和线程,这时候就可以使用快照来获取。通过获取快照,我们可以得到系统的静态信息,这样就可以对这些信息进行遍历和查询。同时,快照也可以在某些情况下提高系统资源的利用率,因为它不需要实时地获取系统信息。而且,快照在一定程度上也可以减少对系统的干扰,因为获取快照不需要打断正在执行的进程和线程
什么是回调函数
回调函数是一种函数指针,它作为参数传递给另一个函数,并在该函数执行过程中被调用。
例如,假设有一个函数doSomething()
,该函数需要在某个事件发生时执行一些特定的操作。可以将一个回调函数作为参数传递给doSomething()
函数,以便在事件发生时调用该回调函数
具体来说,假设回调函数的名称为callback()
,那么可以定义一个函数指针类型,用于存储指向callback()
函数的指针,如下所示:
然后,在doSomething()
函数中,可以将一个指向callback()
函数的指针作为参数传递进去,如下代码所示:
什么是同步读取和异步读取
同步读取是指调用线程会等待读取操作完成后才能继续执行,即读取操作是同步阻塞的。在同步读取中,读取操作完成后,数据被复制到缓冲区,并立即返回给调用线程。这种读取方式简单易用,但是它会阻塞调用线程,降低应用程序的并发性和响应性。
异步读取是指调用线程不会阻塞等待读取操作的完成,而是立即返回,并允许调用线程继续执行其他任务。在异步读取中,读取操作不会立即返回,而是在后台执行,并在完成后通知调用线程。这种读取方式可以提高应用程序的并发性和响应性,但是它需要更复杂的编程模型和更高的系统资源消耗
字符串编码
ASCII编码
ASCII编码是一种最早出现的字符编码方案,它是由美国标准化协会(ASA)于1963年制定的标准,用于在计算机系统中表示英语文本字符集。ASCII编码仅使用7位二进制数(共128个),用于表示英文字母、数字、标点符号以及一些控制字符,例如换行、回车、制表符等。这个编码方案中,每个字符都被分配一个唯一的编号,称为ASCII码值。
由于ASCII编码仅支持128个字符,因此它无法表示其他语言(如汉语、日语等)所需的字符。随着计算机技术和国际化的发展,ASCII编码已经逐渐被更强大的Unicode编码所取代,但ASCII编码仍然是计算机系统中最基本和最常用的字符编码方式之一,对于英语文本处理仍然有广泛的应用
下述表格为ASCII码对照表:
GB2312编码(ASCII码扩展)
GB2312是中华人民共和国发布的一种字符集标准,于1980年发布。它包含了简体中文中常用的6763个汉字以及包括拉丁字母、数字、标点符号等在内的682个字符,共计7445个字符。GB2312采用双字节编码,每个字符用两个字节来表示。该字符集广泛用于中文操作系统、应用程序和互联网应用中
GB2312编码的出现,使得计算机可以更加方便地处理和显示汉字,被广泛应用于中国大陆的计算机系统和应用软件中。但随着汉字数量和应用领域的不断扩大,GB2312编码逐渐无法满足需求,于是在其基础上发展出了GBK编码和GB18030编码,后者已经成为中国大陆计算机系统和应用软件中的主要字符集编码方案
GBK编码
GBK编码是汉字编码的一种,其全称是“汉字内码扩展规范”,由中国国家标准GB2312编码基础之上扩展而来。GBK编码是双字节编码,使用两个字节表示一个中文字符,因此一个GBK编码的字符占用两个字节的存储空间,但是对于英文字母、数字和符号等ASCII字符,仍然使用一个字节来表示。GBK编码支持简体中文和繁体中文,它的编码范围包括了GB2312编码的全部汉字和符号,以及收录了香港繁体中文、台湾繁体中文等汉字,共收录了21003个汉字和符号
UNICODE编码
Unicode编码是一种国际化的字符集标准,旨在为全世界范围内的所有书写系统提供一个统一的编码方案。它包含了世界各种语言所使用的所有字符,包括汉字、拉丁字母、希腊字母、西里尔字母、希伯来字母、阿拉伯字母等在内的超过14万个字符。
Unicode的编码范围是:0~0x10FFFF
要注意的是Unicode只是一个符号集, 它只规定了符号的二进制代码, 并没有规定这个二进制代码该如何存储。若要实现存储, UTF-8和UTF-16则是Unicode字符集的具体实现方式之一
UTF-16
UTF-16是Unicode编码中的一种字符编码方式,它使用16位(即2个字节)来表示每个字符。与UTF-8不同,UTF-16中的每个字符都使用相同数量的字节来表示,因此每个字符的长度都是固定的
UTF-16编码有两种存储方式:UTF-16 LE(Little Endian)和UTF-16 BE(Big Endian)。在UTF-16 LE编码中,低序字节存储在内存的低地址处,高序字节存储在内存的高地址处,这符合小端存储的规则;而在UTF-16 BE编码中,高序字节存储在内存的低地址处,低序字节存储在内存的高地址处,这符合大端存储的规则
要注意的是, 16位只是一个单位, 不代表一个字符只有16位, 具体要看此字符的unicode编码处于什么范围, 有可能此字符占2个字节, 也有可能占4个字节
UTF-8
UTF-8是一种Unicode字符集的变长字符编码方式,它可以用1-4个字节来表示Unicode字符集中的所有字符
UTF-8编码使用可变长度的编码方式,它的编码规则如下(与utf-16编码范围对比):
BOM
BOM(Byte Order Mark)是一个用于表示文本文件字节序的特殊标记,它通常出现在文本文件的开头处, 可用其来判断文本文件的存储格式
以下是不同编码对应的BOM:
细讲UTF-8编码规则
此处有两个文本文件(utf-16.txt和utf-8.txt),其内容是一样的,均为"测C", 分别使用utf-16 le
和utf-8
存储
首先使用notepad++查看utf-16.txt
的16进制内容, 前两个字节是BOM, 可以忽视掉。可以看到字符"测"的编码为"4b 6d", 由于文件采用的是小端存储, 所以其真正编码为"6d 4b"
也就说字符"测"在utf-16
的编码范围中, 属于000800 ~ 00FFFF, 对应的utf-8
的编码规则为1110xxxx 10xxxxxx 10xxxxxx
查看utf-8.txt的16进制内容, 前三个字节是BOM, 此处字符"测"的utf-8编码为"e6 b5 8b", 将其转换成二进制后即为"1110 0110 1011 0101 1000 1011"
根据utf-8的编码规则(1110xxxx 10xxxxxx 10xxxxxx), 我们取x的值, 最终的结果为0110 1101 0100 1011, 对应的16进制为"6d 4b", 即对应utf-16的编码
UTF-8和UTF-16的区别
1.编码方式
UTF-16采用24个字节来表示每个字符,而UTF-8则采用变长的编码方式,使用14个字节来表示不同的字符。UTF-8中的ASCII字符使用单字节编码,而其他字符则使用多字节编码
2.字节长度
UTF-16中的每个字符使用相同数量的字节来表示,因此每个字符的长度都是固定的;而UTF-8中不同的字符使用不同长度的字节来表示,因此字符的长度是可变的
3.存储方式
UTF-16有两种存储方式:UTF-16LE(Little Endian)和UTF-16BE(Big Endian),它们之间的区别在于字节的存储顺序。而UTF-8没有字节序的问题,因为它是以字节为单位进行编码的,不涉及多字节字符的存储顺序问题
一般来说,UTF-8编码更适合数据传输,而UTF-16编码更适合数据存储
常用数据类型
字符串类型
使用实例如下:
整数类型
指针类型
句柄类型
进程
什么是进程
在计算机操作系统中,进程是正在运行中的程序的实例。进程是操作系统进行资源分配和管理的基本单位,包括内存、文件句柄、系统状态等。每个进程都有自己的独立内存空间和运行状态,因此它们不会互相干扰,也不会互相影响。多个进程可以在操作系统上同时运行,每个进程都在自己的空间里执行自己的代码
进程内存空间的划分
在Windows X86环境下, 进程的内存空间通常被划分为以下三个区域
进程的创建过程
首先有一点要清楚, 任何进程都是别的进程创建的, 而第一个进程是系统启动时由操作系统内核创建的, 它的进程ID为0, 称为系统空闲进程或系统进程
在Windows中,进程的创建可以使用CreateProcess
函数,该函数会返回一个指向新进程的句柄
**1.为进程分配内存空间,加载EXE文件, 并将其映射到内存 **
2.创建进程内核对象EPROCESS
EPROCESS是Windows操作系统内核中的一种数据结构,它代表了一个进程对象。EPROCESS结构体包含了进程的许多信息,例如进程的PID、进程的线程列表、虚拟内存映射、访问权限、I/O访问权限等等
3.映射系统dll(ntdll.dll)至内存
4.创建线程内核对象ETHREAD
创建进程的时候系统会默认创建一个线程, 也就是说每个进程都至少有一个线程
5.系统启动线程
线程启动之前, 还需要映射运行可执行文件所需的其他dll文件, 随后线程才开始执行
进程涉及API
CreateProcess
CreateProcess函数用于创建一个新的进程并返回进程句柄, 其原型如下
如下是PROCESS_INFORMATION结构的成员
如下是STARTUPINFO结构的成员
如下代码是一个创建进程的简单实例, 用于启动计算器(calc.exe)
若需要以挂起的形式创建进程, 可将CreateProcess
函数的第六个参数设置为CREARE_SUSPEND
, 这样创建的进程一开始并不会自动启动线程, 而是需要自己手动执行ResumeThread
函数恢复线程后才会启动
如上代码所示, 线程会在for循环打印代码执行结束后才会启动线程, 执行结果如下
OpenProcess
OpenProcess
函数用于打开一个已存在的进程对象,以便对该进程执行操作,例如向该进程发送信号或从该进程读取内存。此函数的调用者必须具有足够的权限来打开目标进程
如果函数执行成功,返回打开进程的句柄,否则返回NULL,并可通过调用GetLastError
函数获取错误码
要注意的是, 使用CloseHandle
函数释放句柄后, 就不能再使用OpenProcess
函数来打开这个进程了, 因为CloseHandle
函数会将句柄从进程的句柄表中移除,并且在所有引用计数都归零之后释放内存资源
OpenProcess函数的语法如下:
以下是dwDesireAccess参数的可取值:
PROCESS_ALL_ACCESS:具有完全访问权限的进程访问权限。
PROCESS_CREATE_PROCESS:允许创建新进程。
PROCESS_CREATE_THREAD:允许在进程中创建新线程。
PROCESS_DUP_HANDLE:允许进程使用 DuplicateHandle 函数复制句柄。
PROCESS_QUERY_INFORMATION:允许查询进程信息,如进程ID、进程优先级等。
PROCESS_QUERY_LIMITED_INFORMATION:允许查询受限信息,如进程ID、进程优先级、进程占用内存等。
PROCESS_SET_INFORMATION:允许设置进程信息,如进程优先级、进程AffinityMask等。
PROCESS_SET_QUOTA:允许设置进程的工作集大小和默认的硬错误模式。
PROCESS_SUSPEND_RESUME:允许挂起和恢复进程。
PROCESS_TERMINATE:允许终止进程。
PROCESS_VM_OPERATION:允许进行虚拟内存操作,如 VirtualAlloc、VirtualProtect 等。
PROCESS_VM_READ:允许读取进程的虚拟内存。
PROCESS_VM_WRITE:允许写入进程的虚拟内存
TeminateProcess
TerminateProcess
函数是Windows操作系统提供的函数之一,用于终止指定进程, 当调用TerminateProcess
函数时,会向指定进程发送一个中断信号,强制其终止。
这个过程是非常暴力的,会直接终止进程的所有线程,不会给进程和线程任何清理资源的机会,因此使用该函数需要非常慎重
函数定义如下:
GetModuleFileName
GetModuleFileName函数用于获取指定模块的完整路径名。通常情况下,可以通过指定NULL作为参数hModule,来获取当前应用程序的完整路径名,该函数的声明如下:
GetCurrentDirectory
GetCurrentDirectory函数用于获取当前进程的工作目录。其函数原型为
这个函数的路径是指当前进程的工作目录,而不是当前模块的目录。如果需要获取当前模块的目录,需要使用GetModuleFileName函数来获取模块文件的路径
GetStartupInfo
GetStartupInfo函数用于检索当前进程的启动信息,它的主要功能是获取STARTUPINFO结构体,其中包含了进程的启动信息,如命令行参数、标准输入输出句柄、窗口显示方式等
调用GetStartupInfo函数需传递一个指向STARTUPINFO类型的指针
GetCurrentProcessID
GetCurrentProcessID函数是Windows API中的一部分,它返回当前进程的进程ID(Process ID, 其返回值是一个无符号长整型
其语法格式如下:
GetCurrentProcess
GetCurrentProcess函数是Windows API提供的一个函数,用于获取当前进程的句柄。该函数没有任何参数,调用后将返回一个类型为HANDLE的句柄,该句柄指向当前进程
GetCurrentProcess 获取当前进程的一个伪句柄GetCurrentProcess 总是返回-1(即0xFFFFFFFF),代表当前进程。这个句柄不在句柄表中,不是真正的句柄,所以叫伪句柄
EnumProcesses
EnumProcesses函数是Windows API中的一个函数,用于列举当前正在运行的进程的ID号,通常用于获取系统中所有进程的ID号列表。若函数执行成功则返回TRUE, 否则返回FLASE, 它的声明如下:
如果包含了Windows.h头文件, 还提示"EnumProcesses"未定义标识符, 请检查是否正确链接了psapi.lib库文件。
可以通过在Visual Studio中转到“项目”菜单,然后选择“属性”来查看和配置链接器选项。在属性页面的左侧选择“链接器”,然后选择“输入”。在“附加依赖项”字段中添加“Psapi.lib”,然后包含头文件:include "psapi.h"
如下实例枚举当前系统运行的所有进程ID, 并打印至控制台
CreateToolhelp32Snapshot
CreateToolhelp32Snapshot函数是Windows系统提供的一个快照函数,可以获取系统中当前正在运行的进程和线程的快照。该函数可以通过枚举系统中所有进程和线程来帮助实现进程和线程的监控和管理。在调用该函数时,需要指定快照类型,如进程快照、线程快照等。函数会返回一个句柄,该句柄可以作为参数传递给其他Tool Help函数,以获取有关系统中进程和线程的详细信息。在使用完成后,需要调用CloseHandle函数关闭句柄
如下实例使用CreateToolhelp32Snapshot
函数枚举系统所有进程:
句柄表
什么是内核对象
在Windows操作系统中,内核对象是由内核负责管理的资源,如进程、线程、文件、互斥体、事件、信号量、共享内存、管道等。
每个内核对象都对应着一个内核对象结构体,内核对象结构体包含了该内核对象的属性、状态、引用计数等信息
什么是句柄表
句柄表(Handle Table)是Windows内核中的一个重要数据结构,用于存储内核对象的句柄(Handle),包括进程、线程、文件、事件、互斥体等。
每个进程都有一个句柄表,这个表存储了当前进程所拥有的所有内核对象的句柄,通过这些句柄可以访问相应的内核对象
如下图所示, 每个进程都在内核区域都会有一个EPROCESS对象(内核进程对象), 在这个进程里执行了四个函数, 分别是CreateProcess
、CreateThread
、CreateEvent
、CreateFile
,执行这些函数的同时, 也会在内核区创建相应的对象
EPROCESS结构体有一个成员叫ObjectTable, 此成员指向句柄表, 句柄表存放了每个内核对象的句柄
多进程共享一个内核对象
在Windows操作系统中,不同的进程之间是相互独立的,它们各自拥有独立的虚拟地址空间和资源,进程之间不能直接访问彼此的内存空间。但有时候,不同的进程需要共享某些内核对象(如互斥体、事件等),以便它们能够协同工作。Windows内核提供了一些机制来实现多进程间共享内核对象的需求
在多进程共享一个内核对象时,可以使用内核对象的名字(例如互斥体名字、事件名字等)在不同的进程之间进行传递和共享。当一个进程创建一个具有名字的内核对象时,其他进程可以通过内核对象名字来打开这个对象,从而共享这个对象
句柄继承问题
在 Windows 操作系统中,每个进程都有一张句柄表,记录了该进程所打开的内核对象的句柄。当一个进程创建子进程时,子进程会默认继承父进程的句柄表。也就是说,子进程会复制父进程的句柄表,并与父进程共享同一份句柄表,子进程可以使用父进程打开的内核对象的句柄进行操作。
但并非所有类型的内核对象都可以被子进程继承,例如父进程打开的仅在父进程内有效的句柄,如 GDI 对象、用户对象等,子进程无法继承。另外,父进程可以在创建子进程时通过传递参数指定子进程继承哪些句柄。如果没有指定,则默认情况下,子进程将继承所有可继承的句柄。
如下图所示, 父进程调用了CreateProcess
函数创建了一个子进程, 同时在内核区也会创建一个属于子进程的EPROCESS, 其实句柄表还有一列字段用于表示内核对象是否允许被继承, 1表示允许, 0表示不允许。CreateProcess
函数有一个参数叫做bInheritHandles
, 若此值为True, 则子进程会复制父进程的句柄表
什么是全局句柄表
全局句柄表是一个系统级别的内核对象,用于存储所有的内核对象句柄。在Windows操作系统中,每个进程都有一个独立的句柄表,用于存储它自己的内核对象句柄,而全局句柄表是操作系统内部用于管理所有进程的句柄的数据结构
当一个进程创建一个内核对象时,Windows操作系统会返回一个唯一的内核对象句柄。这个句柄会被存储到创建进程的句柄表中,同时也会被存储到全局句柄表中。其他进程可以通过特定的API函数获取这个句柄,并使用它来访问创建进程的内核对象
全局句柄表的作用在于提供了一种跨进程共享内核对象的方式,使得多个进程可以访问同一个内核对象,从而实现进程间的通信和协作。全局句柄表的管理和维护由操作系统负责,应用程序无法直接访问和修改它
线程
什么是线程
线程是附属在进程上的执行实体, 是代码的执行流程
一个进程可以包含多个线程, 但一个进程至少要包含一个线程
进程与线程的关系
可以将进程比作一个房子,它是一个容器,可以包含很多个线程(居住者)同时工作。线程可以在进程中进行交互和共享资源(房间、厨房等)。与居住在房子里的人一样,线程需要执行某些任务,并且它们可以使用相同的内存和资源来完成它们的工作
线程涉及API
CreateThread
CreateThread
函数用于创建一个新线程,该线程在进程空间内独立运行。该函数返回新线程的句柄,以及线程的唯一标识符
在调用该函数后,需要通过 CloseHandle
函数关闭线程句柄,否则会导致资源泄漏
线程函数
线程函数是线程执行的代码,它会在调用CreateThread
函数创建线程后被调用, 其返回值为DWORD类型, 此值会传递给GetExitCodeThread
函数。如果线程函数执行完毕后不返回任何值,则默认返回0
以下是一个创建线程并给先传递参数的实例,CreateThread
函数创建了一个新的线程并传递了一个 int
类型的参数 count
。新线程的入口点是 ThreadProc
函数,该函数接受一个 LPVOID
类型的参数 lpParam
,在这里将其转换为 int*
类型的指针,然后使用该参数进行迭代计数
GetExitCodeThread
GetExitCodeThread函数用于获取指定线程的退出代码, 其语法格式如下所示:
此函数的使用实例如下:
线程参数的生命周期
创建线程时需要注意向线程传递的参数的生命周期。一般情况下,线程创建后会立即运行,并且在运行过程中可能需要使用传递进来的参数。如果传递的参数的生命周期比线程短,当线程需要使用参数时,参数可能已经失效了,导致程序出错。因此,一般的做法是将参数拷贝一份给线程,在线程中使用拷贝的参数,确保参数的有效性
假设我们要创建一个线程来打印一个字符串,我们需要将该字符串作为参数传递给线程。但是,如果该字符串是在主线程中声明并初始化的局部变量,那么当主线程完成时,该字符串将被销毁,这可能会导致在线程中引用该字符串时出现问题
为了避免这种情况,可以通过以下方式解决:在主线程中使用动态内存分配函数(例如malloc)为字符串分配内存,将字符串的指针作为参数传递给线程,然后在线程完成后手动释放内存。这样,即使主线程完成并销毁了该字符串,线程仍然可以访问该字符串所在的内存空间。下面是一个示例代码
线程控制函数
SuspendThread
SuspendThread
函数用于暂停线程, 其语法格式如下
ResumeThread
ResumeThread
函数用于恢复线程, 其语法格式如下:
WaitForSingleObject
WaitForSingleObject函数是一个Windows API函数,它可以等待一个指定的内核对象变为可用。它的作用是使当前线程暂停执行,直到指定的内核对象变为有信号(signaled)状态,或者直到超时时间已过。简单来说就是等待指定线程执行结束后当前线程才能恢复执行
其语法格式如下所示:
WaitForMutipleObjects
与WaitForSingleObjects函数不同的是, WaitForMutipleObjects函数可支持等待多个线程执行结束, 或者等待多个线程中其中一个执行结束
其语法格式如下:
以下是WaitForMutipleObjects
函数的使用实例
线程上下文
什么是线程上下文
线程上下文(Thread Context)是指在一个线程中,当前执行代码的相关信息集合,包括寄存器状态、程序计数器、线程优先级、线程的上下文安全堆栈等等。
要注意的是,若要获取线程上下文信息,需要先将线程挂起。
CONTEXT结构
CONTEXT
是Windows API中用于保存线程上下文的结构体,包含了处理器的寄存器、标志和其他与处理器相关的状态信息。
由于CONTEXT
结构体成员太多了, 可以通过设置ContextFlags
成员的值来获取指定范围的寄存器, 以下是常用的寄存器集的描述:
CONTEXT_INTEGER
: 包含通用寄存器集(如EAX、EBX等)和指令指针EIP。CONTEXT_CONTROL
: 包含指令指针EIP、代码段寄存器CS、栈指针ESP和栈段寄存器SS。CONTEXT_SEGMENTS
: 包含数据段寄存器DS、源段寄存器SS、堆栈段寄存器SS和附加段寄存器ES、FS和GS。CONTEXT_FLOATING_POINT
: 包含浮点寄存器集。CONTEXT_DEBUG_REGISTERS
: 包含调试寄存器集。
可以通过GetThreadContext
和SetThreadContext
函数来获取和设置线程上下文
GetThreadContext
用于获取指定线程的上下文信息,调用成功后会将获取到的上下文信息存储在CONTEXT结构体中,其语法格式如下所示:
SetThreadContext
用于设置指定线程的上下文,即线程寄存器和指令指针等信息,其语法结构如下所示:
使用实例
临界区
什么是临界区
当多个线程同时使用同一个资源(全局变量)时, 很可能会出现某些错误, 这时我们可以将这个资源变成临界资源, 然后通过临界区来使用这个临界资源
临界区(critical section)是操作系统用于同步线程之间访问共享资源的一种机制,是一段被保护的代码区域。同一时刻只允许一个线程进入临界区,其他线程需要等待当前线程退出临界区才能进入执行。
当线程进入临界区时,它会对临界资源进行加锁,这时其他线程无法访问该资源。只有当当前线程完成对临界资源的访问并释放锁后,其他线程才能进入临界区访问临界资源
什么是线程锁
线程锁实际上就是基于临界区实现的,通过在临界区代码块前加锁,在代码块结束后释放锁来保证临界区的互斥性
以下是实现线程锁的代码流程:
1.定义一个临界区
2.初始化临界区
3.定义临界区范围
使用实例
以下代码是关于线程锁使用的实例, 线程1的代码执行完毕后线程2才能执行自己的代码
互斥体
什么是互斥体
如下图所示, 互斥体是一种用于控制线程同步的对象, 它能确保同一时刻只有一个线程进入临界区访问临界资源。
互斥体通过两种状态来控制对共享资源的访问, 分别是已锁定(0)和未锁定(1), 当一个线程从互斥体中获取锁(令牌), 其他线程就无法访问共享资源, 只有线程释放了锁(令牌), 其他线程才能继续竞争获取锁
在Windows系统中, 互斥体的实现是一个内核对象, 因此它可以跨进程使用
涉及API
CreateMutex
CreateMutex函数用于创建或者打开一个互斥体对象,其语法格式如下:
ReleaseMutex
ReleaseMutex函数用来释放进程持有的互斥体对象,其语法格式如下:
使用实例
以下代码使用互斥体实现跨进程线程同步, 首先是进程A的代码, 此处为了方便测试, 我利用getchar()
来阻塞代码执行(相当于断点), 这样互斥体就没有释放锁
下面是进程B的代码, 由于进程A的代码还没有释放锁, 因此进程B的代码无法执行
互斥体和线程锁的区别
互斥体可以用于跨进程的线程同步,线程锁只能用于同一进程内的线程同步
互斥体可以设置等待超时, 而线程锁不行。当一个线程在执行过程中因为异常情况(例如程序崩溃)而突然终止时,如果它持有了某个共享资源的互斥体,其他线程在等待这个资源时可能会进入无限等待状态,因为该互斥体没有被释放。为了避免这种情况,互斥体通常会在创建时指定一个超时时间,一旦等待时间超过了这个时间,等待线程就会放弃等待并执行其他任务,从而避免了无限等待的情况发生
互斥体的效率没有线程锁的高
事件
什么是事件
事件是一种同步对象,用于线程之间的通信和协调。事件对象有两种状态:有信号状态和无信号状态。当事件对象处于有信号状态时,等待该事件的线程可以被唤醒并继续执行。当事件对象处于无信号状态时,等待该事件的线程将被阻塞,直到事件被信号化为止
通常,一个线程使用 SetEvent
函数将事件对象信号化,而另一个或多个线程使用 WaitForSingleObject
或 WaitForMultipleObjects
函数等待该事件对象的信号状态
涉及api
CreateEvent
CreateEvent
函数用于创建一个事件对象, 事件对象是内核对象的一种,可用于同步进程和线程,或者通知线程事件的发生
其语法如下所示:
关于第二个参数的描述:当事件对象处于非信号状态时,一个线程调用WaitForSingleObject等待该事件;如果事件已经处于信号状态,则WaitForSingleObject返回。在自动重置模式下,当WaitForSingleObject返回时,事件会自动返回到非信号状态。而在手动重置模式下,事件会一直保持在信号状态,直到由调用ResetEvent
显式重置
SetEvent
SetEvent
函数于设置事件对象为有信号状态, 若事件对象处于无信号状态, 则将其设置为有信号状态, 若事件对象处于有信号状态,则函数不起作用。此函数通常与CreateEvent
函数一起使用来实现线程同步, 在线程同步中,此函数的作用就是把自己的线程挂起, 同时唤醒其他线程
其语法格式如下:
使用实例
这段代码实现了一个生产者-消费者的解决方案,其中两个线程分别为生产者线程和消费者线程,通过共享的仓库(Storage变量)实现数据交互。
生产者线程不断地生产产品,存储到仓库中,并唤醒消费者线程,消费者线程不断地从仓库中消耗产品,并唤醒生产者线程
由下述执行结果可看出, 两个线程有序的完成了任务, 生产者每生产一个产品, 消费者就消耗一个产品。两个线程交替执行, 而不会出现某个线程同时执行多次
线程互斥和线程同步
线程同步是指协调多个线程的执行顺序,以避免在并发执行时出现不一致的结果或冲突的情况。线程同步可以通过多种机制实现,如互斥锁、信号量、事件等。在线程同步中,通常会存在一些共享资源,多个线程需要协调访问这些共享资源,以避免访问的冲突
线程互斥是一种线程同步机制,用于确保同一时间只有一个线程能够访问共享资源。线程互斥可以使用互斥体、临界区、信号量等机制来实现
Windows应用程序
什么是消息队列
当我们在使用鼠标或键盘时,操作系统会将这些动作转换为一个消息(message),并将其发送到相应的消息队列中。这个消息包含了一些信息,例如动作的类型、坐标、时间戳等等。
在Windows系统中,每个窗口都有一个消息队列,操作系统将接收到的消息按照先后顺序依次存储在该队列中,等待程序读取和处理
每个线程只有一个消息队列,消息队列是属于线程的,每个线程在创建窗口时才会分配一个消息队列,并不是每个线程都有消息队列。当消息被发送到一个窗口时,系统会将消息放入该窗口所属的线程的消息队列中,线程可以通过GetMessage函数从消息队列中获取消息并进行处理。
有一点要注意:一个窗口只能属于一个线程,但一个线程可以拥有多个窗口
什么是消息处理函数
消息处理函数用于处于各种事件消息的函数,也称为窗口过程函数。当一个窗口收到一个事件消息时,Windows操作系统会调用该窗口的消息处理函数来处理该消息
通常其语法形式如下:
当用户进行鼠标点击等操作时,操作系统会将消息发送到应用程序的消息队列中,然后应用程序的消息循环会处理这些消息,例如将其传递给某个特定窗口的消息处理函数
在处理窗口消息时,通常需要检查消息是由哪个窗口发送的,以便正确地响应消息。一个应用程序可以拥有多个窗口,并且每个窗口都有自己的消息处理函数
什么是消息循环
消息循环会不断地从系统队列中获取消息,然后将消息传递给相应的消息处理函数进行处理。在Windows操作系统中,消息循环通常是由函数GetMessage和DispatchMessage进行实现的
以下代码是Visual Studio提供的默认消息函数:
常用API
WinMain函数
WinMain是Windows程序的入口函数, WinMain的返回值是一个整数,表示程序的退出状态码。在WinMain函数内部,我们可以创建窗口、初始化程序并执行消息循环等操作
它的语法格式如下:
CreateWindow
CreateWindow函数是Windows API中用于创建一个窗口的函数,返回一个HWND类型的窗口句柄,该句柄可以用于操作该窗口,如显示、隐藏、移动、调整大小等
每当使用CreateWindow创建窗口时,操作系统会在内核生成一个窗口对象,该窗口对象包含了窗口的状态信息(如窗口大小, 位置, 标题), 同时也包含了窗口的过程函数指针,用于处理窗口的消息
其语法格式如下: