1、创建线程
Windows多线程开发中一般使用CreateThread()函数来创建线程,CreateThread()函数原型如下:
HANDLEWINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
-
lpThreadAttributes表示内核对象的安全属性,一般传入NULL表示不能继承
-
dwStackSize表示线程初始栈的大小,若为0则表示采用默认大小初始化
-
lpStartAddress表示线程要执行的函数,类似于回调函数
-
lpParameter表示接收函数的参数
-
dwCreationFlags表示创建线程时的标志,0表示创建之后立即执行
-
lpThreadId保存线程ID
DWORD WINAPI ThreadFun(LPVOID pM) {示例:
printf("HelloWorld\n");
return0;
}
//主函数,所谓主函数其实就是主线程执行的函数。
int main()
{
HANDLEhandle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL); //创建子线程
return0;
}
除了使用CreateThread()来创建线程之外,大多数情况下使用_beginthreadex()进行线程的创建。,_beginthreadex()函数在创建新线程时会分配并初始化一个内存块。这个内存块自然是用来存放一些需要线程独享的数据,新线程运行时会首先将内存块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得内存块的地址再将需要保护的数据存入内存块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。
等待函数WaitForSingleObject()使线程自愿进入等待状态,直到指定的内核对象变为已通知状态为止。
2、原子操作
在多线程环境下,对变量的自增自减这些简单的语句要慎重思考,防止多个线程导致的数据访问出错。由于线程的执行的并发性,很可能线程1在执行过程中,线程2也在执行并且修改了某个共享数据,这样容易造成数据混乱。所以,在多线程环境下对一个共享变量进行读写操作时,可以进行线程的原子操作,也就是一个线程必须等待另一个线程执行完毕才可以开始执行。Windows提供了Interlocked开头的函数来完成这项工作。
-
增减操作
LONG__cdecInterlocked Increment(LONG volatile* Addend);
LONG__cdecInterlocked Decrement(LONG volatile* Addend);
LONG__cdecInterlocked ExchangeAdd(LONG volatile* Addend, LONG Value);
-
赋值操作
LONG__cdeclInterlocked Exchange(LONG volatile* Target, LONG Value);
3、关键段
关键段是让一段共享资源代码段进行原子操作的方法。关键段的进入和离开的相关函数如下:
CRITICAL_SECTION g_csThreadParameter; //定义关键段变量
InitializeCriticalSection(&g_csThreadParameter); //关键段变量定义之后必须先初始化
voidEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
//共享资源//
voidLeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
DeleteCriticalSection(&g_csThreadParameter); //销毁关键段变量
关键段EnterCriticalSection会检查CRITICAL_SECTION中的成员变量,如果没有线程正在访问资源,那么EnterCriticalSection则会更新成员变量,表示当前线程已获准对资源的访问,并且立即返回,线程得以执行。
如果调用线程已经获准进入访问资源,那么EnterCriticalSection会更新成员变量RecursionCount记录线程进入的次数,其他线程则会进入等待状态。调用LeaveCriticalSection之后次数变为0时,系统会更新成员变量并将等待的线程切到可调度状态。
当线程试图进入另一线程所拥有的关键代码段时,线程即进入等待状态,这时候线程会从用户方式进入内核方式,这种转换会付出很大的开销。所以在关键段里引入了循环锁的概念,线程循环一定次数无法进入关键代码段时,线程才会进入内核状态,以此来减小程序的开销。
关键段只能用于对同一进程内的不同线程进行同步,不能用于多个进程的多个线程之间的同步。
4、事件
多线程的事件Event实际上是一个内核对象,它通常用于多线程之间的同步。事件内核对象分为人工重置的和自动重置的事件,当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。事件一开始初始化为未通知状态,在一个线程完成它的初始化后,事件就被置为通知状态,此后,一直等待该事件的另一个线程发现事件已经为通知状态,这时该线程就变为可调度状态。
创建事件内核对象可以使用CreateEvent()函数,它的原型如下:
-
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPTSTR lpName
);
-
lpEventAttributes一般设置为NULL,表示安全控制。
-
bManualReset手动重置(true)或者自动重置(false)。
-
bInitialState表示初始的触发状态
-
lpName事件名称
在事件的触发状态设置中,可以使用SetEvent()和ResetEvent()函数来进行触发和未触发事件。在事件从等待到触发的过程中,可以使用WaitForSingleObject()进行设置,因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。
此外,OpenEvent()可以根据名称获得一个事件句柄。
示例:
HANDLEg_hEvent;
VoidWinMain()
{
g_hEvent=CreateEvent(NULL,true,false,NULL); //设置一个手动重置对象的事件
thread1=beginthreadex(NULL,0, WordCount, NULL, 0, &dwThreadID);
thread2 = _beginthreadex(NULL, 0,SpellCheck, NULL, 0, &dwThreadID);
thread3 = _beginthreadex(NULL, 0,GrammarCheck, NULL, 0, &dwThreadID);
OpenFileAndReadContentsIntoMemory(...);
//通知等待的线程,告诉他们,我的工作做完了,你们可以开始执行了
SetEvent(g_hEvent);
}
DWORDWINAPI WordCount(PVOID pvParam)
{
//等待事件
WaitForSingleObject(g_hEvent, INFINITE);
//Access the memory block.
...
return(0);
}
DWORDWINAPI SpellCheck(PVOID pvParam)
{
//等待事件
WaitForSingleObject(g_hEvent, INFINITE);
//Access the memory block.
...
return(0);
}
DWORDWINAPI GrammarCheck(PVOID pvParam)
{
//Wait until the file's data is in memory.
WaitForSingleObject(g_hEvent, INFINITE);
//Access the memory block.
...
return(0);
}
由于是手动重置事件,在主线程执行完操作时调用SetEvent(),三个子线程都可以进入调度状态。如果使用的时自动重置事件,那么在事件触发后只有一个线程变为可调度状态,此时需要在每个子线程加上SetEvent操作,才能使每个线程得到调度并且执行。
-
5、 互斥量
-
互斥量包含一个线程ID、一个使用数量和一个递归计数器。ID用于表明那个线程当前拥有该互斥对象,递归计数器表明该线程拥有互斥对象的次数。如果线程ID为0,那么互斥对象不被线程拥有,并且发出互斥对象的通知信号。
互斥量是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。
使用CreateMutex()函数创建互斥量,原型如下:
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
-
bInitialOwner初始化对象的状态,如果传入FALSE,那么线程ID和递归计数器都被设置为0,初始化为触发状态,如果传入TRUE,那么对象的线程ID会被设置成当前调用线程,并初始化为未触发
使用ReleaseMutex()释放互斥量。
示例:
HANDLEhMutex = CreateMutex(NULL, FALSE, NULL); //创建一个互斥量
T Read()
{WaitForSingleObject(hMutex, INFINITE);
//read the buffer
ReleaseMutex(hMutex);
}
void Write(T data)
{WaitForSingleObject(hMutex, INFINITE);
//write the buffer
ReleaseMutex();
}
-
6、信号量
-
信号量内核对象是这样的一种对象:它维护一个资源计数,当资源计数大于0,处于触发状态;资源计数等于0时,处于未触发状态;资源计数不可能小于0,也绝不可能大于资源计数上限。
只有资源计数>0时才是触发状态,资源=0时为未触发状态,而WaitForSingleObject成功将递减资源计数,调用ReleaseSemaphore将增加资源计数。当调用ReleaseSemaphore使资源数增加时,相应的WaitForSingleObject等待线程将进入调度状态。
使用CreateSemaphore()创建信号量,ReleaseSemaphore()递增信号量的当前资源计数。
原型:
HANDLECreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
-
lpSemaphoreAttributes表示安全控制,一般直接传入NULL。
-
lInitialCount表示初始资源数量。
-
lMaximumCount表示最大并发数量。
-
lpName表示信号量的名称,传入NULL表示匿名信号量。