Windows核心编程,多线程(Thread)编程学习有这一篇就够了

文档声明:
以下资料均属于本人在学习过程中产出的学习笔记,如果错误或者遗漏之处,请多多指正。并且该文档在后期会随着学习的深入不断补充完善。感谢各位的参考查看。


笔记资料仅供学习交流使用,转载请标明出处,谢谢配合。
如果存在相关知识点的遗漏,可以在评论区留言,看到后将在第一时间更新。
作者:Aliven888

1、 概念

  1. 线程是系统内核对象之一。
  2. 内核对象是系统内核分配的一个内存块,该内存块描述的是一个数据结构,其成员负责维护对象的各种信息。
  3. 常用的系统内和对象:事件对象,文件对象,作业对象,互斥对象,管道对象,进程对象,线程对象。
  4. 属性:计数属性、安全属性。
  5. 三种状态:挂起,唤醒,终止。

2、线程与进程的关系

  进程被认为是一个正在运行的程序的实例,它也是系统内和对象之一。

关系:

  1. 线程存在于进程之中,他负责执行进程地址空间中的代码。
  2. 可以将进程理解为一个容器,他只是提供空间,执行程序的代码是由线程来实现的。
  3. 当一个进程被创建时,系统会自动为其创建一个线程,该线程被称之为主线程。
  4. 在主线程中可以通过代码创建其他的线程(也称子线程),当进程中的主线程结束时,子进程也就结束了。
  5. 线程是进程执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派
    的基本单位。
  6. 线程自己不同拥有资源,但它可以与同一进程的其他线程共享进程的全部资源。
  7. 一个线程可以创建和撤销另一个线程,统一进程的多个线程之间可以并发执行。

3、线程的好处

  1. 易于调度。
  2. 提高并发行:进程中可以创建多个线程来执行程序的不同部分。
  3. 开销小。
  4. 利于充分发挥多处理器系统的功能。通过创建多线程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分的运行。

4、进程与线程的区别

  1. 一个线程只能属于一个进程,而一个进程中至少包含一个线程。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  3. 处理器分配给线程,即真正在处理机上运行的是线程。
  4. 线程在执行中,需要协作同步。

5、多线程同步和互斥的几种方法

临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
互斥量:为协调共同对一个共享资源的单独访问而设计的。
信号量:为控制一个具有有限数量用户资源而设计。
事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

详细解释:

临界区:(Critical Section)(同一个进程内,实现互斥)保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

互斥量:(可以跨进程,实现互斥)互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
  互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话,使用临界区会带来速度上的优势并能够减少资源占用量。

信号量:(Semaphores)(主要是实现同步,可以跨进程)信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。

事 件:(Event,实现同步,可以跨进程)事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。

6、线程的创建

 创建线程最常用的方式是使用windows的 API 函数CeateThread(…)

函数原型:

HANDLE  CreateThread(LPSECURIT lpsa, 
					 DWORD cbStackSize, 
                     LPTHREAD_START_ROUNE lpStartAddr, 
                     LPVOID  lpvThreadParam, 
                     DWORD  fdwCreate, 
                     LPDWORD lpPDThread);

参数介绍:
	Lpsa:表示线程的安全属性,是一个指向SECURITY_ATTRIBUTES结构的指针。
	      如果想使用线程内核对象的默认安全属性,可以为NULL(一般都是这么做的)。
	      但是如果希望所有子线程都能继承到这个线程对象的句柄,必须制定一个SECURITY_ATTRIBUTES结构,并将该结构的bInheritHandle成员初始化。
	cbStackSize:表示线程栈的最大大小,该参数可以被忽略。
	lpStartAddr:表示线程函数,当线程运行时,将执行该函数,
	lpvThreadParam:表示向线程函数传递的参数,
	fdwCreate:表示线程创建的标记。
               CREATE_SUSPENDED:表示线程被挂起,只有调用ReSumeThread函数时才开始执行线程。
               STACK_SIZE_PARAM_IS_A_RESERVATION:表示cbStack参数将不能被忽略。使cbStack参数表示线程堆的最大大小。
    lpIDThread:表示一个整形指针,用于接收线程ID,线程ID在系统范围内唯一标示线程,有些系统函数只需要使用线程ID做参数。如果该参数为NULL,表示线程ID不被返回。

