11.Win多线程(笔记)


windows线程是可以执行的代码的实例。系统是以线程为单位调度程序 。一个程序当中可以有多个线程,实现多任务的处理。

windows线程的特点:

  • 每个线程都具有一个ID

  • 每个线程都具有自己的内存栈(其余内存空间都是共享的)

  • 同一进程中的线程使用同一个地址空间

线程的调度

  • 操作系统将CPU的执行时间划分成时间片,依次根据时间片执行不同的线程。

  • 线程轮询:线程A->线程B-> 线程A。。。。

1.主要函数列表

函数名功能
CreateThread()创建一个新线程
void ExitThread(DWORD dwExitCode)正常结束一个线程的执行
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode)强制终止一个线程的执行
DWORD ResumeThread(HANDLE hThread)重启一个线程
DWORD SuspendThread(HANDLE hThread)挂起一个线程
GetExitCodeThread()得到一个线程的退出码
GetThreadPriority()得到一个线程的优先级
SetThreadPriority()设置一个线程的优先级
CloseHandle()关闭一个线程的句柄
CreateRemoteThread()在另一个进程中创建一个新线程
PostThreadMessage()发送一条消息给指定的线程
GetCurrentThread()得到当前的线程句柄
GetCurrentThreadId()得到当前线程的ID
GetThreadId()得到指定线程的ID
WaitForSingleObject()等待单个对象
WaitForMultipleObjects()等待多个对象
HANDLE WINAPI CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress, // 线程处理函数的函数地址
    _In_opt_ __drv_aliasesMem LPVOID lpParameter, // 传递给线程处理函数的参数
    _In_ DWORD dwCreationFlags, // 线程的创建方式 1) 0 立即执行方式,创建后立即执行 2) CREATE_SUSPEND 挂起方式,创建后先处于休眠,唤醒后继续执行
    _Out_opt_ LPDWORD lpThreadId // 创建成功,返回线程的ID
    );

//等待单个句柄有信号
//必须是可等待句柄
void WaitForSingleObject(
	HANDLE handle,			//句柄BUFF的地址
    DWORD dwMilliseconds	//等候时间ms级别,INFINITE 无限时间
);

//等待多个句柄有信号
DWORD WaitForMultipleObjects(
	DWORD nCount,				//句柄数量
    const HANDLE *lpHandles,	//句柄的地址
    BOOL bWaitAll,				//等待方式 是否等待全部有信号,还是不等待全部有信号就结束等候
    DWORD dwMilliseconds		//等候时间 INFINITE 永无超时
);

2.线程函数的定义

线程函数的规范格式定义为:

DWORD WINAPI ThreadProc (LPVOID lpParam);

格式不正确将无法调用成功,函数名称没有限制,只要符合命名规则就可以。

线程函数必须是全局函数,不能在类中声明和定义。

3.线程同步

原因:操作临界资源,多个线程同时操作。

3.1 原子锁

3.1.1 相关问题

多个线程对同一个数据进行原子操作,会产生结果丢失。

例如执行++运算时,错误代码分析:

  • 当线程A执行g_value++时,如果线程切换时间正好是在线程A将值保存到g_value之前
  • 线程B继续执行g_value++,那么当线程A再此被切换回来之后,会将原来线程A保存的值保存到g_value上,线程B进行的加法操作被覆盖

使用原子锁函数,性能会牺牲。

3.1.2 Window 官方说明及相关API

应用程序必须同步对多个线程共享的变量的访问。应用程序还必须确保对这些变量的操作以原子方式执行(完全执行,或者根本不执行)。

对正确对齐的32位变量的简单读取和写入操作是原子操作。换句话说,最终不会只更新变量的一部分;所有位都以原子方式更新。但是,不保证访问时同步的。如果两个线程从同一个变量读取和写入,则无法确定一个线程是否在另一个线程执行其写入操作之前执行其读取操作。

对正确对齐的64位变量的简单读取和写入操作在64位Windows上是原子的。64位只的读取和写入不保证在32位Windows上是原子的。

对于其他大小的变量,读取和写入操作不保证在任何平台上都是原子的。

互锁API

互锁函数提供了一种简单机制,用于同步对有多个线程共享的变量的访问。它们还以原子方式对变量执行操作。如果变量位于共享内存中,则不同进程的线程可以使用这些函数

  1. InterlockedIncrementInterlockedDecrement 执行递增或递减功能

    函数将递增或递减变量所涉及的步骤合并为原子操作。

    此功能在多任务操作系统中非常有用,在该操作系统中,系统可以中断一个线程的执行,以将一段处理器时间分配给另一个线程。

    如果没有这种同步,两个线程可以读取相同的值,将其递增1,并存储新值,使总值增加1而不是2。

    互锁变量访问函数可防止出现此类错误。

  2. InterlockedExchangeInterlockedExchangePointer 交换值

    函数以原子方式交换指定变量的值。

  3. InterlockedExchangeAdd 相加

    函数将两个操作组合在一起:将两个变量相加,并将结果存储在其中一个变量。

  4. InterlockedCompareExchangeInterlockedCompare64Exchange128InterlockedCompareExchangePointer 比较

    函数将两个操作组合在一起:比较两个值,并根据比较结果,将第三个值存储在其中前一个变量中。

  5. InterlockedAndInterlockedOrInterlockedXor 与、或、异或

    函数分别以原子方式执行AND、OR和XOR操作。

