为什么要引入线程
进程是为了提高CPU的执行效率而提出的,减少因为程序等待而带来的CPU空转以及其他计算机软硬件资源的浪费而提出来的。进程是为了完成用户任务所需要的程序的一次执行过程以及为其分配资源的一个基本单位。以进程为单位来分配资源时,为了便于处理机执行,系统要定义如何对进程进行识别和操作的物理实体。被系统识别和操作的物理实体就是进程控制块PCB。PCB负责记录进程的描述信息、有关的控制信息、各种资源的管理信息和所对应进程的CPU保护现场信息等。
由进程管理部分知道,在网络或多用户环境下,许多用户的应用任务都是并发进行的,而且,它们往往只有较多的软硬件资源。在引入了进程的概念之后,这些用户的应用任务都以进程的方式管理、执行和完成。
在很多情况下,用户所要完成的任务具有许多相似的性质,例如,一个WEB服务器可以同时接收来自不同用户的网页请求。显然服务器处理这些网页请求都是并发进行的,否则,将会造成用户等待时间过长和响应时间降低。如果在服务器中,用进程的办法来处理来自不同用户的网页访问请求的话,我们可以创建父进程和多个子进程的方式来进程处理。
创建一个进程要花费较大的系统开销和占用较多的资源。例如至少要消费一个PCB结构。如果这个进程创建的子进程过多,则消费的进程PCB结构和其他系统资源越多。一般来说,随着访问服务器的用户数增加,子进程数量将增加。因此,对于具有不确定用户和随机访问的WEB服务器来说,用进程来管理用户访问请求的方法会较大的限制开发访问服务器的用户数。
同时,当不同用户的用户请求访问WEB服务器时,不同的用户子进程将被用来完成访问请求处理。这些不同的用户子进程的执行涉及到进程上下文切换。进程上下文是一个复杂的过程,它涉及到对当前正在执行进程的状态和占用资源的保存写处理、选取新的待执行进程以及恢复待执行进程的执行状态和资源的工作。
进程创建和切换过程越多,系统的开销越多和越大,从而服务器可以处理和支持的用户访问请求就会越少。显然,如何减少进程创建和切换所带来的系统开销,是服务器操作系统的主要瓶颈之一。为了减少进程的切换和创建的开销,提高执行效率和节省资源,人们开始在操作系统中引进“线程”的概念。
线程的基本概念
线程是进程的一部分,线程有时又被称为轻权进程或轻量级进程(light weight process),与进程相同,线程也是CPU调度的一个基本单位。
一个没有线程的进程可以被看作是单线程的,即进程的执行过程是线状的,尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。
与此相对应,如果一个进程内拥有多个线程,则进程的执行过程不再是唯一线状的,它由多条线状执行过程组成:
线程与进程的区别
虽然进程和线程都是处理机调度的基本单位,但是,线程的改变只代表了CPU执行过程的改变,而没有发生进程所拥有的资源的变化。或者说,除了CPU之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属的进程的资源。
与进程控制表和PCB相似,每个线程也有自己的线程控制表TCB,而这个TCB中所保存的线程状态信息则要比PCB表少的多,这些信息主要是相关指针用堆栈(系统栈和用户栈)、寄存器中的状态数据。
进程是系统中所有资源分配的基本单位,例如打印机等设备都是以进程为单位进行分配的。
进程拥有一个完整的虚拟地址空间。
进程不依赖于线程而独立存在。反之,线程是进程的一部分,它没有自己的地址空间,它和进程内的其他线程一起共享分配给该进程的所有资源。
线程的使用范围
尽管线程可以提高系统的执行效率,但并不是在所有的计算机系统中,线程都是适用的。事实上在那些很少做进程调度和切换的实时系统、个人数组助理系统中,由于任务的单一性,设置线程相反会占用更多的系统资源。
使用线程的最大好处是在有多个任务需要处理机处理时,减少处理机的切换时间,而且,线程的创建和结束所需要的系统开销也比进程的创建和结束要小的多。因此,最适合使用线程的系统是多处理机系统和网络系统或是分布式系统。
线程的3中典型的应用:
1.服务器中的文件管理或通信控制。在局域网的文件服务器中,对文件的访问要求可被服务器进程派生出的线程进行处理。由于服务器可能同时接受许多个文件访问要求,则系统可以同时生成多个线程来进行处理。如果计算机系统是多处理机的,这些线程还可以安排到不同的处理机上执行。
2.前台处理。一个计算量较大的程序或是实时性要求不高的程序安排在处理机空闲的时候执行。对于同一个进程的上述程序来说,线程可被用来减少处理机的切换时间和提高执行速度。例如在表处理进程中,一个线程可被用来显示菜单和读取用户的输入,另一个线程则可用来执行用户命令和修改表格。由于用户输入命令和命令执行分别由不同的线程在前后台执行,从而提高操作系统的效率。
3.异步处理。程序中的两部分如果在执行上没有顺序规定,则这两部分的程序可用线程来执行。例如一个用户主机通过网络线2台远程服务器进行远程调用(RPC)以获得相应结果的执行情况。如果用户只用一个线程,则第二个远程调用的请求只有在得到第一个请求的执行结果后才能发出;多线程时,用户程序不必等待第一个RPC请求的执行结果而直接发出第2个RPC请求,从而缩短等待时间。
线程的执行特性
线程有3个基本状态,即执行、就绪和阻塞。但是线程没有进程中的挂起状态。也就是说,线程是一个至于内存和寄存器相关的概念,它的内容不会因交换而进入外存。
针对线程的3中状态,存在5中基本操作来转换线程状态,这5种基本操作是:
(1).派生线程在进程内派生出来,它既可由进程派生,也可由线程派生。用户一般用系统调用派生自己的线程。例如,在Linux操作系统中,库函数clonc()和Creat-thread()被分别用来派生不同执行模式下的线程。
一个新派生出来的线程具有相应的数据结构指针和变量,这些指针和变量作为寄存器上下文放在相应的寄存器和堆栈中。
(2).阻塞(block)。如果一个线程在执行过程中需要等待某个事件发生,则被阻塞。阻塞时,寄存器上下文、程序计数器以及堆栈指针都会得到保存。
(3).激活(unblock)。如果阻塞线程的事件发生,该线程被激活并进入就绪队列。
(4).调度(schedule)。选择一个就绪线程进入执行状态。
(5).结束(finish)。如果一个线程执行结束,它的寄存器上下文以及堆栈内容等将被释放。
需要注意一点的是,在某些情况下,某个线程被阻塞也可能导致该线程所属的进程被阻塞。
线程的另一个执行特性是同步。由于同一进程中所有线程共享该进程的所有资源和地址空间,任何线程对资源的操作都会对其他相关线程带来影响。因此,系统必须线程的执行提供同步控制机制。以防止因线程的执行而破坏其他的数据结构和给其他线程带来不利的影响。
使用Win32 SDK函数实现多线程
1.创建线程
创建一个线程需要以下两个步骤:
(1).编写线程函数
所有线程必须从一个指定的函数开始执行,该函数就是所谓的线程函数。线程函数必须具有类似下面所示的函数原型:
DWORD ThreadFunc(LPVOID lpvThreadParm);
ThreadFunc是线程函数的名字,可以由编程者任意指定,但必须符合VC++标识符的命名规范。该函数仅有一个LPVOID的类型定义如下:
typedef void * LPVOID;
它既可以是一个DWORD类型的整数,也可以指向一个缓冲区的void类型指针。函数返回一个DWORD类型的值。
注意:
一般来说,C++的成员函数不能作为线程函数。这是因为在类中定义的成员函数,编译器会给其加上this指针。但如果需要线程函数像类的成员函数那样能够访问类的所有成员,可采用两种方法。第一种方法是将该成员函数声明为static类型,但static成员函数只能访问static成员,不能访问类中的非静态成员,解决此问题的一种途径是可以在调用类静态成员函数(线程函数)时将this指针作为参数传入,并在该线程函数中用强制类型转换将this指针转换成指向该类的指针,通过该指针访问非静态成员。第二种方法是不定义类的成员函数为线程函数,而将线程函数定义为类的友元函数,这样线程函数也可以有类成员函数同等的权限。
(2).创建一个线程
进程的主线程是操作系统在创建进程时自动生成的,但如果要让一个线程创建一个新的线程,则必须调用线程创建函数。Win32 SDK提供的线程创建函数是CreateTread()。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpTreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpTreadId
);
函数参数:
lpTreadAttributes:指向一个LPSECURITY_ATTRIBUTES结构体指针,该结构体决定线程的安全属性,一般设置为NULL。
dwStackSize:指定线程的堆栈深度,一般设置为0。
lpStartAddress:线程的起始地址,通常为线程函数名。
lpParameter:线程函数的参数。
dwCreationFlags:控制线程创建的附加标志。该参数为0,则线程在被创建后立即开始执行;如果该参数为CREATE_SUSPENDED,则创建线程后该线程处于挂起状态,直至函数Resume Tread被调用。
lpTreadId:该参数返回所创建线程的ID。
注意:使用同一个线程函数可以创建多个各自独立工作的线程。
一个简单的线程函数定义及线程创建的例子:
#include "windows.h"
#include <iostream>
using namespace std;
/********定义线程函数***********/
void TreadFun1(){
for(int i=1;i<100;i++){
Sleep(600); //阻塞1000毫秒
cout<<i<<",This is Tread1!"<<endl;
}
}
HANDLE hThread1; //线程句柄
DWORD ThreadID1; //线程ID
int main(){
//创建线程 由于线程函数无参数 所以CreateTread的第四个参数为NULL
hThread1=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)TreadFun1,NULL,0,&ThreadID1);
for(int j=1;j<20;j++){
Sleep(1000);
cout<<j<<",This is MainThread!"<<endl;
}
system("pause");
}
程序运行效果:
2.线程函数的传递
由CreateThread函数原型可以看出,创建线程时可以给线程传递一个void指针类型的参数,该参数为CreateThread()函数的第四个参数。
当需要将一个整型数据作为线程函数的参数传递给线程时,可以将该整型数据强制转换为LPVOID类型,作为其实参传递给线程函数。
当需要向线程函数传递一个字符串时,则创建线程时的实参传递既可以使用字符数组,也可以使用CString类。使用字符数组时,实参可直接使用字符数组名或指向字符数组的char*指针;使用CString类时,可以指向CString对象的指针强制转换为LPVOID。
如果需要向线程传送多个数值时,由于线程函数的参数只有一个,所以需要将它封装在一个结构体变量中,然后将该变量的指针作为参数传给线程函数。
将整数、字符数组和CString对象作为参数传递给线程:
#include "windows.h"
#include "atlstr.h"
#include <iostream>
using namespace std;
/********定义线程函数***********/
int TreadFun0(LPVOID lpParam){
int *a=(int*)lpParam;
for(int i=1;i<10;i++){
Sleep(1000); //阻塞1000毫秒
cout<<"I am Thread0,the number main thread given me is "<<*a<<endl;
}
return 0;
}
int TreadFun1(LPVOID lpParam){
char *p=(char*)lpParam;
Sleep(2000);
cout<<"This is Thread1,the string main thread given me is "<<*p<<endl;
return 0;
}
int TreadFun2(LPVOID lpParam){
CString *p=(CString*)lpParam;
Sleep(3000);
cout<<"This is Thread2,the string main thread given me is "<<*p<<endl;
return 0;
}
HANDLE hThrd0,hThrd1,hThrd2; //线程句柄
DWORD ThrdID0,ThrdID1,ThrdID2; //线程ID
int main(){
int a=888; //传递给线程0的整数
char s1[]="ABCDEFGH"; //传递给线程1的字符数组
CString s2("abcdef"); //传递给线程2的字符串对象
cout<<"This is MainThread!"<<endl;
//创建线程 由于线程函数无参数 所以CreateTread的第四个参数为NULL
hThrd0=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)TreadFun0,&a,0,&ThrdID0);
hThrd1=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)TreadFun1,s1,0,&ThrdID1);
hThrd2=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)TreadFun2,&s2,0,&ThrdID2);
Sleep(100000);
system("pause");
}