线程同步

一个进程中的所有线程共享这个进程的代码段和数据段,但是着每一个线程又拥有属于自己的堆栈和计数器。

并发:一个进程内的多个线程以时间片轮转的方式执行,某一时刻只有一个线程占有CPU。并发模式实际上是串行执行,因此性能提高有限。

并行:在多核或多CPU的系统中,实现的真正多线程同时执行。每一个线程占有一个核或CPU。

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

第一个参数是一个指向安全结构的指针,通过这个安全的属性结构,我们可以指定这个被创建出来的线程能否为当前进程的子进程所继承。

第二个参数指定了创建出来的线程拥有的的堆栈大小。

第三个参数指定了这个线程的入口函数的名称。就像C语言的的main()函数一样每一线程都有一个开始执行的入口点。但是与main()函数不同,线程的入口函数名称不是固定的。可以根据这个参数指定入口函数名。但是针对于每个线程这个线程,它的入口函数的返回类型、调用方式、参数个数和类型是固定的。

这个函数的格式如下

DWORD WINAPI xxx( LPVOID P )

第四个参数指定了传递给这个线程的入口函数的参数值

第五个参数是一个整数值,这个整数值实际上包含了一组标志。一般情况我们把这组参数的值设为0,这样当这个线程被创建出来之后它就立即成为可执行可调度的状态。我们一个将其设为CREATE_SUSPENDED,这时被新创建出来的线程将处于挂起状态不能马上被执行。

最后一个参数是一个LPDWORD类型的指针,当CreateThread函数返回的时候,我们就可以在这个指针所指向的内存单元中得到刚刚创建的这个线程的ID号。

CreateThread函数的返回值是一个句柄,我们可以使用这个句柄来操作这个新创建出来的线程。

线程的销毁是由操作系统自动完成的。当一个进程结束的时候也就是程序退出的时候,OS会自动销毁掉这个进程所包含的所有线程。但是由于每一个线程都占有一定的资源,所以当进程结束之前如果我们不再需要某一个线程了,就应该通知OS及时回收这个线程所占用的资源。通知工作就是通过调用CloseHandle完成的。

BOOL CloseHandle(
HANDLE hObject
);

唯一的一个参数指向我们已经不再使用的这个线程的对象

为了防止主进程在子线程还没有执行完毕之前就退出,我们使用

DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);

它可以用来等待一个对象,调用这个函数的线程将被挂起,直到两种条件中的至少一种满足时才能返回得以继续运行。

第一条件:The time-out interval elapses 如果WaitForSingleObject等待时间太长,即线程的挂起时间已经超过了某一个时间的上限值,那么WaitForSingleObject将会返回。这个时间的上限值是通过WaitForSingleObject函数的第二个参数dwMilliseconds来指定的。dwMilliseconds单位是毫秒。我们可以把它的参数设为INFINITE,在这种情况下WaitForSingleObject将永远不会因为等待时间太长而返回。

第二条件:The specified object is in the signaled stateWaitForSingleObject所等待的对象处于有信号状态。一个对象是出于有信号状态还是无信号状态是由它自己的类型和它当前所处的状态而决定的。对于一个线程对象而言当他还在执行的时候,它所对应的句柄将是无信号状态的。只有当线程退出之后这个对象才会处于有信号状态。

WaitForSingleObject使用时往往将第一个参数设置为附加线程即子线程的句柄,第二个参数设置为INFINITE,我们就可以确保主线程只有在附加线程结束之后才会得到继续运行。

注意:调用WaitForSingleObject的线程会被挂起,因此在等待过程中被挂起的线程并不会浪费CPU的时间。WaitForSingleObject有一个缺点就是它只能被用来等待一个对象如果我们有多个对象需要等待我们要调用另外一个函数:WaitForMultipleObjects。它可以用来等待多达64个对象

DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);

第一个参数指定需要等待的对象个数

第二个参数是一个数组,在调用WaitForMultipleObjects之前,我们应该把被等待对象的句柄连续地放入这个数组之中。

第三个参数是一个BOOL类型的参数,当他的值为TRUE的时候只有当所有的被等待对象全部处于有信号之后,WaitForMultipleObjects才会返回(Wait for All)。如果值为FALSE只要这若干个被等待对象有一个处于有信号状态,WaitForMultipleObjects就将返回(Wait for Any)。

第四个参数指定等待时间的上限值。

为了避免主线程和附属线程竞争访问存取同一个变量我们可以使用两种方法。

第一种:尽量使用局部变量。我们可以在附属线程的入口函数声明这个变量。也可以在附属线程的堆栈上,也可以使用TLSThread Local Storage)技术。