有一写函数专门用于对64位内存值和地址执行互锁变量访问,并针对64位Windows使用进行了优化。其中每个函数的名称中包含“64”;例如 InterlockedDecrement64InterlockedCompareExchangeAcquire64

大多数互锁函数在所有Windows平台上提供完整的内存屏障。还有一些函数将基本的互锁变量访问操作与某些处理器支持的获取和释放内存排序语义相结合。

其中每个函数的名称中包含“Acquire”或“Release”一词;例如, InterlockedDecrementAcquireInterlockedDecrementRelease。获取内存语义指定在尝试任何其他内存操作之前,当前线程执行的内存操作将可见。释放内存语义指定当前线程正在执行的内存操作,将在所有其他内存操作完成后可见。通过这些语义,可以强制按特定顺序执行内存操作。在进入受保护区域时使用获取语义,在离开该区域时使用释放语义。

3.1.3 原子锁的实现

直接对数据所在的内存操作,并且在任何一个瞬间只能有一个线程访问,也就是说直接操作内存,不涉及寄存器。

  • 尝试对数据所在内存进行加锁(独占锁)
  • 加锁成功后操作数据,加锁失败会堵塞,这也是为什么执行速度回慢一些,但是会保证结果的正确性!

示例代码:

#include <iostream>
#include <Windows.h>
 
long g_value = 0;
DWORD WINAPI PTHREAD_START_ROUTINE_Impl(LPVOID lpThreadParameter)
{
	for (int i = 0; i < 100000; i++)
	{
		InterlockedIncrement(&g_value);
		// g_value++;
	}
	return 0;
}
 
DWORD WINAPI PTHREAD_START_ROUTINE_Impl2(LPVOID lpThreadParameter)
{
	for (int i = 0; i < 100000; i++)
	{
		InterlockedIncrement(&g_value);
		// g_value++;
	}
	return 0;
}

int main()
{
	LPDWORD threadid1 = 0, threadid2 = 0;
	HANDLE handles[2];
	handles[0] = CreateThread(NULL, 1024, PTHREAD_START_ROUTINE_Impl, 0, 0, threadid1);
	handles[1] = CreateThread(NULL, 1024, PTHREAD_START_ROUTINE_Impl2, 0, 0, threadid2);
 
	// 需要保证主线程不退出
	WaitForMultipleObjects(2, handles, true, INFINITE);
	printf("wait over!   %d\n", g_value);
 
	getchar();
	return 0;
}

3.1.4 原子锁优缺点

**缺点:**API函数太多。局限性大,只针对操作符号。

**优点:**效率相对最高。

3.2 互斥锁

3.2.1 概述

互斥锁是一种同步对象,当没有任何线程拥有互斥锁时,互斥锁处于有信号(signaled)状态,当互斥锁被某个线程拥有时,则它处于无信号状态(nonsignaled)。

互斥锁就是一种为了达到访问共享资源而互斥目的的锁。

特点:

  • 任何一个互斥锁,一次只能被一个线程拥有。
  • 可以跨进程使用,即进程间同步。

使用场景:

同步一些共享资源,比如共享内存(shared memory)

3.2.2 相关API

