一.进程和线程的基本概念
1.进程的组成
进程由两个部分组成:
(1).操作系统用来管理进程的内核对象,即进程控制块PCB
内核对象也是系统用来存放关于进程的统计信息的地方。内核对象是操作系统内部分配的一个内存块,该内存块是一种数据结构,其成员负责维护该对象的各种信息。由于内核对象的数据结构只能被内核访问使用,因此应用程序在内存中无法找到该数据结构,并直接改变其内容,只能通过Windows提供的一些函数来对内核对象进行操作。
(2).地址空间
它包含所有可执行模块或DLL模块的代码和数据。另外,它也包含动态内存分配的空间,例如线程的栈(stack)和堆(heap)分配的空间。
进程从来不用执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它环境中运行的线程(默认一个进程会主动创建一个线程,即主线程,即main函数),此线程负责执行包含在进程的地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境。
单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,成为主线程,也就是执行main函数或WinMain函数的线程,可以把main函数或WinMain函数看作是主线程的进入点函数。此后,主线程可以创建其他线程。
2.线程的组成
线程由2部分组成:
(1).线程的内核对象,即线程控制块TCB
操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
(2).线程栈(stack)
它用来维护线程在执行代码时需要的所有函数参数和局部变量。
当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。
线程总是在某个进程环境中创建的。系统从进程的地址空间中分配内存,供线程的栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地相互通信。
线程只有一个内核对象和一个栈,保留的记录很少,因此所需要的内存也很少。由于线程需要的开销比进程少,因此在编程中经常采用多线程解决问题,而尽量避免创建新的进程。
注:当多个线程同时执行的时候,从宏观的角度看,所有的线程是同时执行的。对于单核的cpu,看不出来线程之间的切换,即使切换也是原子级别的切换,宏观上看是所有的线程同时执行。下面的认识是错误的:一个线程执行几秒后,会切换到另一个线程去执行。
二.多线程同步的简单程序
下面给出了一个多线程同步的火车售票系统模拟程序。该程序的多线程同步是通过互斥锁实现。
#include <windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI Fun1Proc(
LPVOID lpParamater
);
DWORD WINAPI Fun2Proc(
LPVOID lpParamater
);
int index = 0;
int tickets = 20;
HANDLE hMutex;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hMutex = CreateMutex(NULL,FALSE,NULL);//互斥对象
hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);//关闭新线程的句柄
CloseHandle(hThread2);
Sleep(4000);
getchar();
}
DWORD WINAPI Fun1Proc(LPVOID lpParamater)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (tickets > 0 )
{
cout << "thread1 sell ticket : " << tickets-- << endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParamater)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (tickets > 0 )
{
cout << "thread2 sell ticket : " << tickets-- << endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
程序讲解:
(1).为什么刚创建线程,就将它执行CloseHandle将它关闭?
主函数在创建完线程之后,就立刻调用了CloseHandle函数关闭新线程的句柄。这里,调用CloseHandle不会中止新创建的线程,只是表示在主线程中对新创建的线程的引用计数不感兴趣,因此关闭。另一方面,当关闭该句柄时,系统会递减该线程内核对象的使用计数。当使用计数为0时,系统就会释放该线程的内核对象。如果没有关闭线程句柄,系统会一直保持着对该线程内核对象的引用,即使该线程执行完毕之后,它的引用计数仍不会为0,这样该线程内核对象也不会释放,只有等到进程终止时,系统才会清理这些残留的对象。因此,在程序中,当不再需要线程句柄时,应将其关闭,让这个线程内核对象的引用计数减一。
(2).Sleep(4000)
当主线程执行完毕后,进程也就退出了,这时进程中所有的资源,包括还没有执行的线程都要退出,也就是说新创建的线程还没有机会执行就退出了。为了让新创建的线程能够得到机会执行,就需要使主线程暂停执行,即放弃执行的权利,操作系统就会从等待运行的线程队列中选择一个线程来执行。这就是为什么在主线程main函数中调用Sleep(4000)的原因。当程序执行到Sleep函数时,主线程就放弃其执行的权利,进入等待状态,这时的主线程是不占用CPU时间的。
读者,还可以通过本blog中的"双线程高效下载"和“Python中线程的使用”,更加深刻的理解线程。