下面讲了几种线程同步的方法:
一、互斥对象
二、事件对象
三、邻居区(关键代码段)
其中互斥对象和事件对象都是内核对象。
-------------------------------------------------------------------------------------------------------------------------
当使用CreateProcess调用时,系统将创建一个进程和一个主线程。CreateThread将在主线程的基础上创建一个新线程,大致做如下步骤:
1在内核对象中分配一个线程标识/句柄,可供管理,由CreateThread返回2把线程退出码置为STILL_ACTIVE,把线程挂起计数置1
3分配context结构
4分配两页的物理存储以准备栈,保护页设置为PAGE_READWRITE,第2页设为PAGE_GUARD
5lpStartAddr和lpvThread值被放在栈顶,使它们成为传送给StartOfThread的参数
6把context结构的栈指针指向栈顶(第5步)指令指针指向startOfThread函数
语法:
hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,pParam, dwFlags, &idThread) ;
参数说明:
第一个参数是指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,它被设为NULL。
第二个参数是用于新线程的初始堆栈大小,默认值为0。在任何情况下,Windows根据需要动态延长堆栈的大小。
CreateThread的第三个参数是指向线程函数的指针。函数名称没有限制,但是必须以下列形式声明:
DWORD WINAPI ThreadProc (PVOID pParam) ;
CreateThread的第四个参数为传递给ThreadProc的参数。这样主线程和从属线程就可以共享数据。
CreateThread的第五个参数通常为0,但当建立的线程不马上执行时为旗标CREATE_SUSPENDED。线程将暂停直到呼叫ResumeThread来恢复线程的执行为止。
第六个参数是一个指标,指向接受执行绪ID值的变量。
---------
CreateThread 的最后一个参数是 "线程的 ID";
既然可以返回句柄, 为什么还要输出这个 ID? 现在我知道的是:
1、线程的 ID 是唯一的; 而句柄可能不只一个, 譬如可以用 GetCurrentThread 获取一个伪句柄、可以用 DuplicateHandle 复制一个句柄等等.
2、ID 比句柄更轻便.
在主线程中 GetCurrentThreadId、MainThreadID、MainInstance 获取的都是主线程的 ID.
互斥对象:
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。
互斥对象包含一个使用数量,一个线程ID和一个计数器。
ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
#include <iostream>
#include <windows.h>
using namespace std;
DWORD WINAPI fun1proc(LPVOID lpparentet);
DWORD WINAPI fun2proc(LPVOID lpparentet);
int tickets = 100;
HANDLE hMutex;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL,0,fun1proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,fun2proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
hMutex = CreateMutex(NULL, FALSE, "tickets");
Sleep(4000);
}
DWORD WINAPI fun1proc(LPVOID lpparentet)
{
while (1)
{
WaitForSingleObject(hMutex, INFINITE);
if (tickets > 0)
{
Sleep(1);
cout<<"子线程线程1:" << tickets-- << endl;
}
else
{
break;;
}
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI fun2proc(LPVOID lpparentet)
{
while (1)
{
WaitForSingleObject(hMutex, INFINITE);
if (tickets > 0)
{
//Sleep(1);
cout<<"子线程线程2:" << tickets-- << endl;
}
else
{
break;;
}
ReleaseMutex(hMutex);
}
return 0;
}
程序是计算机指令的集合,它以文件的形式存储在磁盘上。
进程:通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。
进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。
进程由两个部分组成:
1、操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
2、地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。
单个进程可能包含若干个线程,这些线程都“同时” 执行进程地址空间中的代码。
每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程。此后,该线程可以创建其他的线程。
系统赋予每个进程独立的虚拟地址空间。对于32位进程来说,这个地址空间是4GB。
每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。
4GB是虚拟的地址空间,只是内存地址的一个范围。在你能成功地访问数据而不会出现非法访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。
GB虚拟地址空间中,2GB是内核方式分区,供内核代码、设备驱动程序、设备I/O高速缓冲、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方
线程由两个部分组成:
1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
2、线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。
当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。
线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。
线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。
因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。
事件对象:
事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。(不要使用人工重置的事件对象,应使用自动重置的事件对象)
#include <iostream>
#include <windows.h>
using namespace std;
DWORD WINAPI fun1proc(LPVOID lpparentet);
DWORD WINAPI fun2proc(LPVOID lpparentet);
int tickets = 100;
HANDLE g_hEvent;
int main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL,0,fun1proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,fun2proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
g_hEvent = CreateEvent(NULL,FALSE,FALSE,"ticket");
if(g_hEvent)
{
if(ERROR_ALREADY_EXISTS == GetLastError())
{
cout<<"only instance can run!"<<endl;
return 0;
}
}
SetEvent(g_hEvent);
Sleep(4000);
CloseHandle(g_hEvent);
return 0;
}
DWORD WINAPI fun1proc(LPVOID lpparentet)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE);
if (tickets > 0)
{
Sleep(1);
cout<<"子线程线程1:" << tickets-- << endl;
}
else
{
break;
}
SetEvent(g_hEvent);
}
return 0;
}
DWORD WINAPI fun2proc(LPVOID lpparentet)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE);
if (tickets > 0)
{
Sleep(1);
cout<<"子线程线程2:" << tickets-- << endl;
}
else
{
break;
}
SetEvent(g_hEvent);
}
return 0;
}
临界区(关键代码段):
关键代码段(临界区)工作在用户方式下。关键代码段(临界区)是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。
#include <iostream>
#include <Windows.h>
using namespace std;
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets = 100;
CRITICAL_SECTION g_cs;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
InitializeCriticalSection(&g_cs);
Sleep(4000);
DeleteCriticalSection(&g_cs);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&g_cs);
if (tickets > 0)
{
Sleep(1);
cout<<"子线程线程1:" << tickets-- << endl;
}
else
{
break;
}
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
EnterCriticalSection(&g_cs);
while (TRUE)
{
if (tickets > 0)
{
Sleep(1);
cout<<"子线程线程2:" << tickets-- << endl;
}
else
{
break;
}
LeaveCriticalSection(&g_cs);
}
return 0;
}