1. CreateMutex 函数

  • 作用:创建或打开命名或未命名的互斥锁对象。如果某互斥锁已经被创建,当再次使用CreateMutex操作该互斥锁,实际的操作等效于OpenMutex,但通过GetLastError会返回ERROR_ALREADY_EXISTS标识。

  • 语法

    HANDLE CreateMutexA(
    	[in,optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
        [in] BOOL bInitialOwner,
        [in,optional] LPCSTR lpName
    );
    
  • 参数说明

    • lpMutexAttributes,为NULL时,句柄不能被子进程继承。
    • bInitialOwner,为true时,创建该互斥锁的线程获取该互斥锁。
    • lpName,互斥锁的名字,为NULL时,为未命名互斥锁,关于未命名互斥锁如何传递,见下方”未命名互斥锁的同步“。

2. OpenMutex 函数

  • 作用:作用是为现有的一个已命名互斥体对象创建一个新句柄。

  • 语法

    HANDLE OpenMutex(
    DWORD dwDesiredAccess, // access
    BOOL bInheritHandle, // inheritance option
    LPCTSTR lpName // object name
    );
    
  • 参数说明:

    • dwDesiredAccess 表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
    • bInheritHandle 表示互斥量句柄继承性,一般传入TRUE即可。
    • lpName 表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
    • 函数访问值:成功返回一个表示互斥量的句柄,失败返回NULL。

3. Wait 函数

  • Wait函数是一系列提供类似功能的等待函数(如WaitForMultipleObjects),该函数的作用是请求某个互斥锁的使用权,若没有获取到,则阻塞。
  • 等待函数的返回值,表明等待函数因为某些原因返回,而不是正常的互斥锁信号转换
  • 多个线程等待互斥锁时,只有一个线程会被随机选择获取互斥锁

4. ReleaseMutex 函数

  • 作用:释放控制权,释放后,互斥锁变为有信号状态。

  • 语法

    BOOL ReleaseMutex(
    	[in] HANDLE hMutex
    );
    
  • 参数

    hMutex 是要释放的互斥锁句柄

5. CloseHandle 函数

  • 作用:关闭句柄,此处即关闭互斥锁。

  • 注:

    • 除了使用CloseHandle手动关闭句柄外,当某个进程终止后,也会自动关闭句柄

    • CloseHandle关闭当前线程中,使用的句柄,但是如果还有其他线程拥有该句柄,那么句柄对象并未真正的关闭,只有当最后一个该对象被关闭,句柄才会真正关闭。
      在这里插入图片描述

      如图,只有当所有句柄均被关闭,互斥锁对象才会自动关闭。

3.2.3 其他说明

1. 互斥锁的名字

互斥锁的名字和其他同步对象(如,事件,信号量)的名字位于相同的命名空间,因此如果互斥锁有名字为"ExampleName",事件也有名字为"ExampleName"则发生冲突,通过GetLastError函数返回ERROR_INVALID_HANDLE错误。

2. 未命名互斥锁的同步

  • 命名的互斥锁我们可以很容易理解,如何让两个或多个线程使用相同的互斥锁。而未命名的互斥锁要如何让系统中多个线程与同一个互斥锁产生联系那,答案是通过线程间(或进程间)复制句柄或者父子句柄继承实现。

  • 复制句柄:

    通过该方法可以在两个进程之间传递句柄,但**相比于命名句柄和父子继承的方式,这种方式是最麻烦的,**它需要再创建句柄的进程和使用句柄的进程间进行通信(如,命名管道,命名共享内存),当然这一步Windows通过高层函数隐藏了底层实现的细节,也就是说,你只需要调用一个DuplicateHandle函数即可完成两个进程的通信。

    **另一个需要注意的地方,**复制的句柄本质上和源句柄是等同的,可以理解为指针间的赋值,赋值过后的两个指针实际是指向的相同的区域,任何改变都会影响两个指针指向的区域,句柄也是如此。

  • DuplicateHandle 说明

    BOOL DuplicateHandle(
    	[in] HANDLE hSourceProcessHandle,
        [in] HANDLE hSourceHandle,
        [in] HANDLE hTargetProcessHandle,
        [out] LPHANDLE lpTargetHandle,
        [in] DWORD dwDesireAccess,
        [in] BOOL bInheritHandle,
        [in] DWORD dwOptions
    );
    
    • hSourceProcessHandle:源进程的句柄,该进程必须有PROCESS_DUP_HANDLE的接入权限。源句柄可以通过GetCurrentProcess获得。
    • hSourceHandle:要被复制的句柄,比如互斥锁句柄。
    • hTargetProcessHandle:目标进程的句柄,该进程必须有PROCESS_DUP_HANDLE的接入权限。目标句柄可以通过OpenProcess获得。
    • lpTargetHandle:注意它是一个指针,指向复制过来的句柄,“LP” 是 long pointer 的缩写。
    • dwDesiredAccess:新句柄的权限设置。一般通过复制得到句柄的权限范围 <= 原句柄。
    • bInheritHandle:该句柄能否被继承。
    • dwOptions:一些可选的配置项。

3. 互斥锁的意外终止

比如,当前拥有互斥锁的线程终止,而该线程并未释放互斥锁,此时互斥锁被标记为遗弃(abandoned),它表明互斥锁未被正确释放。

其他等待该互斥锁的线程可以获取它,但对应的wait函数会返回WAIT_ABANDONED来表明互斥锁对象被遗弃,由此表明此时被互斥锁操作的共享资源处于未定义的状态(undefined state)。

4. 临界区对象

临界区(critical section)对象提供类似与互斥锁的功能,区别在于临界区对象不提供进程间同步,只能提供同一进程中的线程的同步。

注:这里的临界区对象的是 Windows 提供的一种用户模式下的,线程同步机制,不完全等同于操作系统中的临界区这个概念。

3.2.4 示例

bool ACopyFileExt(std::string src_filename, std::string dst_filename)
{
	HANDLE mutex;
	mutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "ACopyFileExt");
	if (mutex == NULL)
	{
		mutex = CreateMutex(NULL, FALSE, "ACopyFileExt");
	}
	WaitForSingleObject(mutex, INFINITE);

	//功能区
	bool rlt = ACopyFile(src_filename, dst_filename);

	ReleaseMutex(mutex);
	return rlt;
}

3.3 事件

3.3.1 概述

事件(Event)是Win32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同,事件可以分为两类:

  • 手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。
  • 自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。

使用"事件"机制应注意一下事项:

  • 如果跨进程访问事件,必须对事件名称,在对事件名称的时候,要注意不要与系统命名空间中的其他全局名称冲突;
  • 事件是否要自动恢复;
  • 事件的初始状态设置

由于event对象属于内核对象,故进程B可以调用OpenEvent函数通过对象的名称获得进程A中event对象的句柄,然后将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一线程的运行,例如:

HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);

3.3.2 API函数

1. CreateEventA 创建事件

HANDLE WINAPI CreateEventA(
	_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,		//安全属性,设置为0即可
    _In_ BOOL bManualReset,		//手工重置
    _In_ BOOL bInitialState,	//初始创建 true有信号
    _In_opt_ LPCSTR lpName		//事件名
);
  • 参数说明:

    • bManualReset 如果为false,表示这个event将在变成激发状态之后自动重置为非激发状态,如果是true,表示不会自动重置,必须靠程序操作(调用ResetEvent()才能将激活状态的event重置为非激活状态)。
    • bInitialState 如为true,表示这个event一开始处于激发状态,如为false,表示这个event一开始处于非激发状态。
    • lpName Event对象的名称。任何线程或进程都可以根据这个文字名称,使用这一event对象。
  • 返回值

    如果调用成功,会传回一个event handle,GetLastError() 会传回0。如果 lpName 已经存在,则不会产生一个新的,返回该event handle,如果创建失败,则返回null。

