cyxyrq-code-loading

 

Win32学习4

13、事件

①通知类型:

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES IpEventAttributes,//SD
BOOL bManualReset, // reset type
BOOL bInitialState, // initial state
LPCTSTR IpName // object name
);

bManualReset 这个成员如果是 TRUE,这个事件就是通知类型,如果这个是 FALSE则就不是。 bInitialState 这个成员代表了是否产生信号。(如WaitForSingleObject 这样的 API ,如果该成员没有产生信号的话,就会一直处于堵塞状态直到产生信号才会接着继续执行。) IpName 这个成员代表了该事件的名字。(如果是当前的进程的使用的话,可以不设置名字,如果是多个进程使用就需要设置名字了。)

#include<stdio.h>
#include<windows.h>

HANDLE g_hEvent;

DWORD WINAPI ThreadProc_1(LPVOID IpParameter)
{
TCHAR szBuffer[10] = {0};

//当事件变成已通知时
WaitForSingleObject(g_hEvent,INFINITE);

//线程执行
printf("ThreadProc_1执行了\n");

getchar();

return 0;
}

DWORD WINAPI ThreadProc_2(LPVOID IpParameter)
{
TCHAR szBuffer[10] = {0};

//当事件变成已通知时
WaitForSingleObject(g_hEvent,INFINITE);

//线程执行
printf("ThreadProc_2执行了\n");

getchar();

return 0;
}
int main()
{

//创建事件
//默认安全属性 TRUE通知/FALSE互斥 初识没信号 没有名字
g_hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);

HANDLE hThread[2];

//创建2个线程
hThread[0] = CreateThread(NULL,0,ThreadProc_1,NULL,0,NULL);
hThread[1] = CreateThread(NULL,0,ThreadProc_2,NULL,0,NULL);

//设置事件为已通知
//SetEvent(g_hEvent);

//等待线程结束 销毁内核对象
WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(g_hEvent);

return 0;
}

如同上方这段测试代码,如果CreateEvent的时候设置没信号,两个线程都不会正常执行,都会变为堵塞状态。如果将其设置为有信号,将会正常执行线程。

如果是通知类型的两个线程都能正常的执行(不会修改对应的状态),而如果设置为互斥类型,一个没执行完毕,另一个便无法正常开始执行。除非第一个线程在执行中执行了SetEvent,即修改对应的状态之后,另一个才能正常的执行。(应用场景即为,如果想要多个线程无法同时执行,就用互斥类型;如果想要多个线程能够执行,需要依次发送一个信号,即为实现线程同步,就用通知类型)

②线程同步

<1>线程互斥:线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排 它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去 使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。 <2>线程同步:线程同步是指线程之间所具有的一种制约关系,一个线程的执行依 赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时 才被唤醒。

③同步的前提是互斥:

image-20230201170739901)

互斥是一种无序的概念,即需要满足同一时间仅有一个线程使用该共享资源;而同步即为互斥加上有序,就是当某个线程执行完后,发送信号另一个线程便能够执行,而并非产生只能一个线程无序的不断使用该资源。

可以理解为: 同步 = 互斥 + 有序

线程同步形象的例子就比如:生产者和消费者。利用如下的代码来解释一下为什么利用事件能够更好的实现线程同步。

#include<stdio.h>
#include<windows.h>

HANDLE hMutex;
int g_Max = 10; //生产几个产品
int g_Number = 0; //容器 存储产品

//生产者线程函数
DWORD WINAPI ThreadProduct(LPVOID pM)
{
for(int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(hMutex,INFINITE);
g_Number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n",id,g_Number);
ReleaseMutex(hMutex);
}
return 0;
}

DWORD WINAPI ThreadConsumer(LPVOID pM)
{
for(int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(hMutex,INFINITE);
g_Number = 0;
DWORD id = GetCurrentThreadId();
printf("---消费者%d将数据%d放入缓冲区\n",id,g_Number);
ReleaseMutex(hMutex);
}
return 0;
}
int main()
{

//创建一个互斥体
hMutex = CreateMutex(NULL,FALSE,NULL);

HANDLE hThread[2];

hThread[0] = ::CreateThread(NULL, 0,ThreadProduct,NULL,0,NULL);
hThread[1] = ::CreateThread(NULL, 0,ThreadConsumer,NULL,0,NULL);

WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);

