了解多线程,起码要对进程和线程的概念,以及进程和线程的创建方式有一些了解。那么继续测试吧!
对于win32程序而言,实现线程同步的方式至少有三种,之前测试过互斥对象Mutex,其他两种分别是使用时间对象Event和临界区对象CriticalSection(也被称为关键代码段)
①使用Event对象实现线程同步
#include<windows.h> #include<iostream.h> DWORD WINAPI thread1(LPVOID lpParam); DWORD WINAPI thread2(LPVOID lpParam); int tickets = 100; HANDLE hEvent; void main() { cout << "Main thread is running...\n"; HANDLE hThread1 = CreateThread(NULL, 0, thread1, NULL, 0, NULL); HANDLE hThread2 = CreateThread(NULL, 0, thread2, NULL, 0, NULL); CloseHandle(hThread1); CloseHandle(hThread2); hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);// SetEvent(hEvent); Sleep(4000); cout << "Main thread run over"<<endl; } DWORD WINAPI thread1(LPVOID lpParam) { cout << "thread1 is running...\n"; while (1) { WaitForSingleObject(hEvent,INFINITE); if (tickets > 0) { cout << "thread1 sell tickets" << tickets--<<endl; } else { break; } SetEvent(hEvent); } return 0; } DWORD WINAPI thread2(LPVOID lpParam) { cout << "thread2 is running...\n"; while (1) { WaitForSingleObject(hEvent,INFINITE); ResetEvent(hEvent); if (tickets > 0) { cout << "thread2 sell tickets" << tickets-- << endl; } else { break; } SetEvent(hEvent); } return 0; }
备注:CreateEvent函数可以创建事件对象,第二个参数是一个人工重置事件标识,设为TRUE表示人工重置事件,FALSE表示自动重置事件;第三个参数为初始化状态标识,TRUE表示初始化有信号,FALSE表示初始化无信号(有无信号,可以理解为能否被线程请求到),可以使用SetEvent/ResetEvent设置时间为有信号状态和无信号状态。
关于人工重置和自动重置:当人工重置的事件得到通知时,等待该时间的所有线程均变为可调度线程(线程1和线程2均可以运行)。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程(同一时间,一个线程运行)。
②使用CriticalSection对象实现线程同步
#include<windows.h> #include<iostream.h> DWORD WINAPI thread1(LPVOID lpParam); DWORD WINAPI thread2(LPVOID lpParam); int tickets = 100; CRITICAL_SECTION cs; void main() { cout << "Main thread is running...\n"; HANDLE hThread1 = CreateThread(NULL, 0, thread1, NULL, 0, NULL); HANDLE hThread2 = CreateThread(NULL, 0, thread2, NULL, 0, NULL); CloseHandle(hThread1); CloseHandle(hThread2); InitializeCriticalSection(&cs); Sleep(4000); DeleteCriticalSection(&cs); cout << "Main thread run over"<<endl; } DWORD WINAPI thread1(LPVOID lpParam) { cout << "thread1 is running...\n"; while (1) { EnterCriticalSection(&cs); if (tickets > 0) { cout << "thread1 sell tickets" << tickets--<<endl; } else { break; } LeaveCriticalSection(&cs); } return 0; } DWORD WINAPI thread2(LPVOID lpParam) { cout << "thread2 is running...\n"; while (1) { EnterCriticalSection(&cs); if (tickets > 0) { cout << "thread2 sell tickets" << tickets-- << endl; } else { break; } LeaveCriticalSection(&cs); } return 0; }
备注:临界区是非内核对象,只在用户态进行锁操作,速度快;互斥体是内核对象,在核心态进行锁操作,速度慢。
③关于内核对象(kernel object)
内核对象是一个内存块,它由操作系统内核分配,并只能由操作系统访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。应用程序不能在内存中定位这些数据结构并直接更改其内容。windows提供了一组函数,给应用程序访问这些内核对象。调用一个创建内核对象的函数后,会返回一个句柄,它标识了所创建的对象。《windows核心编程》
内核对象由一些共同的成员。如系统给与核心对象一个计数值(usage count)作为管理之用,内核对象用一个安全描述符(security descriptor,SD)来保护内核对象等。如创建event对象,包含一个指向SECURITY_ATTRIBUTES结构的指针:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
内核对象包含以下几种:
内核对象 |
产生方法 |
说明 |
event |
CreateEvent |
执行线程同步化 |
mutex |
CreateMutex |
|
semaphore |
CreateSemaphore |
|
file |
CreateFile |
文件操作 |
file-mapping |
CreateFileMapping |
内存映射文件 |
process |
CreateProcess |
进程对象 |
thread |
CreateThread |
线程对象 |
执行一个程序,必然就产生一个进程(process)。最直接的程序执行方式就是在shell (如
Win95 的文件总管或Windows 3.x 的文件管理员)中以鼠标双击某一个可执行文件图标
(假设其为App.exe),执行起来的App 进程其实是shell 调用CreateProcess 激活的。
让我们看看整个流程:
1. shell 调用CreateProcess 激活App.exe。
2. 系统产生一个「进程核心对象」,计数值为1。
3. 系统为此进程建立一个4GB 地址空间。
4. 加载器将必要的码加载到上述地址空间中,包括App.exe 的程序、资料,以及
所需的动态联结函数库(DLLs)。加载器如何知道要加载哪些DLLs 呢?它
们被记录在可执行文件(PE 文件格式)的.idata section 中。
5. 系统为此进程建立一个执行线程,称为主执行线程(primary thread)。执行线程才是
CPU 时间的分配对象。
6. 系统调用C runtime 函数库的Startup code。
7. Startup code 调用App 程序的WinMain 函数。
8. App 程序开始运作。
9. 使用者关闭App 主窗口,使WinMain 中的消息循环结束掉,于是WinMain 结束。
10. 回到Startup code。
11. 回到系统,系统调用ExitProcess 结束进程。《深入浅出MFC》
对于这段文字的解释如下:
这里面说的那个shell,应该是Explorer.exe。双击应用图标的时候,Explorer.exe的一个线程会侦探到这个操作,它根据注册表中的信息取得文件名,并根据这个文件名调用CreateProcess。CreateProcess用于创建一个进程,和该进程的主线程,相关用法可以查看msdn。
创建进程的时候,系统会产生一个process内核对象。进程内核对象可以看做操作系统管理进程的内核对象,它是系统用来存放进程统计信息的地方。进程在初始化的时候,系统会为其分配一个空的句柄表。(handle table)。
句柄表有点难以理解,我猜大概是这样,一开始系统给一个进程分配的句柄表是空的,等到这个进程里的一个线程调用API函数(特定的创建内核对象的函数)再去创建一个内核对象,就会在空句柄表加上新创建内核对象数据结构内部内存地址。创建内核对象的时候,都会返回一个句柄,这个句柄可以被同一个进程的所有线程使用,得益于句柄表中保存了新创建内核对象的信息。
主调进程(就是创建内核对象的进程)可以使用CloseHandle()来关闭一个内核对象。主调进程一旦调用CloseHandle(),那这个内核对象的信息会从主调进程的句柄表中抹去,而主调进程也不再拥有对内核对象的控制权。但是这个内核对象可能并不会被销毁,因为系统为内核对象维护了一个计数值。调用CloseHandle()会让内核对象的计数值-1,只有当内核对象的计数值为0的时候,内核对象才会被销毁。内核对象可能被多个进程使用,只有所有使用它的进程都调用CloseHandle()时,它才会被销毁。
......
最后,还是写个简单的CreateProcess应用示例:
#include<windows.h>
#include<iostream.h>
#include<string.h>
void main(){
STARTUPINFO sui;
ZeroMemory(&sui,sizeof(STARTUPINFO));
PROCESS_INFORMATION processInfo;
TCHAR szCommandLine[] = "NOTEPAD 1.txt" ;
BOOL success=CreateProcess(NULL,szCommandLine,NULL,NULL,FALSE,0,NULL,NULL,&sui,&processInfo);
if(success){
cout<<"启动记事本成功!"<<endl;
}else{
cout<<"启动记事本失败!"<<endl;
}
}
⑤优先权
.................................
参考
孙鑫 VC++深入详解
推荐
《一个exe可执行文件的生与死》
原文:https://www.cnblogs.com/vocus/p/12859386.html