2. 等候事件

WaitForSingleObject / WaitForMultipleObjects

3. 触发事件(将事件设置成有信号状态)

BOOL SetEvent(
	HANDLE hEvent	//handle to event
);

4. 复位事件(将事件设置为无信号状态)

BOOL ResetEvent(
	HANDLE hEvent	//handle to event
);

5. 关闭事件CloseHandle

3.3.3 示例

#include <windows.h>
#include <stdio.h>

//线程数
#define THREADCOUNT 4

//worker 签名
DWORD WINAPI ThreadProc(LPVOID);

//handle
HANDLE ghWriteEvent;
HANDLE ghThreads[THREADCOUNT];

//创建线程和事件
void CreateEventsAndThreads(void)
{
    int i;
    DWORD dwThreadID;
    
    // Create a manual-reset event object. The write thread sets this
    // object to the signaled state when it finishes writing to a
    // shared buffer
    ghWriteEvent = CreateEvent(
		NULL,				//default security attributes
        TRUE,				//manual-reset event
        FALSE,				//initial state is nonsignaled
        _T("WriteEvent")	//object name
    );
    
    if(ghWriteEvent == NULL)
    {
        printf("CreateEvent failed (%d)\n", GetLastErrir());
        return;
    }
    
    // Create multiple threads to read from the buffer
    for(i = 0;i < THREADCOUNT;i++)
    {
        //TODO: More conplex scenarios may require use of a parameter
        //to the thread procedure, such as an event pre thread to
        //be used for synchronization
        ghThreads[i] = CreateThread(
        	NULL,			// default security
            0,				// default stack size
            ThreadProc,		// name of the thread function
            NULL,			// no thread parameters
            0,				// default startup flags
            &dwThreadID);
        
        if(ghThread[i] == NULL)
        {
            printf("CreateThread failed (%d)\n",GetLastError());
            retrun;
        }
    }
}

//关闭事件
void CloseEvents()
{
    // Close all event handles(currently,only one global handle)
    CloseHandle(ghWriteEvent);
}

//主线程写操作
void WriteToBuffer(VOID)
{
    // TODO:Write to the shared buffer
    printf("Main thread writing to the shared buffer...\n");
    
    // Set ghWriteEvent to signaled
    if(!SetEvent(ghWriteEvent))
    {
        printf("SetEvent failed (%d)\n",GetLastError());
	}
}

//其他线程完成读操作
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    //lpParam not used in this example
    UNREFRENCED_PARAMETER(lpParam);
    
    DWORD dwWaitResult;
    
    printf("Thread %d waiting for write event...\n",GetCurrentThreadId());
    
    dwWaitResult = WaitForSingleObject(
    	ghWriteEvent,		//event handle
        INFINITE			//indefinite wait
    );
    
    switch(dwWaitResult)
    {
        //Event object was signaled
        case WAIT_OBJECT_0:
            //Read from the shared buffer
            printf("Thread %d reading from buffer\n",
                   GetCurrentThreadId());
            break;
            
        // An error occurred:
        default:
            printf("Wait error (%d)\n",GetLastError());
            return 0;
    }
    // Now that we are done reading the buffer, we could use another
    // event to signal that this thread is no longer reading. This
    // example simply uses the thread handle for synchronization (the
    // handle is signaled when the thread terminates)
    printf("Thread %d exiting\n",GetCurrentThreadId());
    return 1;
}

//入口
int main(void)
{
    DWORD dwWaitResult;
    // Create the shared buffer
    
    // Create events and THREADCOUNT Threads to read from the buffer
    CreateEventsAndThreads();
    
    // At this point, the reader threads have started and are most
    // likely waiting for the global event to be signaled.However,
    // it is safe to write to the buffer because the event is a
    // manual-reset event
    WriteToBuffer();
    printf("Main thread waiting for threads to exit...\n");
    
    // The handle for each thread is signaled when the thread is
    // terminated
    dwWaitResult = WaitForMultipleObjects(
    	THREADCOUNT,		//number of handles in array
        ghThreads,			//array of thread handles
        TRUE,				//wait until all are signaled
        INFINITE);
    switch(dwWaitResult)
    {
        // All thread objects were signaled
        case WAIT_OBJECT_0: 
            printf("All threads ended, cleaning up for application exit...\n");
            break;

        // An error occurred
        default: 
            printf("WaitForMultipleObjects failed (%d)\n", GetLastError());
            return 1;    
    }
    // Close the events to clean up
    CloseEvents();
    return 0;
}

3.4 信号量(Semaphores)

3.4.1 概述

信号量是解决各种生产者/消费者问题的关键要素。这种问题会存在一个缓冲区,可能在同一时间内被读出数据或被写入数据。

Win32中的一个 Semaphore 可以被锁住最多n次,其中n是semaphore被产生时指定的。n常常被设计用来代表"可以锁住一份资源"的线程个数,不过并非单独一个线程就不能够拥有所有的锁定,这个没什么理由可言。