getchar();
//销毁
CloseHandle(hMutex);

return 0;
}

通过运行上面的这些代码可以发现,当两个线程互斥的时候,是无法同时执行的。但是实际上这样的是有逻辑漏洞的,其并非是有序的。消费者想要消费就必须需要生产者进行生产之后才可以。如果我们利用事件中的通知来实现线程同步,就能够得到一个有序的互斥。

#include<stdio.h>
#include<windows.h>

HANDLE g_hSet,g_hClear;
int g_Max = 10; //生产几个产品
int g_Number = 0; //容器 存储产品

//生产者线程函数
DWORD WINAPI ThreadProduct(LPVOID pM)
{
for(int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(g_hSet,INFINITE);
g_Number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n",id,g_Number);
SetEvent(g_hClear);
}
return 0;
}

DWORD WINAPI ThreadConsumer(LPVOID pM)
{
for(int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(g_hClear,INFINITE);
g_Number = 0;
DWORD id = GetCurrentThreadId();
printf("---消费者%d将数据%d放入缓冲区\n",id,g_Number);
SetEvent(g_hSet);
}
return 0;
}
int main()
{

HANDLE hThread[2];

g_hSet = CreateEvent(NULL,FALSE,TRUE,NULL);
g_hClear = CreateEvent(NULL,FALSE,FALSE,NULL);

hThread[0] = ::CreateThread(NULL, 0,ThreadProduct,NULL,0,NULL);
hThread[1] = ::CreateThread(NULL, 0,ThreadConsumer,NULL,0,NULL);

WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);

getchar();
//销毁
CloseHandle(g_hSet);
CloseHandle(g_hClear);

return 0;
}

我们稍加修改,将代码改成如上所示,初识生产者是有信号的,就可以直接执行生产者线程函数;消费者是没有信号的,必须等到每次生产者生产之后,利用SetEvent 传递一个信号,消费者线程函数才可以开始执行。而且如果是互斥体的话,CPU 正常分配数毫秒,如果在小于这个时间内,走到了函数的末尾,他会继续执行直到消耗完分配的时间。但是如果修改成时间这种的话,就不会出现这种情况,每当发出信号,当前的事件就会将自己挂起,挂起之后就会选择执行另一个线程,直到传回信号再接着执行。

14、窗口的本质

从这一节开始学习图形化界面。

①窗口的本质

每个程序都有自己 4GB 的虚拟空间,低2G是每个进程自己私有的用户空间,高2G 实际上是各个进程共享的内核空间。

 

image-20230202191443526

每个进程都是由模块组成,分别由一个exe 和多个 dll组成。其实实际上高2G 的内核空间也有模块的概念,许多的模块。但是和我们编程息息相关的主要有两个模块,如下所示:

主要是 ntoskrnl 和 win32k 这两个模块。其实之前我们实现一些创建进程和创建线程的时候,虽然那些函数可能是 kernel32.dll中的,但是实际上这个 dll 只是提供了一个接口,真正实现操作的还是实际上调用了ntoskrnl 这个内核模块的来创建线程和创建进程的。

而我们创建窗口或者消息管理的时候,主要是win32k.sys 这个内核模块做的。如果我们想要使用这个模块的话,就使用 user32.dll 和 gdi32.dll这两个dll就可以了。

如果我们想要使用 windows 已经绘制好的窗口,我们就可以使用user32.dll,并且这种编程被称之为GUI(Graphical User Interface图形用户界面),主要就是使用user32.dll;而如果我们不想要使用已经绘制好的界面,想要自己制作界面绘图的话,这种编程被称之为GDI(Graphics Device Interface图形设备接口),主要就是使用gdi32.dll。但是无论是那一种最终都是由win32.sys这个内核模块实现的。

在句柄表中我们知道,每个进程都有属于自己的一张私有的句柄表,这张句柄表只能对于自己的这个进程有用,而对于别的进程是无意义的。所以其实可以理解为HANDLE这个值实际上就是私有的。而在图形化界面中,句柄通常为HWND。这个句柄实际上是全局表的索引。针对窗口的这个表只有一个,是一张全局句柄表。

