一、上路吧,线程
从Win32角度看,进程含有内存(理论上可达2G)和资源,本身并不能够执行,只是提供一个安置内存和线程的地方。32位程序最多可有2G-128K内存,可通过设置达到3G。用线程而非进程主要原因是线程价廉,启动、退出快,共享系统资源。线程被中断(接收发来的interrupt信号)时,CPU获取其当前状态(即把所有寄存器内容拷贝到堆栈中,再把它从堆栈拷贝到一个CONTEXT结构中,要恢复时反过来,这整个过程便称为context switch。 race condition竞争条件 Atomic Operations。
二、线程的第一次接触
线程执行顺序是随机的,与创建顺序无关。Unix是line mode输出先被放到一个以一行一行为根据的缓冲区中,不会出现输出错乱的情况,针对有终端机的情况。程序结果输出到文件要比到屏幕上快得多。CreateThread返回的handle被称为一个核心对象(Kernel object),它和所谓的GDI对象如画笔、画刷或DC差不多,不过它是由KERNEL32.DLL管理,而非GDI32.DLL管理。GDI对象只有唯一的一个HANDLE,并只能被创建此对象的进程(或线程)所私有。而核心对象的HANDLE则可以有多个,甚至可以是跨进程的拥有这个对象,包括:进程Process;线程Thread;文件File;事件Event;信号量semaphore;互斥器mutex;管道pipe。为了保持对拥有者的跟踪,核心对象保持了一个引用计数Reference Count,记录多少HANDLE对应此对象;对象中记录了哪一个进程或者线程是这些Handle的拥有者。当调用CloseHandle函数时,计数就会-1,到0就会被释放。面对一个打开的对象,区分其拥有者是进程或是线程,是件很重要的事情。因为这会决定系统何时做清除善后(cleans up)操作。所谓 cleanup 操作,包括将该进程或线程所拥有的每一个对象的引用计数减 1。若有必要,则对象会被摧毁掉。程序员不能选择由进程或线程拥有对象,一切得视对象类型而定。由于引用计数的设计,对象有可能在“产生该对象之进程”结束之后还继续幸存,即不是创建但有了些对象的进程或线程使用了此对象。不可以依赖“因线程的结束而清理所有被这一线程产生的核心对象”。许多对象,例如文件,是被进程拥有,而非被线程拥有,在进程结束之前不能够清理它们。线程的 handle 是指向“线程核心对象”,而不是指向线程本身。“线程核心对象”引用到的那个线程也会令核心对象开启。因此,线程对象的默认引用计数是 2。当你调用 CloseHandle( )时,引用计数下降 1,当线程结束时,引用计数再降 1。只有当两件事情都发生了(不管顺序如何)的时候,这个对象才会被真正清除。由于被 CreateThread( )传回的那个 handle 属进程所有,而非线程所有,所以很可能有一个新产生的线程调用 CloseHandle( ),取代原来的线程。检查线程是否结束:GetExitCodeThread(Handle, LPDWORD lpExitCode),lpExitCode返回值:如还在运行返回STILL_ACTIVE,否则返回线程函数返回值(如果没有就返回0)。ExitThread(DWORD dwExitCode)中参数指定线程的结束代码,可被GetExitCodeThread捕获。主线程结束,子线性也被迫结束,且没有机会做清理工作,因此要等待子线程完成后主线程才能退出。主线程中调用ExitThread时,主线程会结束但子线性可以继续执行,但这样会跳过runtime library中的清理cleanup函数,因而没有将已开启的文件清理掉,不建议这样做。
三、快跑与等待
对死循环里重复执行(busy waits)直到某信号到达,系统也会同样执行抢先式任务方式,因为它不知道哪个任务是有用的,哪个是没有用的。可打开“性能监视器”,打开“添加”,然后在找到Process,在选定对象实例中找到自己的工程,然后添加,可看到自己工程占用CPU时间来确定是否有死循环。线程执行,系统知道何时执行完成,然后将消息发送给其他线程。将等待线程睡眠,WaitForSingleObject传入线程核心对象(正在执行的线程句柄)。设置其等待时间为0,可检测handle核心对象是否处于激活状态。当线程正在执行时,线程对象处于未激发状态,结束时被激发了,当核心对象被激发时,会导致WaitForSingleObject醒来。用cout输出可能会错乱用printf不会,cout是对象只在缓冲流中,直接endl,flush等才会输出,可能其他线程也在在这期间输出到缓冲区,而printf是函数,直接运行了。多个WaitForMultipleObjects,可等待handles数组中的(任)一个或全部对象。MsgWaitForMultipleObjects()在对象被激发或消息到达队列时被唤醒而返回,当表示“消息到达队列”时,返回值是WAIT_OBJECT_0+nCount,其中民Count是handles的个数,返回值:WAIT_ABANDONED:这是针对等待对象是互斥体的情况,当互斥体对象虽然没有被占用它的线程释放,但是占用它的线程已提前中止时,WaitForSingleObject 就返回此值。
四、同步控制
SendMessage()是同步行为,PostMessage是异步。Critical section不是核心对象,因此没有所谓的handle,这和核心对象不同,它存在于进程的内存空间中,不需要使用像Create这样的API获取一个handle,而是应声明一个CRITICAL_SECTION局部变量来初始化InitializeCriticalSection(),使用完全后用DeleteCriticalSection来清除它,这个函数并没有“释放对象”的意义在里头,和C++里的delete运算符不一样,EnterCriticalSection,LeaveCriticalSection,一旦线程进入一个critical section,它就能一再重复进入该critical section(即同一个线程可进入多个critical section),最小锁定时间,critical section中不应写Sleep,Wait...等函数,Critical section的缺点是没有办法获知进入critical section中的线程是生是死,由于它不是核心对象,如果进入后的线程结束或当掉了,而未调用Leave的话,系统没有办法将critical section清除。一种确保死锁不会发生的方法:强迫将资源锁定,使它们成为“all-or-nothing”。锁定一个未被拥有的mutex比critical section慢100倍,因此后者不需要进入操作系统核心,直接在用户态进行操作。等待一个mutex时可指定“结束等待”的时间长度,critical section不可。CreateMutex,OpenMutex,WaitForSingleObject,WaitForMultipleObjects,MsgWaitForMultipleObjects,ReleaseMutex,CloseHandle。mutex对整个系统而言是全局的,可根据其名称来使用(不是handle),mutext和其他核心对象一样,有一个引用计数,每次调用CloseHandle时引用计数便减1,当为0时mutex便自动被系统清除掉。如果已经产生了mutex,且命名了,那么任何其他进程、线程都可打开此mutex,如果调用CreateMutex并指定一个已经存在的名称,Win32会回给一个mutex handle,而不是产生一个新的mutex,GetLastError会传回ERROR_ALREADY_EXISTS,可用OpenMutex。创建Mutex时第二个参数bInitialOwner指定现行线程是否立即拥有创建出来的mutex,避免其他进程或线程通过wait与它产生race condition。 mutex是semaphore的一种退化(值为1时),mutex又常被称为binary semaphore,CreateSemaphore可以给出名称,semaphore不像mutex,没有拥有权的概念,且一个线程可以反复调用wait...()且可能后面阻塞,而拥有mutex的线程无论调用多少次wait...()也不会被阻塞,ReleaseSemaphore,与mutex不同,调用ReleaseSemaphore的那个线程不一定就是调用Wait...(),任何线程任何时间都可调用Release。 事件,它的状态完全精确可控,不像Mutex与Semaphore一样,状态因为诸如WaitForSingleObject之类的函数调用而变化,CreateEvent里bManualReset当为True时表示event在变成激发状态后(因而唤醒一个线程),自动重置为非激发状态,PulseEvent函数,其“要求苏醒”的请求并不会被储存起来,可能会遗失掉,如已经苏醒再让他苏醒,后面就不会再发苏醒信号,然后可能导致永远醒不来。 iterlocked函数有两个,InterlockedIncrement和InterlockedDecrement,它们只能和0比较,不能和其他数值比较,参数是32位变量地址,指向long word,指向的变量值经过运算(加1或减1)后,如果等于0返回0,大于0传回正数,小于返回负数。InterlockedExchange设定新值并传回旧值。Interlocked...()被使用于spin-lock。
区别总结:Critical Section用户态,速度快,不可跨线程,Mutex内核态,可跨进程、线程,本线程申请本线程释放,Semaphore与Mutex区别是本线程申请,可由其他线程释放,Event状态可控,通常用于overlapped I/O或用来设计某些自定义的同步对象,也可设置名称。除了临界区,其他都可在创建时指定名称。event可同时触发多个线程。
五、不要让线程成为脱缰野马
线束线程:1.TerminateThread比较的暴力的直接结束掉,线程没有机会在结束前清理自己,目标线程在核心层面就被根本抹杀,没有机会捕捉所谓的“结束请求”并从而获得清理自己的机会,且线程的堆栈没有被释放掉,可能导致内存泄漏,而且任何与此线程有附着关系的DLLs也没有机会获得“线程解除附着”的通知。唯一可以预期的是线程handle将变成激发状态,并传回指向返回码,且如果线程正进入一个critical section那将永远锁定,而不会像mutex一样有abandoned状态。结论是远离TerminateThread。2.Signals, C runtime library有signal函数。Win32的signals查利用异常情况模拟的,Win32并没有真正的signals。3.跨越线程,丢出异常,Win32中没有标准方法可以把一个异常丢到另一个线程,可以模拟但有缺陷。4.设立一个标记,
各个章节代码:
链接:https://pan.baidu.com/s/1fQZiwZzaah5KWK5cG5ggeA 密码:0u46
临界区,互斥量,信号量,事件的区别
临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。 互斥量比临界区复杂。因为使用互斥不仅仅能够在同 一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。总结:1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使 用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。 2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但 对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和 线程退出。 临界区域的一个缺点就是:Critical Section不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统无法获知,而且没有办法释放该临界资源。Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。只有临界区不是内核对象,其他三个都是。https://blog.csdn.net/wangweitingaabbcc/article/details/7445465