Win32多线程编程学习心得

时间:2022-09-29 16:25:11

博客原文地址:http://jerkwisdom.github.io/study/thread/thread-Summary/

此处博客不再更新。

为什么多线程?


多线程并不一定是最好的,合适才是最好的。

多线程主要的优点是价廉物美,启动快、退出快、与其他线程共享核心对象,很容易实现共产主义的伟大梦想。但是其又有不可预期、测试困难的缺点。

使用好多线程,就是要知道何时应该用多线程,何时不该用。如果应该用多线程,如何解决Race Condition问题?如何共享数据?如何提高效率?如何同步线程和数据?总结起来就是:

  • 有始有终,线程的创建和释放都要靠自己
  • 不抛弃不放弃,等一等线程,让它做完自己的工作
  • 文明有序,资源占用无冲突

但是有时候却不建议使用多线程:

  • 针对于慢速I/O设备,Overlapped I/O更能胜任
  • 程序的健壮性要求很高,值得付出比较多的额外负担,多进程可能更能胜任

操作线程


如何创建线程?

如果要写一个多线程程序,第一步就是创建一个线程,我们可以使用CreateThread API函数,也可以使用_beginthreadex C 函数,其实我大多数时候使用的是Boost库上面的boost::thread对象来创建线程对象。如果有兴趣可以看看Boost库,这里暂且不讨论Boost库thread。

如果使用上面两个函数,可以去msdn查看。使用上面两种函数创建线程,其线程函数都必须符合以下格式,当然函数名可以更换:

DWORD WINAPI ThreadFunc(LPVOID n);

使用CreateThread API函数或者_beginthreadex函数,可以传回两个值用以识别一个新的线程——返回值Handle(句柄)和输出参数lpThread(线程ID)。为了安全防护的缘故,不能根据一个线程的ID获得其handle。

如何释放线程?

线程和进程一样,都是核心对象。如何释放线程属于如何释放核心对象的问题。CloseHandle函数在这里起了十分重要的作用。CloseHandle函数的功能是将核心对象的引用计数减1。其不能直接用来释放核心对象,核心对象只有在其引用计数为0的时候会被操作系统自动销毁。

BOOL CloseHandle(HANDLE hObject);

如果你不调用该函数,即使线程在创建之后执行完毕,引用计数还是不为0,线程无法被销毁。如果一个进程没有在结束之前对它所打开的核心对象调用CloseHandle,操作系统会自动把那些对象的引用计数减一。虽然操作系统会做这个工作,但是他不知道核心对象实际的意义,也就不可能知道解构顺序是否重要。如果你在循环结构创建了核心对象而没有CloseHandle,好吧!你可能会有几十万个句柄没有关闭,你的系统会因此没有可用句柄,然后各种异常现象就出现了。记住当你完成你的工作,应该调用CloseHandle函数释放核心对象。

在清理线程产生的核心对象时也要注意这个问题。不要依赖因线程结束而清理所有被这一线程产生的核心对象。面对一个打开的对象,区分其拥有者是进程或是线程是很重要的。这决定了系统何时做清理工作。程序员不能选择有进程或者线程拥有对象,一切都得视对象类型而定。如果被线程打开的核心对象被进程拥有,线程结束是无法清理这些核心对象的。

线程核心对象与线程

其实这两个是不同的概念。CreateThread函数返回的句柄其实是指向线程核心对象,而不是直接指向线程本身。在创建一个新的线程时,线程本身会开启线程核心对象,引用计数加1,CreateThread函数返回一个线程核心对象句柄,引用计数再加1,所以线程核心对象一开始引用计数就是2。

调用CloseHandle函数,该线程核心对象引用计数减一,线程执行完成之后,引用计数再减一为零,该核心对象被自动销毁。

结束主线程

首先得了解哪个线程是主线程:程序启动后就执行的线程。主线程有两个特点:

  • 负责GUI主消息循环
  • 主线程结束时,强迫其他所有线程*结束,其他线程没有机会执行清理工作

第二个特点也就意味着,如果你不等待其他线程结束,它们没有机会执行完自己的操作,也没有机会做最后的cleanup操作。我遇到过由于没有等待,而出现程序奔溃的情况。反正很危险。

结束线程并获取其结束代码

这个没什么好说的,可以使用ExitThread函数退出线程,返回一个结束代码。GetExitCodeThread函数获取ExitThread函数或者return语句返回的结束代码。不过想通过GetExitCodeThread来等待线程结束是个很糟糕的注意——CPU被浪费了。下一节提及的WaitForSingleObject才是正道。

终止其他线程

终止其他线程可以使用TerminateThread()函数,也可以使用全局标记。

