多线程

来源:互联网 发布:广汽丰田2016年网络数 编辑:程序博客网 时间:2024/04/28 06:03
多线程
1、基本概念
1.1、进程
1、程序和进程
程序是计算机指令的集合,它以文件的形式存储在磁盘上,而进程通常被定义为一个正在运行的程序的实例,它是一个程序在其自身的地址空间的一次执行活动。
进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源
2、进程组成
进程由两个部分组成:
1)操作系统用来管理进程的内核对象
内 核对象也是系统用来存放关于进程的统计信息的地方。内核对象是操作系统内部分配的一个内存块,该内存块是一中数据结构,其成员负责维护该对象的各种信息。 由于内核对象的数据结构只能被内核访问使用,因此应用程序在内存中无法找到该数据结构,并直接改变其内容,只能通过Windows提供的一些函数来对内核 对象进行操作
2)地址空间
它包含所有可执行模块或DDL模块的代码和数据。另外,它也包含动态内存分配的空间,例如线程的栈和堆分配空间
进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境
单个进程可能包含若干个线程,这些线程都“同时”执行进程空间中的代码。每个进程至少拥有一个线程,用来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程,也就是执行main函数或WinMain函数的线程,可以把main函数或者WinMain函数看作是主线程的进入点函数。此后,主线程可以创建其他线程
3、进程地址空间
系统赋予每个进程独立的虚拟地址空间,对于32位进程来说,这个地址空间是4GB因为对于32位指针来说,它能寻址的范围是2的32次幂,即4GB
1.2、线程
1、线程组成
线程由两个部分组成:
1)线程的内核对象:操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方
2)线程栈:它用于维护线程在执行代码时需要的所有函数参数和局部变量
当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构
线程只有一个内核对象和一个栈,保留的记录很少,因此所需要的内存也很少。由于线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程
2、线程运行
操作系统为每一个运行线程安排一定的CPU时间——时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此给用户的感觉就是多个线程是同时运行的一样
3、单线程程序与多线程程序
对单线程程序来说,在进程的地址空间中只有一个线程在运行。多线程程序,在进程地址空间中有多个线程,其中有一个是主线程。
编写多线程程序,每一个线程可以独立地完成一个任务,当该程序移植到多CPU的平台上时,其中的多个线程就可以真正意义上并发地同时运行了
2、线程创建函数
创建线程可以使用系统提供的API函数:CreateThread来完成,该函数将创建一个线程,函数原型如下:
HANDLE CreateThread(
 LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes
 DWORD dwStackSize,                         // initial thread stack size
 LPTHREAD_START_ROUTINE lpStartAddress,     // pointer to thread function
 LPVOID lpParameter,                        // argument for new thread
 DWORD dwCreationFlags,                     // creation flags
 LPDWORD lpThreadId                         // pointer to receive thread ID
);
lpStartAddress 参数是一个函数指针,这个函数将由新线程执行,表明新线程的起始地址。main函数是主线程的入口函数,同样地,新创建的线程也需要有一个入口函数,这个 函数的地址就由此参数指定。这就要求在程序中定义一个函数作为新线程的入口函数,该函数的名称任意,但函数类型必须遵照下述声明形式:
DWORD WINAPI ThreadProc(
 LPVOID lpParameter   // thread data
);
lpParameter参数提供了一种将初始值传递给线程函数的手段,这个参数的值即可以是一个数值,也可以是一个指向其他信息的指针
在创建线程完成之后,调用CloseHandle函数关闭新线程的句柄。实 际上调用CloseHandle函数并没有终止新创建的线程,只是表示在主线程中对新创建的线程的引用不感兴趣,因此将它关闭。另一方面,当关闭该句柄 时,系统会递减该线程对象的使用计数。当创建的这个新线程执行完毕之后,系统也会递减该线程内核对象的使用计数。当使用计数为0时,系统就会释放该线程内 核对象。如果没有关闭线程句柄,系统就会一直保持着对线程内核对象的引用,这样,即使该线程执行完毕,它的引用计数仍不为0,这样该线程内核对象也就不会 被释放,只有等到进程终止时,系统才会清理这些残留的对象。因此,在程序中,当不再需要线程句柄时,应将其关闭,让这个线程内核对象的引用计数减1
当主线程执行完毕后,进程也就退出了,这是进程中所有的资源,包括还没有执行的线程都要退出。为了让新创建的线程能够得到执行的机会,就需要使主线程暂停执行,即放弃执行权力,操作系统就会从等待运行的线程队列中选择一个线程来执行
在程序中,如果想让某个线程暂停运行,可以调用Sleep函数,该函数可以使调用线程暂停自己的运行,直到指定的时间间隔过去为止。该函数的原型声明如下:
void Sleep(DWORD dwMlilliseconds);
3、线程同步
一般来说,对多线程程序,如果这些线程需要访问共享资源,就需要进行线程间的同步处理
3.1、利用互斥对象实现线程同步
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID用于表示系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数
为了创建互斥对象,需要调用函数:CreateMutex,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用该互斥对象完成线程间的同步。该函数的原型声明如下:
HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
                       // pointer to security attributes
 BOOL bInitialOwner, // flag for initial ownership
 LPCTSTR lpName       // pointer to mutex-object name
);
如果调用成功,该函数将返回所创建的互斥对象的句柄。如果创建的是命名的互斥对象,并且在CreateMutex函数调用之前,该命名的互斥对象存在,那么该函数将返回这个已经存在的这个互斥对象的句柄
另外,当线程对共享资源访问结束后,应释放该对象的所有权,也就是让该对象处于已通知状态,这时需要调用ReleaseMutex函数,该函数将释放指定对象的所有权,该函数的原型声明如下所示:
BOOL ReleaseMutex(
 HANDLE hMutex   // handle to mutex object
);
另外,线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过调用WaitForSingleObject函数来实现,该函数的原型声明如下:
DWORD WaitForSingleObject(
 HANDLE hHandle,        // handle to object to wait for
 DWORD dwMilliseconds   // time-out interval in milliseconds
);
对于互斥对象来说,它是惟一与线程相关的内核对象,当主线程拥有互斥对象时,操作系统系统会将互斥对象的线程ID设置为主线程的ID。当在线程1中调用ReleaseMutex函数释放互斥对象的所有权时,操作系统会判断线程1的线程ID与互斥对象内部所维护的线程ID是否相等,只有相等才能完成释放操作
也就说对互斥对象来说,谁拥有谁释放
当调用WaitForSingleObject函数请求互斥对象时,操作系统需要判断当前请求互斥对象的线程ID是否与互斥对象当前拥有者的线程ID相同,如果相等,即使该互斥对象处于未通知状态,调用线程仍然能够获得其所有权,然后WaitForSingleObject函数返回。对于同一个线程多次拥有的互斥对象来说,该互斥对象内部的计数器记录了该线程拥有的次数。当接下来调用ReleaseMutex函数释放该互斥对象的所有权时,实际上就是递减这个计数器,只有当计数器的值变为0时,操作系统才会将互斥对象变为已通知状态
在 程序运行时,操作系统维护了线程的信息以及与该线程相关的互斥对象的信息,因此它知道哪个线程终止了。如果某个线程得到其所需互斥对象的所有权,完成其线 程代码的运行,但没有释放该互斥对象的所有权就退出之后,操作系统一旦发现该线程已经终止,它就会自动将该线程所拥有的互斥对象的线程ID设置为0,并将其计数器归0
5、保证应用程序只有一个实例运行
通过命名的互斥对象可以实现同时只有应用程序的一个实例运行的功能。在 调用CreateMutex函数创建一个命名的互斥对象后,如果其返回值是一个有效的句柄,那么接着调用GetLastError函数,如果该函数返回的 是ERROR_ALREADY_EXISTS,就表明先前已经创建了这个命名的互斥对象,因此就可以知道先前已经有该应用程序的一个实例在运行了。当然如 果GetLastError函数返回的不是ERROR_ALREADY_EXISTS,就说明这个互斥对象是创建的,从而也就知道当前启动的这个进程是应 用程序的第一个实例
 
原创粉丝点击