理论上说,mutexsemaphore的一种退化。如果你产生一个semaphore并令最大值是1,那就是一个mutex。也因此,mutex又常常被称为binary semaphore。如果某个线程拥有一个binary semaphore,那么就没有其他线程能够获得其拥有权。在Win32中,这两种东西的拥有权(ownership)的意义完全不同,所以它们不能够交换使用,semaphores不像mutexes,它并没有所谓的“wait abandoned”状态可以被其他线程侦测到。

在许多系统中,semaphores常被使用,因为mutexes可能并不存在,因为mutex存在的缘故,

信号量的特点和用途,可以用下列几句话定义:

  • 如果当前资源的数量大于0,则信号量有效;
  • 如果当前资源数量是0,则信号量无效;
  • 系统决不允许当前资源的数量为负值;
  • 当前资源数量决不能大于最大资源数量。

3.4.2 API函数

1. CreateSemaphore 产生信号量(Semaphore)

  • 作用:在Win32环境中产生一个Semaphore

  • 语法

    HANDLE CreateSemaphore(
    	LPSECURITY_ATTRIBUTES lpAttributes,
        LONG lIntialCount,
        LONG lMaximumCount,
        LPCTSTR lpName
    );
    
  • 参数

    • lpAttributes 安全属性,如果是NULL,就表示要使用默认属性
    • lInitialCount semaphore的初值,必须大于或等于0,并且小于或等于lMaximumCount
    • lMaximumCount semaphore的最大值,这也就是在同一时间内能够锁住semaphore之线程的最多个数。
    • lpName semaphore的名称(一个字符串)。任何线程(或进程)都可以根据这一名称引用到这个semaphore。这个值可以为NULL,意思是产生没有名字的semaphore。
  • 返回值

    如果成功,就返回一个handle,否则返回NULL,不论那一种情况,GetLastError()都会传回一个合理的结果。

    如果指定的semaphore名称以及存在,则该函数还是成功的。GetLastError()会返回ERROR_ALREADY_EXISTS

2. WaitFor… 等候信号量

Semaphore的现值代表的意义是目前可用的资源数,如果semaphore的现值为1,表示还有一个锁定动作可以成功;如果现值为5,就表示还有五个锁定动作可以成功。

每当一个锁定动作成功,semaphore的现值就会减1,你可以使用任何一种WaitFor...函数(例如WaitForSignalObject())要求锁定一个一个semaphore。因此,如果semaphore的现值不为0,Wait…函数会立即返回。

但是如果锁定成功,也不会收到Semaphore的拥有权,因为可以有一个以上的线程同时锁定一个semaphore。semaphore并没有所谓“独占锁定”,也因为没有拥有权的观念,一个线程可以反复调用Wait…()函数以产生新的锁定。这和mutex绝不相同,拥有mutex的线程不论调用多少次Wait…函数,也不会被阻塞住。

一旦semaphore的现值降到0,就表示资源已经耗尽。此时,任何线程如果调用Wait…()函数,必然要等待,直到某广告锁定解除为止。

3. ReleaseSemaphore 解除锁定

  • 作用

    用于解除锁定,将semaphore的现值增加一个定额。通常为1,并传回semaphore的前一个现值。

    Semaphore 常常被用来保护固定大小的环状缓冲区(ring buffer)。程序如果要读取环状缓冲区的内容,必须等待semaphore。

    线程将输入写入环状缓冲区,写入的数据可能不只一笔,在这种情况下,解除锁定时的Semaphore增额,应该等于写入的数据笔数。

  • 语法

    BOOL ReleaseSemaphore(
    	HANDLE hSemaphore,
        LONG lReleaseCount,
        LPLONG lpPreviousCount
    );
    
  • 参数

    • hSemaphore Semaphore 的handle。
    • lReleaseCount Semaphore现值的增额,该值不可以为0或者负数。
    • lpPreviousCount 借此传回semaphore原来的现值。
  • 返回值

    如果成功,传回TRUE,否则,传回FALSE。失败时,可以调用GetLastError()获得原因。

    ReleaseSemaphore()对于semaphore所造成的现值的增加,绝对不会超过CreateSemaphore()所指定的lMaximumCount

    请记住,lpPreviousCount所传回来的是一个瞬间值,你不可以把lReleaseCount加上*lpPreviousCount,就当作是semaphore的现值,因为其他线程可能改变了semaphore的值。

4. 线程池

4.1 概念

4.1.1 线程的执行流程

在这里插入图片描述

4.1.2 线程池

在这里插入图片描述

线程池是代表应用程序高效执行异步回调的工作线程集合。线程池主要用于减少应用程序线程数并提供工作线程的管理。应用程序可以对工作项进行排队,将工作与可等待句柄相关联、基于计时器自动排队,并使用I/O绑定。

1. 体系结构

以下应用程序可以从使用线程池中获益:

  • 高度并行且可以异步(调度大量小型工作项的应用程序,例如分布式索引或网络I/O)。
  • 创建和销毁大量线程的应用程序,每个线程运行时间很短。使用线程池可以减少线程管理的复杂性以及线程创建和销毁所涉及的开销。
  • 在后台和并行(处理独立工作项的应用程序,例如)加载多个选项卡。
  • 必须对内核对象执行独占等待,或对对象上的传入事件执行阻止的应用程序。使用线程池可以降低线程管理的复杂性,并通过减少上下文切换的数量来提高性能。
  • 一个应用程序,用于创建自定义服务员线程来等待事件。

