Windows 多线程编程总结

Windows 多线程编程总结

关键字:多线程 线程同步 线程池 内核对象

1 内核对象

1 .1 内核对象的概念

内核对象是内核分配的一个内存块,这种内存块是一个数据结构,表示内核对象的各种特征。并且只能由内核来访问。应用程序若需要访问内核对象,需要通过操作系统提供的函数来进行,不能直接访问内核对象( Windows 从安全性方面来考虑的)。

内核对象通过 Create* 来创建,返回一个用于标识内核对象的句柄,这些句柄 (而不是内核对象)可在创建进程范围内使用,不能够被传递到其他进程中被使用。

1 .2 内核对象使用的计数

因为内核对象的所有者是内核,而不是进程,所以何时撤销内核对象由内核决定,而内核做这个决定的依据就是该内核对象是否仍然被使用。那么如何判断内核对象是否被使用呢?可以通过内核对象的“使用计数”属性,一旦这个值变成 0 了,内核就可以释放该对象了。

1 .3 创建内核对象

1 .3.1 进程与句柄表

每个进程在初始化的时候,将被分配一个句柄表,该句柄表中只存储内核对象的句柄,不存储用户对象的句柄。句柄表的详细结构微软没有公布,但是大致包含三个内容:内核对象句柄,内核对象地址,访问屏蔽标志。

微软为何要将内核对象的句柄设置为进程相关的呢?理由有:

l          不同的进程对内核对象的访问权限是不同的,有必要区分对待

l          如果句柄是全局的,则一个进程可以控制另外一个进程的句柄,破坏另外一个进程的句柄。

 

1 .3.2 创建内核对象及操作系统内部机制

利用 CreateSomeObject 的函数来创建内核对象。调用该函数的时候内核就为该对象分配一个内存块,并进行初始化,然后内核再扫描该进程的句柄表,初始化一条记录并放在句柄表中。

1 .3.3 进程中使用内核对象的内部机制

假设函数 F 使用某个内核对象,其参数为 Handle1 ,则该函数内部需要查找该进程的句柄表,找出参数句柄对应的记录,然后才能使用该内核对象。

1 .4 关闭内核对象

无论进程怎样创建内核对象,在不使用该对象的时候都应当通过 Bool CloseHandle(HANDLE hobj) 来向操作系统声明结束对该对象的访问。为什么叫声明呢?是因为此时也许还有其他进程对该对象的访问,操作系统可能并不立即释放该对象。操作系统需要做的是:从进程的句柄表中删除该内核对象的记录,另外再考察该内核对象的使用计数以决定是否需要释放该对象。

1 .5 内核对象的共享

说到共享,与之孪生的就是共享权限。 Windows 内核对象的共享有三种方式:

1 .5.1 继承式共享(父子进程间)

只有当进程是父子关系的时候,才能使用此种方式的共享。特别要注意的是继承的是内核对象的句柄,内核对象本身是不具备继承性。要达到这种继承的效果需要做以下几件事:

l          在进程创建内核对象的时候,需要一个安全结构 sa (SECURITY_ATTRIBUTES 类型,以向 OS 声明对象的访问方式)作为参数。继承式共享需要将结构的成员 sa.bInheritHandle 设置为 TRUE 。此时 OS 内部的处理式将进程的句柄表中的该对象的访问屏蔽字段设置成“可继承”。

l          在创建子进程( CreateProcess 函数)时,设置创建参数 bInheritHandles 为TRUE 。表示被创建的子进程可以继承父进程中的所有可继承内核对象。 OS 内部的处理是:复制父进程句柄表中的记录到子进程的句柄表中,并使用相同的句柄值;为内核对象的使用计数器加 1 。

特别说明:子进程能够继承的的内核对象仅局限于父进程创建它的时候所拥有的可继承内核对象。子进程诞生后,父进程再搞出什么可继承的东西,子进程是不能用的。这就需要在子进程中使用继承的内核对象的时候需要慎重,以确定内核对象是否已被继承了。

利用 SetHandleinformation 方法可以随时修改内核对象句柄的一些属性,目前公开的句柄属性有两种,一种是该句柄是否能被继承,另一种是该句柄是否能被关闭。

1 .5.2 同名共享

同名共享,不需要共享进程之间存在父子关系。但局限于内核对象是否支持这种共享方式。创建内核对象的 Create 函数中是否包含 pszName 是该内核对象是否支持同名共享的标志。

l          方法一:当 Process1 通过 CreateObject ( …”someName” )创建了一个名字为someName 的内核对象后, Process2 也调用了 CreateObject ( …”someName” ),此时内核的动作是:在全局中查询发现已经存在 someName1 的对象;为 Process2 的句柄表添加一条 Ojbect 的记录,使用的句柄不确定;为 someName 这个 Object 的引用计数器加 1 。