第二种:使用同步对象来保护临界区域。所谓临界区域是这样一段代码。在其中我们存储了某种全局变量。但是我们又不能允许多个线程同时运行这段代码。为了达到只允许一个线程运行这段代码的目的。我们将使用同步对象来保护它。下面我们回顾一下windows下常用的同步对象。

第一种同步对象是互斥对象(Mutex)。这是一种内核对象。线程可以获得对Mutex对象的所有权,当有线程拥有这个Mutex对象的时候,Mutex对象会处于无信号状态。如果没有任何一个线程当前拥有这个对象,Mutex对象将处于有信号状态。下面说明使用Mutex对象的3种操作。

第一种是CreateMutex

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);

当我们使用CreateMutex时我们就可以创建一个Mutex对象来。

第二种操作是WaitForSingleObject

DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);

当一个线程调用WaitForSingleObject

来等待一个互斥对象的时候,这个调用线程会被挂起,直到没有任何一个线程拥有这个互斥对象为止。这个时候调用线程会被唤醒继续执行。而这个Mutex对象会被重新置为无信号状态。也就是说这个等待了很久的线程终于获得这个Mutex对象了。点三种,当我们不再需要一个Mutex对象的时候我们应该及时调用ReleaseMutex来释放掉这个Mutex对象。

BOOL ReleaseMutex(
HANDLE hMutex
);

Mutex对象是可以有名字的因此它可以用来同步不同的进程。

使用Mutex对象实际上已经达到了枷锁和解锁的目的。但是它有一个缺点,就是Mutex对象的操作是比较费时的。

第二种:为了提高程序的运行效率我们可以使用另外一种更轻量级的同步对象就是Critical Section 对象。

Critical Section 对象所涉及的操作和所花销的时间都不多。但是它也有一个缺点就是它只能用于一个进程的内部的线程间的同步。也就是说Critical Section 对象它不能垮进程使用。相对Mutex对象而言Critical Section 对象更为常用。下面我们来详细地看一下Critical Section 对象的使用方法。

要使用Critical Section 对象我们首先要声明一个CRITICAL_SECTION结构。

例:CRITICAL_SECTION cs;

接着我们使用InitializeCriticalSection

void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);

来初始化这个结构

例:InitializeCriticalSection( &cs );

当我们不需要这个对象的时候我们可以使用DeleteCriticalSection

void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);

来销毁掉这个结构。例:DeleteCriticalSection( &cs );

假设A线程企图执行某临界区,则在一个临界区域的开始位置,A线程会发现这段临界区的开始位置被设置了EnterCriticalSection

void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);

例:EnterCriticalSection( &cs );

因为此时有B线程正在运行这段临界区域(也就是说编写临界区的人设置了EnterCriticalSection),那么EnterCriticalSection将会把刚才企图执行临界区域的A线程挂起,只有当其他的线程(B线程)都退出了临界区域之后,刚才企图执行临界区的A线程才会被唤醒,然后继续执行。如果当前没有任何线程在执行临界区域,EnterCriticalSection会立刻返回。在临界区域的结束位置我们可以调用LeaveCriticalSection

void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);

例:LeaveCriticalSection( &cs );

来允许其他的线程进入这段临界区域。需要说明的是EnterCriticalSectionLeaveCriticalSection必须配对使用。

事件对象 Event

Event 对象通常用来通知其他的线程某个事件已经发生了。这种事件通常是指某个消息或者是某个数据已经准备就绪了。

我们可以通过调用WaitForSingleObject或者是WaitForMultipleObjects来等待这种事件的发生。

Event 对象可以被分为两种:一种是自动复位对象(Auto-reset)一种是手动复位对象(Manual-reset)。

对于一个自动复位的Event对象而言,如果当前有多个线程正在等待这个对象,那么当这个对象变为有信号对象的时候,只有这若干个等待线程中的一个被唤醒。而且这个Event 对象将立刻被置为无信号状态。与此不同,当多个线程等待在一个手动复位的(Manual-resetEvent对象的时候,一旦这个对象变为有信号状态这若干个对象将都被唤醒

我们可以通过调用CreateEvent函数来创建一个Event对象。

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);

第一个参数是一个指向安全属性结构的指针,我们通常情况下把它的数值赋为NULL

第二个参数指定了创建的Event对象的类型,当为TRUE时我们创建的Event对象为手动复位型,当为FALSE时我们创建的Event对象为自动复位型。

第三个参数指明了这个新创建出来的Event对象初始状态是怎样的。如果为TRUE,则Event对象为有信号状态,反之初始状态为无信号。用户可以通过调用SetEventResetEvent这两个函数来手工的将Event对象设置为有信号状态和无信号状态。

第四个参数指定了新创建事件名称。如果指定参数为NULL,则创建了一"个无名"事件。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值