返回值:
	如果执行成功,返回线程的句柄,否则返回NULL. 

举例:
	//线程函数
	DWORD WINAPI threadFun(){
    
    ....}

	//创建线程
	DWORD dwThreadId = -1;
	HANDLE m_hThread = CreateThread(null, 0, (PVOID)&threadFun, 0 , &dwThreadId);

7、线程的调度

7.1、挂起线程

方法 1:
  在线程创建时,将CreateThread(…) 函数的第五个参数(fdwCreate)设置为CREATE_SUSPENDED,那么线程在创建成功后,就会处于挂起状态。

方法 2:
  如果线程处于运行状态,可以使用SuspendThread(…) 函数将线程挂起,

函  数:DWORD  SuspendThread(HANDLE  hThread);
参  数:hThread:表示线程句柄
返回值:如果成功,返回值为之前挂起的线程次数。如果函数执行失败,返回值为0xFFFFFFFF

7.2、唤醒线程

  同理,线程可以被挂起,那么也可以被唤醒。

函  数: DWORD  ResumeThread(HANDLE  hThread)
参  数:hThread:表示线程句柄
返回值:成功:返回值为之前挂起的线程次数
       失败:返回值为0xFFFFFFFF

7.3、终止线程

方法 1:
  线程函数返回(即线程函数执行结束返回)。该方法最佳,在编写程序时,尽量采用该方式。

方法 2:
  调用ExitThread函数退出线程(强制终止线程的运行)。 – 不安全,存在内存泄露

函数: VOID  ExitThread(DWORD  dwExitCode);
参数:dwExitCode: 表示线程退出代码
缺点:可能会出现内存泄露的情况

方法 3:
  调用TerminateThread函数终止线程(该函数知只是告诉了系统你想终止线程,但是函数返回时,并不能保证该线程又被终止了)。 – 最差

函数:BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode)
参数:hThread : 待终止的线程句柄。
	 dwExitCode:表示线程退出代码。
缺点:在设计时,尽量避免使用TerminateThread函数。

7.4、睡眠线程

  如果线程在一段时间内不需要执行(调度)了,可以通过Sleep函数实现。

函数:VOID Sleep(DWORD dwMilliseconds);
参数:dwMilliseconds 睡眠时间,单位毫秒(ms)

注意事项:
  如果我们对一个挂起的线程进行多次的挂起操作,则需要相同次数的唤醒操作才能将其唤醒;反之,如果对一个已唤醒的线程进行多次唤醒操作,则只需要一次挂起操作就可以将线程挂起。

8、线程同步

线程同步的必要性:
  比如在一个多线程的进程中,多个线程共同访问某一资源;如果A线程正在读取该资源的某一数据,而线程B则是正在对该数据进行修改,那么就可能会导致线程A接收的数据有两种情况,一是线程B修改之前的,二是线程B修改之后的,而且这两种结果的任何一种结果都是不可以预测的。

8.1、线程同步的实现方法

方法 1:
  使用事件对象来实现线程的同步。在所有的内核对象中,时间比其他对象要简单的多。当一个事件被触发(设为通知状态)时,正在等待改事件的所有线程都将编程可调度状态。
  事件的通常用途是,让一个线程执行初始化工作,然后在触发另一个线程,让他执行剩余的工作。

函数:HANDLE  CreateEvent(LPSECURITY_ATTRIBUTES  lpEventAttributes, 
						 BOOL  bManualReset,  
						 BOOL  bInitialState, LPCTSTR  lpName);
参数:
	 lpEventAttributes:表示事件对象的安全属性
     bManualReset:表示事件对象的类型, 为true表示创建人工重置事件对象,为false表示创建自动设置事件对象。
     bInitialState:表示事件初始化的通知状态。为true表示通知状态;为false表示未通知状态。
     LpName: 表示事件对象的名称

函数:BOOL  SetEvent(HANDLE hEvent)
作用:用于将事件设置为通知状态
参数:表示事件对象的句柄

函数:BOOL  ResetEvent(HANDLE  hEvent)
作用:用于将事件设置为未通知状态
参数:表示事件对象的句柄