l          方法二: Process2 使用 OpenObject ( …”someName” )的方式来获得对名someName 的 Object 的句柄。用这种 Open 方法的时候,需要提供一个参数让 OS 鉴权,以判定是否能够以参数指定的方式来访问内核对象。

1 .5.3 复制内核对象的句柄的方式共享

跨进程边界的内核对象共享的另外一个方法是通过 DuplicateHandle 来复制内核对象句柄。

如果要将 ProcessS 中的对象拷贝到 ProcessT 中则调用 DuplicateHandle 的进程一定要有对这两个进程的访问权,即句柄表中拥有这两个进程内核对象的句柄记录。

2 线程的一般概念

2 .1 视图

l          进程只是线程的容器,从来不执行任何东西

l          线程总是在某个进程中被创建

l          线程在进程的地址空间中执行代码

l          线程们共享进程中的所有内核对象

3 线程的创建

HANDLE CreateThread(

         PSECURITY_ATTRIBUTES psa,

         DWORD cbStack,

         PTHREAD_START_ROUTINE pfnStartAddr,

         PVOID pvParam,

         DWORD fdwCreate,

         PDWORD pdwThreadID);

《 Windows 核心编程》 P124 介绍说应当使用编译器提供的线程创建函数,而不应当直接使用 CreateThread 。

3 .1 CreateThread 调用的内核行为

调用 CreateThread 后, OS 进行如下几个动作:

l          生成一个线程内核对象

l          在进程空间内为线程分配堆栈空间

因为线程的环境同于其所在进程的环境,所以创建的线程可以访问进程中的所有资源,包括线程中所有的内核对象。

4 线程销亡

4 .1 终止线程的方式:

l          线程函数返回(最好使用这个方式,可以保证:线程种创建的 C++ 对象正常析构; OS 释放线程堆栈内存; OS 将线程的退出码设置为线程函数的返回值;系统将递减该线程内核对象的的使用计数器【如果此时还有其他引用 …… ,见下面说明】。)

l          调用 ExitThread (不能释放 C++ 对象,所以最好不要使用这个方式。另外,如果非要调用也应当调用编译器推荐的,如 _endThread 【 Windows 核心编程 P127】)

l          同进程内的其他线程(包括主线程)调用 TerminateThread (被撤销线程得不到通知,不能释放资源,尽量避免这种方式。另外这个函数是个异步函数,返回时,线程不保证已经被撤销,如果要观察线程是否被撤销,应当使用 WaitForSingleObject)

l          包含线程的进程终止(应当避免这种方式)

4 .2 线程退出时 OS 的行为

l          线程内的所有用户对象被释放。

l          线程的退出码从 STILL_ACTIVE 改为传递给 ExitThread 或 TerminateThread 的代码

l          线程内核对象的状态改为“已通知”

l          如果线程为进程中的最后一个线程,则 OS 将进程当作已终止运行

l          线程内核对象的引用计数器减 1 (一旦线程终止了,其他引用改线程内核对象将不能够处理改线程的句柄,但是可以通过调用 GetExitcodeThread 来检查 hThread代表的线程是否已经终止运行了。)

5 线程同步

5 .1 线程同步的起因以及解决之道

5 .1.1 共用资源型:多个线程需要访问同一个资源的时候,为了保证资源不被破坏,需要线程对资源的访问具有原子性。
5 .1.2 依赖型:一个线程等待另外一个线程某件事情完成后才能执行 _ 可以通过手动事件的方式互相通知。

5 .2 线程同步种类细分

同步起因

同步种类

同步方法备注

共用资源

多个线程对共用变量做加减操作

互锁函数族之: InterlockedExchangeAdd

共用资源

多个线程对公共变量、指针做赋值操作

互锁函数族之: InterlockedExchange ,InterlockedExchangepoint

共用资源

多个线程需要根据对公共变量、指针的判断做操作选择

互锁函数族之:InterlockedCompareExchange

              InterlockedCompareExchangePointer

共用资源

复杂数据结构(非单值),不适合互锁函数族处理的

用“关键代码”的方式,关键代码中要注意 1 、要尽量的快速处理完,以防止其他等待线程等待太长时间 2 、线程等待过程中由用户模式切换到内核模式,耗费 1000个 CPU 周期,时间比较长。 3 、只能对单个进程中的线程进行同步)

InitializeCriticalSection;

EnterCriticalSection;

LeaveCriticalSection;

DeleteCriticalSection;

处理线程同步的一种方法

对线程同步做的一个抽象,线程的同步本质上都是依赖于某个其他事件的发生,用软件的方法来对所依赖的事件做一个抽象,将有助与程序编写的简捷