原始线程池已在 Window Vista 中完全重新构建。新线程池已得到改进,因为它提供单个工作线程类型,(支持I/O和非 I/O),不使用计时器线程,提供单个计时器队列,并提供专用的永久性线程。它还提供清理组、更高的性能、每个进程独立计划的多个池,以及新的线程池 API。

线程池体系结构包括以下内容:

  • 执行回调函数的工作线程
  • 在多个等待句柄上等待的等待线程
  • 工作队列
  • 每个进程的默认线程池
  • 管理工作线程的工作器工厂

2. 最佳实践

与原始 线程池API 相比,新的 线程池API 提供了更大的灵活性和控制。但是,存在一些微妙但重要的差异。在原始API中,等待重置是自动的;在新API中,每次都必须显式重置等待。原始API自动处理模拟,将调用进程的安全上下文传输到线程。使用新API时,应用程序必须显式设置安全上下文。

下面是使用线程池时的最佳做法:

  • 进程的线程共享线程池。单个工作线程可以一次执行多个回调函数。这些工作线程由线程池管理。因此,不用通过在线程上调用 TerminateThread 或从回调函数调用 ExitThread来终止线程池中的线程。

  • I/O 请求可以在线程池中的任何线程上运行。取消线程池线程上的I/O需要同步,因为取消函数可能在与处理I/O请求的线程不同的线程上运行,这可能会导致取消未知操作。若要避免这种情况,请始终提供在调用CancelIoEx进行异步I/O时启动I/O请求时,使用的OVERLAPPED结构,或使用你自己的同步来确保在调用CancelSynchronousloCancelIoEx函数之前,无法在目标线程上启动其他I/O。

  • 在从函数返回之前,请清理回调函数中创建的所有资源。其中包括TLS、安全上下文、线程优先级和COM注册。回调函数还必须在返回之前还原线程状态。

  • 使等待句柄及其关联对象保持活动状态,直到线程池发出该句柄已完成的信号。

  • 标记所有正在等待长时间操作的线程(例如I/O刷新或资源清理),以便线程池可以分配新线程,而不是等待此线程。

  • 卸载使用线程池的DLL之前,请取消所有工作项、I/O、等待操作和计时器,并等待执行回调函数完成。

  • 通过消除工作项之间和回调之间的依赖关系、确保回调不会等待自身完成以及保留线程优先级来避免死锁。

  • 不要使用默认线程池与其他组件在进程中过快地将太多项排入队列。每个进程都有一个默认的线程池,包括Svchost.exe。默认情况下,每个线程池最多有500个工作线程。当处于就绪/正在运行状态的工作线程数必须小于处理器数时,线程池会尝试创建更多的工作线程。

  • 避免使用COM单线程单元模型,因为它与线程池不兼容。STA创建可能影响线程的下一个工作项的线程状态。STA通常生存期较长,并且具有线程相关性,这与线程池相反。

  • 创建新的线程池来控制线程优先级和隔离,创建自定义特征,并可能提高响应能力。但是,其他线程池需要更多系统资源(线程、内核内存)。池太多会增加CPU争用的可能性。

  • 如果可能,请使用可等待对象而不是基于APC的机制向线程池线程发出信号。APC与其他信号机制一样适用于线程池线程,因为系统控制线程池线程的生存期,因此可以在传递通知之前终止线程。

  • 使用线程池调试器扩展,!tp。此命令具有以下用法:

    • 池 地址标志
    • obj 地址标志
    • tqueue 地址标志
    • 服务员 地址
    • 辅助 角色地址

    对于池、服务员和辅助角色,如果地址为零,则命令将转储所有对象。对于服务员和辅助角色,省略地址会转储当前线程。定义了一下标志:0x1(单行输出)、0x2(转储成员)、以及0x4(转储池工作队列)。

4.2 线程池API

(API)的线程池应用程序编程接口使用基于对象的设计。以下每个对象都有用户模式数据结构表示:

  • 池对象是一组可用于执行工作的工作线程。每个进程可以根据需要创建具有不同特征的多个隔离池。每个进程还有一个默认池。
  • 清理组与一组回调生成对象相关联。函数存在以等待并释放作为每个清理组成员的所有对象。这样,应用程序就无需跟踪它创建的所有对象。
  • 将工作对象分配给池,并选择性地分配给清理组。可以发布它,导致池中的工作线程执行其回调。一个工作对象可以有多个未完成的职位:每个都会生成回调。由于缺少资源,发布操作不能失败。
  • 一个计时器对象控制回调的计划。计时器每次到时后,其回调都会发布到其辅助角色池。由于缺少资源,设置计时器不能失败。
  • wait对象导致等候线程,等候可等待句柄。满足等待条件或超时期限到期后,等待者线程会将等待对象的回调发布到等待的工作器池。由于缺少资源,设置等待不能失败。
  • I/O对象将文件句柄与线程池的I/O完成端口相关联。异步I/O操作完成后,工作线程将选取操作的状态并调用I/O对象的回调。

4.2.1 原始和当前线程池API

