多线程我之总结:
-
对于临界区(CritialSection)的计数,其计数最小被减为0,不会为负数;
-
线程A的计数可以被线程B减小;
-
Mutex遗弃条件的判断?初始,未设置owership的怎么判断
1.
第一个CreateThread
函数功能:创建线程
函数原型:
HANDLEWINAPICreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
函数说明:
第一个参数表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。
第二个参数表示线程栈空间大小。传入0表示使用默认大小(1MB)。
第三个参数表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
第四个参数是传给线程函数的参数。
第五个参数指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。
第六个参数将返回线程的ID号,传入NULL表示不需要返回该线程ID号。
函数返回值:
成功返回新线程的句柄,失败返回NULL。
2.
假设某个线程A在执行上面的代码,该线程在调用system()之后且尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。
为了解决这个问题,Windows操作系统提供了这样的一种解决方案——每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的。下面列出_beginthreadex()函数的源代码(我在这份代码中增加了一些注释)以便读者更好的理解_beginthreadex()函数与CreateThread()函数的区别。
由上面的源代码可知,_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。相信阅读到这里时,你会对这句简短的话有个非常深刻的印象,如果有面试官问起,你也可以流畅准确的回答了^_^。
3.
临界区(CriticalSection) CRITICAL_SECTION
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
(1) 临界区共初始化、销毁、进入和离开临界区四个函数。
(2) 临界区可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。
(3) 推荐临界区与旋转锁配合使用。
4. 事件
最后总结下事件Event
1) 事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2) 事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
3) 事件可以解决线程间同步问题,因此也能解决互斥问题。
4) HANDLE g_hThreadEvent;//申明事件
g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);//创建一个事件,(安全性,是否手
动,是否触发,事件名称)NULL表示匿名事件
SetEvent(g_hThreadEvent);//触发事件
WaitForSingleObject(g_hThreadEvent,INFINITE);//等待事件,取得事件后调用ResetEvent
RsetEvent(g_hThreadEvent);//将事件设置为未触发
5. Mutex
最后总结下互斥量Mutex:
1) 互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
2) 互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。
3) 互斥量也是有“线程拥有权”概念的。“线程拥有权”在关键段中有详细的说明,这里就不再赘述了。另外由于互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。
6. 信号量
首先也来看看如何使用信号量,信号量Semaphore常用有三个函数,使用很方便。下面是这几个函数的原型和使用说明。
第一个CreateSemaphore
函数功能:创建信号量
函数原型:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTESlpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数表示初始资源数量。
第三个参数表示最大并发数量。
第四个参数表示信号量的名称,传入NULL表示匿名信号量。
第二个OpenSemaphore
函数功能:打开信号量
函数原型:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
函数说明:
第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示信号量句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。
第三个ReleaseSemaphore
函数功能:递增信号量的当前资源计数
函数原型:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
函数说明:
第一个参数是信号量的句柄。
第二个参数表示增加个数,必须大于0且不超过最大资源数量。
第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。
注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
7. 生产者消费者问题
生产者消费者问题已经圆满的解决了,下面作个总结:
1.首先要考虑生产者与消费者对缓冲区操作时的互斥。
2.不管生产者与消费者有多少个,缓冲池有多少个缓冲区。都只有二个同步过程——分别是生产者要等待有空缓冲区才能投放产品,消费者要等待有非空缓冲区才能去取产品。
8. 读写锁 SRWLock
申明读写锁
SRWLOCK g_srwLock;
函数功能:初始化读写锁
VOID InitializeSRWLock(PSRWLOCK SRWLock);
函数功能:写入者线程申请写资源。
函数原型:VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);
函数功能:写入者线程写资源完毕,释放对资源的占用。
函数原型:VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
函数功能:读取者线程申请读资源。
函数原型:VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
函数功能:读取者线程结束读取资源,释放对资源的占用。
函数原型:VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
最后总结一下读写锁SRWLock
1.读写锁声明后要初始化,但不用销毁,系统会自动清理读写锁。
2.读取者和写入者分别调用不同的申请函数和释放函数。
注意一个线程仅能锁定资源一次,不能多次锁定资源。
(没有删除或销毁SRWLOCK的函数,系统会自动清理)
9. “遗弃“问题
由本文所做的试验可知,互斥量能够处理“遗弃”情况,事件与信号量都无法解决这一情况。
再思考下互斥量能处理“遗弃”问题的原因,其实正是因为它有“线程所有权”概念。在系统中一旦有线程结束后,系统会判断是否有互斥量被这个线程占有,如果有,系统会将这互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这表示该互斥量已经不为任何线程占用,处于触发状态。其它等待这个互斥量的线程就能顺利执行下去了。至于线程如何获取互斥量的“线程所有权”,MSDN上介绍为——A threadobtainsownership of a mutex either by creating it with the bInitialOwnerparameter set to TRUE or by specifying itshandle in a call toone of the waitfunctions.