CreateEvent

Event 的重要属性有一个是“自动” or“手动”,如果是自动的,则在某个线程用 Wait ××成功等待到事件的“通知”状态后,则事件状态立刻变成“未通知”状态,以保证同时对资源访问的线程只有一个。

原则上不算线程同步范畴,而属于对 wait** 的一种应用方式

一个可以作为定时器的内核对象, Waitable Timer

CreateWaitableTimer

SetWaitableTimer,

CancelWaitableTimer

共用资源

一组线程对一组同样性质的资源的争用,则这组资源需要有所表示,以告知线程们是否有空闲的给以为他们服务,以信号量机制实现

CreateSemaphore,ReleaseSemaphore

共用资源

一组线程对一个单一的资源的争用,需要有一种机制保证同一个事件只有一个线程能得到资源。以 Mutex 方式实现

CreateMutex

ReleaseMutex

与关键代码的差别在于:

1、    允许不同进程的线程之间同步

2、    内核对象,用户模式和内核模式切换的时候需要更多的 CPU 开销

特别说明: WaitForSingleObject/WaitForMultipleObject 是抑制线程本身的一种手法,配合以共用资源对象或所依赖的其他对象“通知状态”的原子性变化,以达到线程在争用资源、互相依赖时执行的顺序化,从而达到同步的目的。

综上:其实 Windows 的线程同步机制是提供了一组不同情况下的资源争用处理办法而已。与此同时推出的 Wait ××却可以带来很多其他好处,甚至部分缓解 C++ 语言没有事件机制的缺憾,部分达到了 JAVA,C# 中事件机制的效果,为 Oberserve 模式的实现做了些贡献。






Windows 多线程编程基础(云端笔记)

>> 函数

一、 创建线程:CreateThread

hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,pParam, dwFlags, &idThread) ;

第一个参数: 指向SECURITY_ATTRIBUTES形态结构的指针,Win98 中忽略该参数,WinNT中,被设为NULL。
第二个参数: 用于新线程的初始堆栈大小,默认值为0,在任何情况下,Windows根据需要动态延长堆栈的大小。
第三个参数: 指向线程函数的指标,函数名称没有限制,但是必须以下列形式进行声明:
DWORD WINAPI ThreadProc( PVOID pParam);

第四个参数: 传递给ThreadProc 的参数,这样主线程和从属线程就可以共享数据。
第五个参数: 通常为0,但当建立的线程不马上执行时为CREATE_SUSPENDED 旗标,前程将暂停直到 ResumeThread 来恢复线程的执行为止。 
第六个参数: 指向 接受执行绪的ID值的变量。 (指向 存储执行绪ID 值的变量) 

二、 启动线程:_beginthread 

1. hThread=_beginthread(ThreadPcor.uiStackSize,pParam); // PROCESS.H 头文件中的声明的 C执行时期链接库函数_beginthread启动一个新的线程。
第一个参数: 线程消息处理过程
第二个参数: 堆栈控件大小
第三个参数: 可以是一个创建另一个线程的线程传递给被自己创建的线程的参数

2. void __cdecl ThreadProc (void * pParam) ;

三、 结束线程:_endthread () 
_endthread () // 结束我们创建的线程




>> 注意:

在建立多线程的Windows 程序时,需要在[project setting]对话框中做一些修改。
选择[C/C++] 页面标签,然后在Category 下拉菜单方块中选择[Code Generation] 在Use Run Time Library 下拉菜单中 [Release] 设定的Single- Thread 和 用于Debug 设定的 [Debug Single- Thread] .
将这些分别改为 [Multithread] 和 [Debug Multithreaded] 。

这将把编译器旗标改为/MT,它是编译器在编译多线程的应用程序所需要的。具体地说,编译器将在.OBJ文件中插入LIBCMT.LIB文件名,而不是LIBC.LIB。连结程序使用这个名称与执行期链接库函数连结。

LIBC.LIB 和 LIBCMT.LIB文件包含C语言链接库函数,有些C语言链接库函数包含静态数据。

例如,由于strtok函数可能被连续地多次呼叫,所以它在静态内存中储存了一个指标。在多线程程序中,每个线程必须在strtok函数中有它自己的静态指针。因此,这个函数的多线程版本稍微不同于单线程的strtok函数。

PROCESS.H,这个文件定义一个名为_beginthread的函数,它启动一个新的线程。只有定义了_MT标识符,才会声明这个函数,这是/MT旗标的另一个结果。