Feature原始API当前API
同步RegisterWaitForSingleObject
UnregisterWaitEx
CloseThreadpoolWait
CreateThreadpoolWait
SetThreadpoolWait
WaitForThreadpoolWaitCallbacks
工作QueueUserWorkItemCloseThreadpoolWork
CreateThreadpoolWork
SubmitThreadpoolWork
TrySubmitThreadpoolCallback
WaitForThreadpoolWorkCallbacks
TimerCreateTimerQueue
CreateTimerQueueTimer
ChangeTimerQueueTimer
DeleteTimerQueueTimer
DeleteTimerQueueEx
CloseThreadpoolTimer
CreateThreadpoolTimer
IsThreadpoolTimerSet
SetThreadpoolTimer
WaitForThreadpoolTimerCallbacks
I/OBindIoCompletionCallbackCancelThreadpoolIo
CloseThreadpoolIo
CreateThreadpoolIo
StartThreadpoolIo
WaitForThreadpoolIoCallbacks
清理组CloseThreadpoolCleanupGroup
CloseThreadpoolCleanupGroupMembers
CreateThreadpoolCleanupGroup
CloseThreadpool
CreateThreadpool
SetThreadpoolThreadMaximum
SetThreadpoolThreadMinimum
回调环境DestroyThreadpoolEnvironment
InitializeThreadpoolEnvironment
SetThreadpoolCallbackCleanupGroup
SetThreadpoolCallbackLibrary
SetThreadpoolCallbackPool
SetThreadpoolCallbackPriority
SetThreadpoolCallbackRunsLong
回调CallbackMayRunLong
回调清理取消关联CurrentThreadFromCallback
FreeLibraryWhenCallbackReturns
LeaveCriticalSectionWhenCallbackReturns
ReleaseMutexWhenCallbackReturns
ReleaseSemaphoreWhenCallbackReturns
SetEventWhenCallbackReturns

4.2.2 使用线程池函数

#include <windows.h>
#include <tchar.h>
#include <stdio.h>

//
// Thread pool wait callback function template
//
VOID
CALLBACK
MyWaitCallback(
    PTP_CALLBACK_INSTANCE Instance,
    PVOID                 Parameter,
    PTP_WAIT              Wait,
    TP_WAIT_RESULT        WaitResult
    )
{
    // Instance, Parameter, Wait, and WaitResult not used in this example.
    UNREFERENCED_PARAMETER(Instance);
    UNREFERENCED_PARAMETER(Parameter);
    UNREFERENCED_PARAMETER(Wait);
    UNREFERENCED_PARAMETER(WaitResult);

    //
    // Do something when the wait is over.
    //
    _tprintf(_T("MyWaitCallback: wait is over.\n"));
}


//
// Thread pool timer callback function template
//
VOID
CALLBACK
MyTimerCallback(
    PTP_CALLBACK_INSTANCE Instance,
    PVOID                 Parameter,
    PTP_TIMER             Timer
    )
{
    // Instance, Parameter, and Timer not used in this example.
    UNREFERENCED_PARAMETER(Instance);
    UNREFERENCED_PARAMETER(Parameter);
    UNREFERENCED_PARAMETER(Timer);

    //
    // Do something when the timer fires.
    //
    _tprintf(_T("MyTimerCallback: timer has fired.\n"));

}


//
// This is the thread pool work callback function.
//
VOID
CALLBACK
MyWorkCallback(
    PTP_CALLBACK_INSTANCE Instance,
    PVOID                 Parameter,
    PTP_WORK              Work
    )
{
    // Instance, Parameter, and Work not used in this example.
    UNREFERENCED_PARAMETER(Instance);
    UNREFERENCED_PARAMETER(Parameter);
    UNREFERENCED_PARAMETER(Work);

    BOOL bRet = FALSE;
 
    //
    // Do something when the work callback is invoked.
    //
     {
         _tprintf(_T("MyWorkCallback: Task performed.\n"));
     }

    return;
}