函数:DWORD WaitForSingleObject(HANDLE hHandle, 
DWORD dwMilliSeconds)
作用:设置线程的等待状态
参数:hHandle:表示等待对象的句柄。
     DwMilliSeconds: 表示等待的时间,单位为毫秒(ms)。如果等待时间超过了该参数表示的时间,函数将返回。如果该参数设置为INFINITE,则函数将会一直等待,直到hHandle参数表示的对象处于通知状态。详细参考《Visual C++开发实战宝典》p545

方法 2:
使用互斥对象实现线程同步。

函数:HANDLE  CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, 
						 BOOL bInitialOwner,  
						 LPCTSTR  lpName);
参数:
	 lpMutexAttributes:表示互斥对象的安全属性,可为NULL。
     bInitialOwner:表示互斥对象的初始状态。如果为true,互斥对象的线程ID为当前调用线程的ID,互斥对象的递归计时器为1。 当前创建互斥对象的线程用优盘互斥对象的所有权。为false,互斥对象的递归计时器为0,系统会发出该互斥对象的通知信号。
     LpName:表水互斥对象的名称,若为NULL,将创建一个匿名的互斥对象。
返回值 : 成功:返回互斥对象的句柄。
        失败:返回NULL。

函数:BOOL  ReleaeMutex(HANDLE hMutex);
作用:释放互斥对象的所有权,表示当前线程不在需要访问该资源了。
参数:表示互斥对象的句柄。

方法 3:
使用信标对象实现线程同步。

函数:HANDLE  CreateSemaphore(LPSECURITY_ATTRIBUTES lpMutexAttributes, 
							 LONG  lInitialCount, 
							 LONG  lMaximumCount, 
							 LPCTSTR  lpName);
参数: lpMutexAttributes:表示互斥对象的安全属性,可为NULL。
      lInitialCount:表示信标的初始计数。
      lMaximumCount:表示信标的最大计数。
      lpName:表示信标的名称。

函数:BOOL ReleaseSemaphore(HANDLE  hSemaphore, LONG  lReleaseCount, LPLONG  lpPreviousCount);
参数:hSemaphore:表示信标对象句柄
     lReleaseCount:表示信标的自增数量
     lpPreviousCount:用于返回之前的信标的使用计数。

方法 4:
  使用关键代码段(临界区)实现线程同步。关键代码段又称临界区,指的是一小段代码,在执行代码前,它需要独占某些资源。在程序中,通常将多个线程同时访问的某个资源的代码作为临界区。为了方便使用临界区,系统提供了一组操作临界区对象的函数。

函数:void  InitializeCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);
作用:用于初始化临界区对象。
参数:表示一个临界区对象指针。
注意:在使用临界区对象时,首先需要定义一个临界区对象,然后使用该函数进行初始化。

函数:void EnterCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);
作用:用于等待临界区对象的所有权
参数:表示一个临界区对象指针

函数:void LeaveCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);
作用:用于释放临界区对象的所有权。
参数:表示一个临界区对象指针

函数:void DeleteCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);
作用:用于释放为临界区分配的相关资源,使临界区对象不可再用
参数:表示一个临界区对象指针

9、线程池

  为了简化线程的创建和撤销处理而提出的概念。

特点:

  1. 异步调用函数。
  2. 按照指定的时间间隔调用某一函数。。
  3. 当一个内核对象变成为已通知状态调用函数。
  4. 当异步I/O请求完成时调用某一函数。

  异步调用函数:winows系统提供了QueueUserWorkItem函数用于在线程池中使用一个线程来执行一个“工作项目”也就是一个用户定义的函数。

函数:DWORD QueueUserWorkItem(LPTHREAD_START_ROUTINE FunName, 
							  PVOID  Contex, 
							  ULONG  flags);
参数:FunName:表示用户定义的函数,自定义函数函数原型为 DWORD  WINAPI  FunName(PVOID lpParam)
			 注意:lpParam与函数QueueUserWorkItem的Contex的参数是对应的,在调用QueueUserWorkItem函数时,Contex参数会作为FunName函数的实际参数。 返回值是DWORD类型,可以根据自己的需求随意定义。
     Contex:用于作为FunName函数的实际参数。
     Flags:表示一组标记值,可以为0.
注意:在VC++6.0中并没有提供QueueUserWorkItem函数,但是它位于Kernel32.dll动态链接库中,因此可以直接从该库中导出。

笔记跟新记录

时间 内容
2020-11-14 创建笔记

猜你喜欢

转载自blog.csdn.net/Aliven888/article/details/109588212