>> 我们的窗口消息处理程序可以很容易的使用_beginthread(ThreadProc,0,NULL); //第一个参数:线程过程,第二个参数:堆栈大小;第三个参数:传递到线程函数的参数
创建新的线程,当我们使用_beginthread 调用新线程之后,线程函数中的代码将和程序中的其他代码一起工作,知道工作完成通知主程序。
两个或者多个执行绪使用一个程序中的同一函数,在这种情况下,动态区域变量(存储在堆栈上)对每个执行绪是唯一的,对程序的所有执行绪来说,所有静态变量都是一样的。

>> 但是通常我们需要唯一与各个执行绪的存储性数据。通常是静态变量,那么可以是使用[线程存储空间]


>> Sleep的好处

我们使用其中一个线程做后台工作时候,如果此线程做的是动画,那么我们要限制他的效应时间,作业时间,否则会太快。
实际上,线程呼叫Sleep函数来自动暂停执行,该函数唯一一个参数是以毫秒计的时间。

Sleep函数呼叫在指定的时间过去以前不会传回控制权。在这段时间内,线程被暂停,并且不会被配置给时间片段(尽管该线程显然仍然要求在tick时给予一小段的处理时间,因为系统必须确定线程是否应该重新开始执行)。给Sleep一个值为0的参数将导致线程交回它尚未使用完的时间片段。

当一个线程呼叫Sleep时,只是该线程被暂停指定的时间。系统仍然执行其它的执行绪,这些执行绪和暂停的执行绪可以是在同一个程序中,也可以是在另一个程序中。

不应该在您的主线程中使用Sleep函数,因为这会减慢对消息的处理速度


>> 线程同步

线程同步是必须的,同步信号就是我们十字路口的红绿灯,如果同步信号出现失误,那么在交界处,交叉运行的线程将遇到问题。

在单任务操作系统中,传统的计算机程序不需要我们的红绿灯来帮助他们协调它们之间的行为,它们在执行时似乎独占了整条路,而且也确实是这样,没有什么会干扰它们的工作。

即使在多任务操作系统中,大多数程序也可以各自独立的在执行,但是可能会发生一些问题。例如,两个程序可能会需要同时从同一个文件中读或者对同一文件进行写。在这种情况下,操作系统提供了一种共享文件和记录上锁的技术来帮助解决这个问题。








>> 临界区域

概念:
临界区域就是一块不可中断的程序代码。

临界区域是一个形态为 CRITICAL_SECTION 的整体变量。

CRITICAL_SECTION cs ;

这个CRITICAL_SECTION数据型态是一个结构,但是其中的字段只能由Windows内部使用。这个临界区域对象必须先被程序中的某个线程初始化,通过呼叫:

InitializeCriticalSection (&cs) ;

这样就建立了一个名为cs的临界区域对象
警告:「临界区域对象不能被移动或者复制,程序也不能修改该对象,但必须在逻辑上把它视为不透明的。」

当临界区域对象被初始化之后,线程可以通过下面的呼叫进入临界区域:

EnterCriticalSection(&cs);
在这时,线程被认为「拥有」临界区域对象。

注意:

两个线程不可以同时拥有同一个临界区域对象,因此,如果一个线程进入了临界区域,那么下一个使用同一临界区域对象呼叫EnterCriticalSection的线程将在函数呼叫中被暂停。

我们可以建立多个临界区域对象,那么一个程序如果有多个线程,可以使用不同的临界区域共享不同的数据。




只有当第一个线程通过下面的呼叫离开临界区域时,函数才会传回控制权:

LeaveCriticalSection (&cs) ;

这时,在EnterCriticalSection呼叫中被停住的那个线程将拥有临界区域,其函数呼叫也将传回,允许线程继续执行。

当临界区域不再被程序所需要时,可以通过呼叫:

DeleteCriticalSection (&cs) ;

将其删除,该函数释放所有被配置来维护此临界区域对象的系统资源。

概述:
优势:

在任何时刻,只有一个线程能够进入一个临界区域,然后在结束自身的工作之后,将会退出临界区域,另一个线程才有机会进入临界区域,然后才可以对临界区域数据进行操作,然后退出该临界区域。

在主线程中使用临界区域,要小心:
如果从属线程在它自己的临界区域中花费了一段很长的时间,那么它可能会将主线程的执行阻碍很长一段时间。从属执行绪可能只是使用临界区域复制该结构的字段到自己的区域变量中。

缺陷:

临界区域的一个限制是他们只能用于在同意程序内的线程之间的协调。但是在某些情况下,我们需要协调两个不同程序对统一资源的共享(如共享内存等)。

替代者(互斥对象mutex object):

在此情况下不能使用临界区域,而应该使用[互斥对象 mutex object]的技术。
mutex 是一个合成字,代表 [mutual exclusion]互斥,他在这里精确的表达了我们的目的,我们想防止一个程序的线程在更新数据或者使用共享内存与其他资源时被中断。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值