②GDI 图形设备接口(Graphics Device Interface)

<1>设备对象(HWND) <2> DC(设备上下文,Device Contexts) <3> 图像 对象

image-20230202204305676

#include<stdio.h>
#include<windows.h>

int main()
{
HWND hwnd;
HDC hdc;
HPEN hpen;

//1.设备对象 画在哪
hwnd = (HWND)0x000E0244;//找到窗口的一个句柄(此处为空默认为桌面)

//2.获取设备对象上下文 DC
hdc = GetDC(hwnd);

//3.创建画笔 设置线条的属性
hpen = CreatePen(PS_SOLID,5,RGB(0xFF,00,00));

//4.关联
SelectObject(hdc,hpen);

//5.开始画
LineTo(hdc,400,400);//gdi32.dll

//6.释放资源
DeleteObject(hpen);
ReleaseDC(hwnd,hdc);


return 0;
}

我们可以利用上方这段代码来实现在窗口上绘制,利用gdi32.dll提供的一些 API 实现绘画。如果要是实现GDI,其步骤为首先指明画在哪,即拿到窗口的一个句柄,其次因为绘制并不仅仅是绘制于窗口上,其为在内存中,之后是由操作系统翻译再将图案绘制出来,所以我们需要获取设备对象上下文以绘制。利用 GetDC 这个API 就能通过句柄拿到设备上下文。之后如果我们不想使用默认的绘画模式,我们需要设置图像对象,再将DC和设置的图像对象的属性进行关联,就实现了绘画模式的替换,再利用对应的 API 我们就能够实现绘制窗口了。如果我们想要绘制不同的形状,我们只需要更改一下图像对象。

其实窗口是一直不停画的过程。

15、消息队列

①什么是消息?

当我们点击鼠标的时候,或者当我们按下键盘的时候,操作系统都要把 这些动作记录下来,存储到一个结构体中,这个结构体就是消息。

②消息队列:每个线程只有一个消息队列

image-20230206213452933

每次进行操作的时候,操作系统会将这些动作进行记录,并将其传给某个进程的一个线程,再通过这个动作进行相应的处理。

③窗口与线程:

在操作系统中,可能同时有多个界面,同时应用了多个程序,当利用鼠标点击的时候,操作系统如何传递信息给对应的进程,如何传递给对应进程的线程。实际上,在内核中是有对应的结构体存储着每个窗口所对应的信息,能够知道窗口对应的是哪一个线程,知道每个窗口所拥有的数据。

image-20230206225315325

操作系统封装数据到内核结构体的之后,会遍历消息列表,找到对应的进程,并找到对应的线程,最后根据其消息,进行相应的操作。

一个窗口只能属于一个线程,但是一个线程可以拥有多个窗口。

16、第一个Windows程序

利用windows 提供的 API 编写一个windows 程序。

利用vc6,新建一个win32程序。

image-20230207220525638

image-20230207220547224

①WinMain函数

所有的控制台程序都是由 main 函数开始的,所有的 Win32 程序都是由 WinMain函数开始的。

int WINAPI WinMain(
HINSTANCE hinstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR IpCmdLine,// command line
int nCmdShow // show state
);

一般在Win32中以H开头的都是句柄,例如HANDLE 是指向内核对象的句柄,HWND 是指向窗口的句柄,HDC 是指向设备上下文的句柄,HINSTANCE 是指向一个模块的句柄。(所有的句柄指向的真正的对象在r0,这个句柄仅仅是一个索引)。而句柄都是一个 DWORD 。

第一个参数hinstance就是当前这个模块在内存中的位置,实际上也可以理解到,任何一个模块的句柄中存的值,实际上就是整个模块在进程空间中的地址。第二个参数hPrevInstance 永远为 NULL,可以不加设置。任何一个进程都是由其他进程创建出来的,进程创建函数中的第二个参数 IpCommandLine 实际上就是 WinMain 中的第三个参数 IpCmdLine,创建进程的时候,就会将这个命令行参数传递。而创建进程函数中的 IpStartupInfo 还可以创建启动参数,实际上就是 WinMain 的第四个参数 nCmdShow,在创建的时候就会传递进去。

②调试信息的输出:

char szOutBuff[Ox80];
sprintf(szOutBuff,"Error: %d",GetLastError());
OutputDebugString(szOutBuff)//包含头文件 stdio.h

 

③创建窗口程序的步骤: <1>定义窗口类 <2> 创建窗口并显示 <3>接收消息并处理

#include "stdafx.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR     lpCmdLine,
                    int       nCmdShow)
{
// TODO: Place code here.
//创建一个Windows程序
//1、第一步:定义你的窗口是什么样的?
TCHAR classname[] = TEXT("My First Window");//设置名字,类型需要为TCHAR
WNDCLASS wndclass = {0}; //初始化全为0,不必全部记忆,常用仅有四个成员
wndclass.hbrBackground = (HBRUSH)COLOR_BACKGROUND; //背景色(需要注意因为背景色属于画刷类型,所以需要强转类型)
wndclass.lpszClassName = classname;//名字
wndclass.hInstance = hInstance;//属于哪一个程序(当前程序)
wndclass.lpfnWndProc = WindowProc//窗口程序,用于处理操作系统返回的消息,但是并非调用函数,仅仅是告诉操作系统函数的名字,当真正需要时,操作系统会根据这个名字来调用函数。
RegisterClass(&wndclass);//注册窗口,告诉操作系统该结构体的存在。

//2.第二步:创建并显示窗口
/*HWND CreateWindow(  
LPCTSTR lpClassName,  
LPCTSTR lpWindowName,
DWORD dwStyle,  
int x,  
int y,  
int nWidth,  
int nHeight,
HWND hWndParent,  
HMENU hMenu,  
HANDLE hInstance,  
PVOID lpParam
); */


HWND hwnd = CreateWindow(
className,
TEXT("My First Window),
WS_DISABLED,
10,
10,
600,
300,
NULL,
NULL
hInstance,
NULL
);//实际上仍是内核对象替我们绘制窗口,我们仅仅是利用了Win32的API 调用了函数。CreateWindow这个函数如果创建成功的话,就会返回一个句柄;反之如果创建失败,就会返回NULL。

/* BOOL ShowWindow(   HWND hWnd,   int nCmdShow ); */
ShowWindow(hwnd,SW_SHOW);//显示窗口

//3.第三步:接收消息并处理
/* BOOL GetMessage(  
LPMSG lpMsg,  
HWND hWnd,  
UINT wMsgFilterMin,
UINT wMsgFilterMax
); */
//第一个参数是一个指针,用于存放消息;后三个都属于是过滤条件,因为一个线程可能创建很多个窗口,所以消息队列里面就有很多消息,需要告诉这个函数获取到哪一个窗口的消息。第二个参数就是指定哪一个窗口的消息,如果填NULL,就是取所有的窗口的消息。第三四个就是描述取哪样的消息。

MSG msg;
GetMessage(&msg,NULL,0,0);//如果没有取到消息,就会在这堵塞。
BOOL bRet;
while (bRet = GetMessage( &msg, NULL, 0, 0)) !=0)
{
if (bRet ==-1)
{
//handle the error and possibly exit
}
else
{
TranslateMessage (&msg);//转换消息
DispatchMessage (&msg);//分发消息(因为调用窗口程序需要进入零环才能调用,所以需要先分发消息,而不是直接调用,因为直接调用无法找到对应的窗口)
//而且调用窗口程序,对应的函数不能简单返回一个NULL,需要选择返回调用一个默认的消息处理函数 DefWindowProc(hwnd,uMsg,wParam,lParam) ,用以保证我们的窗口能够实现最简单的操作,而非每个操作都需要我们单独实现。
}
}

return 0;
}

 

image-20230211214802231

并不是每一个线程对象都有消息队列的,只有当创建窗口对象的时候才会有消息队列,而只要有消息队列,就会有操作将往消息队列中存消息。例如:键盘、鼠标、内核程序等。他们会在操作系统下,找到对应的窗口对象,再找到对应的进程的线程对象,再在消息队列中实现对应的操作。

image-20230211224540732

DispatchMessage (&msg) 会拿着对应的窗口句柄,回到零环,在内核中找到对应的窗口对象,再利用内核程序调用 WindowProc 。

posted on 2023-07-17 07:57  清雨中欣喜  阅读(42)  评论(0编辑  收藏  举报

导航