VOID
DemoCleanupPersistentWorkTimer()
{
    BOOL bRet = FALSE;
    PTP_WORK work = NULL;
    PTP_TIMER timer = NULL;
    PTP_POOL pool = NULL;
    PTP_WORK_CALLBACK workcallback = MyWorkCallback;
    PTP_TIMER_CALLBACK timercallback = MyTimerCallback;
    TP_CALLBACK_ENVIRON CallBackEnviron;
    PTP_CLEANUP_GROUP cleanupgroup = NULL;
    FILETIME FileDueTime;
    ULARGE_INTEGER ulDueTime;
    UINT rollback = 0;

    InitializeThreadpoolEnvironment(&CallBackEnviron);

    //
    // Create a custom, dedicated thread pool.
    //
    pool = CreateThreadpool(NULL);

    if (NULL == pool) {
        _tprintf(_T("CreateThreadpool failed. LastError: %u\n"),
                     GetLastError());
        goto main_cleanup;
    }

    rollback = 1; // pool creation succeeded

    //
    // The thread pool is made persistent simply by setting
    // both the minimum and maximum threads to 1.
    //
    SetThreadpoolThreadMaximum(pool, 1);

    bRet = SetThreadpoolThreadMinimum(pool, 1);

    if (FALSE == bRet) {
        _tprintf(_T("SetThreadpoolThreadMinimum failed. LastError: %u\n"),
                     GetLastError());
        goto main_cleanup;
    }

    //
    // Create a cleanup group for this thread pool.
    //
    cleanupgroup = CreateThreadpoolCleanupGroup();

    if (NULL == cleanupgroup) {
        _tprintf(_T("CreateThreadpoolCleanupGroup failed. LastError: %u\n"), 
                     GetLastError());
        goto main_cleanup; 
    }

    rollback = 2;  // Cleanup group creation succeeded

    //
    // Associate the callback environment with our thread pool.
    //
    SetThreadpoolCallbackPool(&CallBackEnviron, pool);

    //
    // Associate the cleanup group with our thread pool.
    // Objects created with the same callback environment
    // as the cleanup group become members of the cleanup group.
    //
    SetThreadpoolCallbackCleanupGroup(&CallBackEnviron,
                                      cleanupgroup,
                                      NULL);

    //
    // Create work with the callback environment.
    //
    work = CreateThreadpoolWork(workcallback,
                                NULL, 
                                &CallBackEnviron);

    if (NULL == work) {
        _tprintf(_T("CreateThreadpoolWork failed. LastError: %u\n"),
                     GetLastError());
        goto main_cleanup;
    }

    rollback = 3;  // Creation of work succeeded

    //
    // Submit the work to the pool. Because this was a pre-allocated
    // work item (using CreateThreadpoolWork), it is guaranteed to execute.
    //
    SubmitThreadpoolWork(work);


    //
    // Create a timer with the same callback environment.
    //
    timer = CreateThreadpoolTimer(timercallback,
                                  NULL,
                                  &CallBackEnviron);


    if (NULL == timer) {
        _tprintf(_T("CreateThreadpoolTimer failed. LastError: %u\n"),
                     GetLastError());
        goto main_cleanup;
    }

    rollback = 4;  // Timer creation succeeded

    //
    // Set the timer to fire in one second.
    //
    ulDueTime.QuadPart = (ULONGLONG) -(1 * 10 * 1000 * 1000);
    FileDueTime.dwHighDateTime = ulDueTime.HighPart;
    FileDueTime.dwLowDateTime  = ulDueTime.LowPart;

    SetThreadpoolTimer(timer,
                       &FileDueTime,
                       0,
                       0);

    //
    // Delay for the timer to be fired
    //
    Sleep(1500);

    //
    // Wait for all callbacks to finish.
    // CloseThreadpoolCleanupGroupMembers also releases objects
    // that are members of the cleanup group, so it is not necessary 
    // to call close functions on individual objects 
    // after calling CloseThreadpoolCleanupGroupMembers.
    //
    CloseThreadpoolCleanupGroupMembers(cleanupgroup,
                                       FALSE,
                                       NULL);

    //
    // Already cleaned up the work item with the
    // CloseThreadpoolCleanupGroupMembers, so set rollback to 2.
    //
    rollback = 2;
    goto main_cleanup;

main_cleanup:
    //
    // Clean up any individual pieces manually
    // Notice the fall-through structure of the switch.
    // Clean up in reverse order.
    //

    switch (rollback) {
        case 4:
        case 3:
            // Clean up the cleanup group members.
            CloseThreadpoolCleanupGroupMembers(cleanupgroup,
                FALSE, NULL);
        case 2:
            // Clean up the cleanup group.
            CloseThreadpoolCleanupGroup(cleanupgroup);

        case 1:
            // Clean up the pool.
            CloseThreadpool(pool);

        default:
            break;
    }

    return;
}

VOID
DemoNewRegisterWait()
{
    PTP_WAIT Wait = NULL;
    PTP_WAIT_CALLBACK waitcallback = MyWaitCallback;
    HANDLE hEvent = NULL;
    UINT i = 0;
    UINT rollback = 0;

    //
    // Create an auto-reset event.
    //
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

    if (NULL == hEvent) {
        // Error Handling
        return;
    }

    rollback = 1; // CreateEvent succeeded

    Wait = CreateThreadpoolWait(waitcallback,
                                NULL,
                                NULL);

    if(NULL == Wait) {
        _tprintf(_T("CreateThreadpoolWait failed. LastError: %u\n"),
                     GetLastError());
        goto new_wait_cleanup;
    }

    rollback = 2; // CreateThreadpoolWait succeeded

    //
    // Need to re-register the event with the wait object
    // each time before signaling the event to trigger the wait callback.
    //
    for (i = 0; i < 5; i ++) {
        SetThreadpoolWait(Wait,
                          hEvent,
                          NULL);

        SetEvent(hEvent);

        //
        // Delay for the waiter thread to act if necessary.
        //
        Sleep(500);

        //
        // Block here until the callback function is done executing.
        //

        WaitForThreadpoolWaitCallbacks(Wait, FALSE);
    }

new_wait_cleanup:
    switch (rollback) {
        case 2:
            // Unregister the wait by setting the event to NULL.
            SetThreadpoolWait(Wait, NULL, NULL);

            // Close the wait.
            CloseThreadpoolWait(Wait);

        case 1:
            // Close the event.
            CloseHandle(hEvent);

        default:
            break;
    }
    return;
}

int main( void)
{
    DemoNewRegisterWait();
    DemoCleanupPersistentWorkTimer();
    return 0;
}

此定义创建自定义线程池,创建工作项和线程池计时器,并将其与清理组相关联。池由一个永久性线程组成。它演示了以下线程池函数的用法:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值