TerminateThread()函数的缺点是:
1、线程没有机会在结束前清理自己,其堆栈也没有被释放掉,出现内存泄露;
2、任何与此线程有附着关系的DLLs也没有机会获得线程解除附着通知;
3、线程进入的Critical Section将永远处于锁定状态(Mutex会返回wait_abandoned状态)。
4、线程正在处理的数据会处于不稳定状态。

TerminateThread()唯一可以预期的是:线程handle变成激发状态,并且传回dwExitCode所指定的结束代码。

设立全局标记的优点:保证目标线程在结束之前安全而一致的状态
设立全局标记的缺点:线程需要一个polling机制,时时检查标记值。(可以使用一个手动重置的event对象

等一等线程


等待一个线程的结束

使用WaitForSingleObject最显而易见的好处是你终于可以把以下代码精简成一句了。

for(;;)
{
int rc;
rc = GetExitCodeThread(hThread, &exitCode);
if(!rc && exitCode != STILL_ACTIVE)
break;
}
→ → → → → →
WaitForSingleObject(hThread, INFINITE);

其他好处就是:

  • busy loop浪费太多CPU时间
  • 可以设定等待时间

等待多个线程的结束

WaitForSingleObject函数不好同时判断多个线程的状态,WaitForMultipleObjects可以同时等待多个线程,可以设定是否等待所有线程执行结束还是只要一个线程执行完立马返回。

在GUI线程中等待

在GUI线程中总是要常常回到主消息循环,上述两个wait api函数会阻塞主消息循环。MsgWaitForMultipleObjects函数可以在对象呗激发或者消息到达时被唤醒而返回。

线程同步


线程同步主要有Critical Sections、Mutex、Semaphores、Event,除了Critical Section是存在于进程内存空间内,其他都是核心对象

Critical Sections

Critical Section用来实现排他性占有,适用范围时单一进程的各个线程之间。

使用示例:

CRITICAL_SECTION cs ; // here must be global attributes to related thread
InitializeCriticalSection (&cs );
EnterCriticalSection(&cs );
LeaveCriticalSection(&cs );
DeleteCriticalSection(&cs );

Critical Sections注意事项:

  • 一旦线程进入一个Critical Section,再调用LeaveCriticalSection函数之前,就能一直重复的进入该Critical Section。
  • 千万不要在一个Critical section之中调用Sleep()或者任何Wait... API函数。
  • 如果进入Critical section的那个线程结束了或者当掉了,而没有调用LeaveCriticalSection函数,系统就没有办法将该Critical Section清除。

Critical Section的优点:

  • 相对于Mutex来说,其速度很快。锁住一个未被拥有的mutex要比锁住一个未被拥有的critical section,需要花费几乎100倍时间。(critical section不需要进入操作系统核心)

Critical Section的缺陷:

  • Critical Section不是核心对象,无法WaitForSingleObject,没有办法解决死锁问题(一个著名的死锁问题:哲学家进餐问题)
  • Critical Section不可跨进程
  • 无法指定等待结束的时间长度
  • 不能够同时有一个Critical section被等待
  • 无法侦测是否已被某个线程放弃

Mutex

Mutex可以在不同的线程之间实现排他性战友,甚至即使那些线程属于不同进程。

使用示例:

HANDLE hMutex ; // global attributes
hMutex = CreateMutex (
NULL, // default event attributes
false, // default not initially owned
NULL // unnamed
);
DWORD dwWaitResult = WaitForSingleObject (hMutex , INFINITE );
if (dwWaitResult == WAIT_OBJECT_0 )
{
// wait succeed, do what you want
...
}
ReleaseMutex(hMutex );

示例解释:
1、HMutex在创建时为未被拥有未激发状态;
2、调用Wait...()函数,线程获得hMutex的拥有权,HMutex短暂变成激发状态,然后Wait...()函数返回,此时HMutex的状态是被拥有未激发
3、ReleaseMutex之后,HMutex的状态变为未被拥有未激发状态

Mutex注意事项:

  • Mutex的拥有权并非属于哪个产生它的哪个线程,而是那个最后对此mutex进行Wait...()操作并且尚未进行ReleaseMutex()操作的线程。
  • 如果线程拥有一个mutex而在结束前没有调用ReleaseMutex(),mutex不会被摧毁,取而代之,该mutex会被视为“未被拥有”以及“未被激发”,而下一个等待中的线程会被以WAIT_ABANDONED_0通知。
  • Wait...()函数在Mutex处于未被拥有未被激发状态时返回。
  • 将CreateMutex的第二个参数设为true,可以阻止race condition,否则调用CreateMutex的线程还未拥有Mutex,发生了context switch,就被别的线程拥有了。

Mutex优点

  • 核心对象,可以调用Wait...() API函数
  • 跨线程、跨进程、跨用户(将CreateMutex的第三个参数前加上"Global//")
  • 可以具名,可以被其他进程开启
  • 只能被拥有它的哪个线程释放

Mutex缺点

  • 等待代价比较大

Semaphores

Semaphore被用来追踪有限的资源。

和Mutex的对比

  • mutex是semaphore的退化,令semahpore的最大值为1,那就是一个mutex
  • semaphore没有拥有权的概念,也没有wait_abandoned状态,一个线程可以反复调用Wait...()函数以产生锁定,而拥有mutex的线程不论在调用多少次Wait...()函数也不会被阻塞。
  • 在许多系统中都有semaphore的概念,而mutex则不一定。
  • 调用ReleaseSemaphore()的那个线程并不一定是调用Wait...()的那个线程,任何线程都可以在任何时间调用ReleaseSemaphore,解除被任何线程锁定的Semaphore。

Semaphore优点

  • 核心对象
  • 可以具名,可以被其他进程开启
  • 可以被任何一个线程释放

Semaphore缺点

Event

Event通常用于overlapped I/O,或者用来设计某些自定义的同步对象。

使用示例:

HANDLE hEvent ; // global attributes
hEvent = CreateEvent (
NULL, // default event attributes
true, // mannual reset
false, // nonsignaled
NULL // unnamed
);

SetEvent(hEvent);
PulseEvent(hEvent);
DWORD dwWaitResult = WaitForSingleObject (hEvent , INFINITE );
ResetEvent(hEvent);
if (dwWaitResult == WAIT_OBJECT_0 )
{
// wait succeed, do what you want
...
ResetEvent(hEvent );
}

示例解释:
1、CreateEvent默认为非激发状态、手动重置
2、SetEvent把hEvent设为激发状态
3、在手动重置情况下(bManualReset=true),PulseEvent把event对象设为激发状态,然而唤醒所有等待中的线程,然后恢复为非激发状态;
4、在自动重置情况下(bManualReset=false),PulseEvent把event对象设为激发状态,然而唤醒一个等待中的线程,然后恢复为非激发状态;
5、ResetEvent将hEvent设为未激发状态

Event注意事项:

  • CreateEvent函数的第二个参数bManualReset若为false,event会在变成激发状态(因而唤醒一个线程)之后,自动重置为非激发状态;
  • CreateEvent函数的第二个参数bManualReset若为true,event会在变成激发状态(因而唤醒一个线程)之后,不会自动重置为非激发状态,必须要手动ResetEvent;

Event优点:

  • 核心对象
  • 其状态完全由程序来控制,其状态不会因为Wait...()函数的调用而改变。
  • 适用于设计新的同步对象
  • 可以具名,可以被其他进程开启

Event缺点:

  • 要求苏醒的请求并不会被存储起来,可能会遗失掉。如果一个AutoReset event对象调用SetEvent或PulseEvent,而此时并没有线程在等待,这个event会被遗失。如Wait...()函数还没来得及调用就发生了Context Switch,这个时候SetEvent,这个要求苏醒的请求会被遗失,然后调用Wait...()函数线程卡死。

替代多线程


Overlapped I/O

Win32之中三个基本的I/O函数:CreateFile()、ReadFile()和WriteFile()。

  • 设置CreateFile()函数的dwFlagsAndAttributes参数为FILE_FLAG_OVERLAPPED,那么对文件的每一个操作都将是Overlapped。此时可以同时读写文件的许多部分,没有目前的文件位置的概念,每一次读写都要包含其文件位置。
  • 如果发出许多Overlapped请求,那么执行顺序无法保证。
  • Overlapped I/O不能使用C Runtime Library中的stdio.h函数,只能使用ReadFile()和WriteFile()来执行I/O。

Overlapped I/O函数使用OVERLAPPED结构来识别每一个目前正在进行的Overlapped操作,同时在程序和操作系统之间提供了一个共享区域,参数可以在该区域双向传递。

多进程

如果一个进程死亡,系统中的其他进程还是可以继续执行。多进程程序的健壮性远胜于多线程。因为如果多个线程在同一个进程中运行,那么一个误入歧途的线程就可能把整个进程给毁了。

另一个使用多重进程的理由是,当一个程序从一个作业平台被移植到另一个作业平台,譬如Unix(不支持线程,但进程的产生与结束的代价并不昂贵),Unix应用程序往往使用多个进程,如果移植成为多线程模式,可能需要大改。

文献



欢迎访问我的个人博客click me
博客原文地址:Win32 MultiThread Study Summary - Let's Thread