要点:将互斥量 + 条件变量/事件对象封装为了一个类CMutexLock,该类同时支持windows和linux下的互斥量同步。对熟悉windows和linux下面的多线程开发很有帮助。下面的代码可以直接在VS2008,2010中编译通过,linux下也是没问题的。如果你觉得写得不错也可以直接移植到你的代码库中,比较适合多线程中采用生产者-消费者这样的场景,比如子线程读取和解析文件生产给其它线程用,网络接收子线程接收处理数据给其它线程使用等。
强烈建议有需要的跑起来调试下(本项目类型是VS win32项目cpp中需要#include "stdafx.h",其它类型项目或者linux下简单修改下即可),代码注释中有详细的说明。如果你觉得有更好的做法,或者那里注释错了,非常欢迎你的回复。本代码也是参考了很多网上的资料,感谢参考文章中的各位作者。
工程设置:
1.pthread多线程库使用:
pthread库源代码:ftp://sources.redhat.com/pub/pthreads-win32/多线程代码:
例子,具体用法和分析见代码注释:
MutexLock.h
#ifndef _MUTEXLOCK_H_ #define _MUTEXLOCK_H_ #ifdef WIN32 //#define WINOS // 这里可以注释掉就是linux形式的互斥量 #else #undef WINOS #endif #ifdef WINOS #include <Windows.h> #include <process.h> #else #include <pthread.h> //来自ftp://sources.redhat.com/pub/pthreads-win32/ /* 获取互斥量属性对象在进程间共享与否的标志 int pthread_mutexattr_getpshared (__const pthread_mutexattr_t *__restrict __attr, \ int *__restrict __pshared); 设置互斥量属性对象,标识在进程间共享与否 int pthread_mutexattr_setpshared (pthread_mutexattr_t *__attr, int __pshared); 底层库都像一个状态机,包括socket,pthread,directx,opengl,openal... 这是对外统一接口,松耦合,但是又可能需要调整内部的参数,那么必须采用这样的方式,不论编写的语言是基于过程还是基于对象的。 */ /*参考文章:http://www.ibm.com/developerworks/cn/linux/l-cn-mthreadps/ http://blog.csdn.net/anonymalias/article/details/9093733 https://software.intel.com/zh-cn/blogs/2011/03/24/linux-windows */ #endif class CMutexLock { public: CMutexLock(); ~CMutexLock(); void init(); void release(); void lock(); void unlock();// 设计的时候,不要unwaite放置到unlock里面去,否则会导致职责不分明,如果有内部控制的还会导致无法唤醒。 void waite();// 当获取不到数据,那么waite挂起线程,等待其它线程通知释放 void unwaite();// 生产了数据那么需要调用unwaite. private: #ifdef WINOS HANDLE m_mutex; HANDLE m_event;//事件如果有信号那么可以正常执行,如果无信号那么只能等待 #else pthread_mutex_t m_mutex; pthread_cond_t m_condition; pthread_mutexattr_t m_mutexAttr; #endif }; #endif
#include "stdafx.h" #include "MutexLock.h" /*参考文章: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682411%28v=vs.85%29.aspx http://blog.csdn.net/anonymalias/article/details/9080881 http://blog.csdn.net/anonymalias/article/details/9174403 */ CMutexLock::CMutexLock() { #ifndef WINOS m_mutex = NULL; m_condition = NULL; #else m_mutex = NULL; m_event = NULL; #endif } CMutexLock::~CMutexLock() { release(); } void CMutexLock::release() { #ifdef WINOS CloseHandle(m_mutex);//所有内核对象,或者用其它方式创建的,都可以用closeHandle将引用计数减1。 m_mutex = NULL; #else pthread_mutexattr_destroy(&m_mutexAttr); pthread_mutex_destroy(&m_mutex); pthread_cond_destroy(&m_condition); m_mutex = NULL; m_condition = NULL; #endif } void CMutexLock::init() { #ifdef WINOS // arg1 是NULL,互斥量用默认的安全描述信息,这个时候子进程不能继承该互斥量. // arg2 是当前指明互斥量指向的线程为空,且被引用的次数是0,没有线程/进程拥有该互斥量;否则当前线程拥有该互斥量。 // arg3 互斥量的名字 m_mutex = CreateMutex(NULL, FALSE, NULL); DWORD dwLastError = GetLastError(); if( dwLastError == ERROR_ALREADY_EXISTS) { CloseHandle(m_mutex); m_mutex = NULL; } //事件是有信号线程不阻塞,无信号阻塞线程睡眠。 // arg1是事件属性。 // arg2是手动还是自动调用ResetEvent将事件设置为无信号,SetEvent是将事件设置为有信号 // ResetEvent是否手动设置为无信号,WaitForSingleObject后如果是自动方式那么会自动调用ResetEvent将事件设置为无信号。 // arg3是初始状态信号,一般设置为FALSE无信号,让线程挂起阻塞。 // arg4是线程的名字。 m_event = CreateEvent(NULL, FALSE, FALSE, NULL); #else // arg1是初始化的互斥量,arg2是pthread_mutexattr_t属性指针,如果是NULL,那么没有线程拥有该初始化好的互斥量。 int nResult = pthread_mutex_init(&m_mutex, NULL); if(nResult == 0) { printf("pthread_mutex_init result OK.\n"); } else { printf("pthread_mutex_init result error:%d\n", nResult); } pthread_mutexattr_init(&m_mutexAttr); // 设置 recursive 属性,使得linux下可以递归加锁,避免递归加锁死锁。 pthread_mutexattr_settype(&m_mutexAttr,PTHREAD_MUTEX_RECURSIVE_NP); pthread_mutex_init(&m_mutex, &m_mutexAttr); pthread_cond_init(&m_condition, NULL); #endif } void CMutexLock::lock() { #ifdef WINOS // arg2是等待毫秒时间,INFINITE是永远等待,直到该内核对象被触发可用;该函数是一个异步调用函数,互斥量拥有线程id非0, // 那么该函数将被挂起阻塞,释放当前CPU拥有权,当被其它线程释放互斥量拥有线程id为0,将会唤醒当前阻塞的线程重新获取互斥量。 WaitForSingleObject(m_mutex, INFINITE); /*if(WaiteforSingleObject(m_hMutex, dwMilliSec) == WAIT_OBJECT_0) { return true; } return false; */ #else // 锁定互斥锁,如果该互斥锁被其它线程拥有,那么将被挂起阻塞,指定可用才回调返回; // 线程自己多次锁定将会导致死锁;两个线程需要多个互斥锁相互等待对方的互斥锁,也会导致死锁。 pthread_mutex_lock(&m_mutex); #endif } void CMutexLock::waite() { #ifdef WINOS WaitForSingleObject(m_event, INFINITE);// 等待的事件,和时间 #else //会自动调用pthread_mutex_unlock(&m_mutex)释放互斥量,将当前线程挂起阻塞,等待对方线程pthread_cond_signal通知唤醒, // 唤醒后pthread_cond_wait会调用pthread_mutex_lock重新锁定互斥量。 // pthread_cond_timedwait是阻塞一段时间。 pthread_cond_wait(&m_condition, &m_mutex); #endif } void CMutexLock::unwaite() { #ifdef WINOS SetEvent(m_event);//设置为有信号,唤醒等待事件挂起的线程。 #else pthread_cond_signal(&m_condition);//pthread_cond_broadcast(pthread_cond_t * cond)唤醒在条件上等待的所有线程。 #endif } void CMutexLock::unlock() { #ifdef WINOS ReleaseMutex(m_mutex);// 将互斥量释放,会通知到WaitForSingleObject. #else pthread_mutex_unlock(&m_mutex); #endif }main.cpp
/*多线程的一些总结: 一、互斥量是拥有线程ID的,如果互斥量没有线程ID那么当前线程可以获得互斥量,互斥量函数非阻塞;否则互斥量函数将阻塞当前线程。 linux下条件变量初始化时是没有绑定互斥量的(无信号的),只要waite都会释放当前互斥量,阻塞当前线程,直到有signal发送过来才会唤醒。 window下的事件对象,事件对象无信号情况下会阻塞当前线程,通过SetEvent(m_event)可以触发事件(signal),让当前阻塞的线程唤醒。 二、采用等待机制等有效的提高程序的CPU利用率,注意等待时需要先释放所用拥有的锁(尽管之前释放过,),否则会导致死锁。 s_mutexFileContent.unlock();// 不加这句linux的pthread库会导致死锁,linux不能递归加锁否则会导致死锁,windows下却可以。 s_mutexRequent.unlock(); // 不加这句,windows下的WaitForSingleObject不会先释放互斥量锁,也会导致死锁。 //s_mutexRequent.waite(); 三、pthread.h库下默认创建的线程是可结合的,每个可结合线程都应该要么被显示地回收,即调用pthread_join; 要么通过调用pthread_detach函数分离子线程,子线程被分离后不能再结合了。 pthread_detach(s_loadingThread); 四、window下的WaitForSingleObject线程未运行时候是未触发的,当线程运行完那么是触发的,所以可以等到到线程。 //返回WAIT_OBJECT_0在指定时间内等到到,WAIT_TIMEOUT超时,WAIT_FAILED有错误。 HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL); WaitForSingleObject(handle, INFINITE); 五、windows下众多多线程函数的选择;_beginthread是_beginthreadex的子集参数受限制,释放会有差异,所以用_beginthreadex即可。 _beginthreadex内部会调用CreateThread(),会给C运行库函数开辟堆资源,所以要用_endthreadex和CloseHandle来避免内存泄露。 CreateThread()没有开辟堆资源,所以在C运行库中可能导致多线程数据异常风险,但是在Win32/MFC C++运行库中可以放心使用。 AfxBeginThread()是MFC中的多线程,分工作线程无消息循环,界面线程有消息循环,可以让当前线程创建,挂起,唤醒,终止。 windows下线程常用函数:DWORD SuspendThread(HANDLE hThread);DWORD ResumeThread(HANDLE hThread);BOOL SetThreadPriority(HANDLE hThread,int nPriority); VOID ExitThread(DWORD dwExitCode); BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode); 一般线程的挂起/唤醒都通过同步对象来实现。 如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。 window下的多线程底层都是对CreateThread的封装。 如果在除主线程之外的任何线程中进行一下操作,你就应该使用多线程版本的C runtime library,并使用_beginthreadex和_endthreadex,CloseHandle: 1 使用malloc()和free(),或是new和delete 2 使用stdio.h或io.h里面声明的任何函数 3 使用浮点变量或浮点运算函数 4 调用任何一个使用了静态缓冲区的runtime函数,比如:asctime(),strtok()或rand() 六、linux和window下互斥量和条件变量的区别 1.linux连续上锁会死锁,可以用 pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP); 解决。windows连续上锁不会死锁。 2.SetEvent(m_event)后等待事件对象一直是有signal的,后面waite的不会阻塞;linux下pthread_cond_signal不会一直有信号,后面waite的将会阻塞。 3.pthread_cond_wait()后不需要重新加锁,WaitForSingleObject/SignalObjectAndWait后需要重新加锁。 4.linux的pthread_cond_timedwait等待的是绝对时间1970-01-01 00:00:00 开始的时间,window的WaitForSingleObject是一个从当前开始的相对时间。 5.linux的线程释放,非游离的线程需要主线程调用pthread_join等待子线程结束并释放子线程的栈寄存器,游离的线程需要设置为属性为游离, 或者创建后用pthread_detach设置,子线程结束时候系统会回收子线程的资源。这样才能避免内存泄露。 windows下的释放:_beginthreadex创建的要调用_endthreadex和CloseHandle,其它方式创建的线程ExitThread或CloseHanle即可。 */ #include "stdafx.h" #include "MutexLock.h" #include <iostream> #include <deque> #include <string> using namespace std; #include <windows.h> // 异步线程 #ifdef WINOS static HANDLE s_loadingThread = NULL; #else static pthread_t s_loadingThread; #endif // 异步读取文件的互斥量 static CMutexLock s_mutexRequent; // 异步渲染的互斥量 static CMutexLock s_mutexFileContent; // 根对象,派生Node class Object { public: Object() { m_dObjID = 0; } protected: double m_dObjID; }; // 异步加载后的回调函数 typedef void (* ObjectCallBack)(Object* ); typedef struct tagAsyncFileData { bool m_bLoadOK; Object *m_pTarget; ObjectCallBack m_pCallback; string strFilePath; tagAsyncFileData() { m_bLoadOK = false; m_pTarget = NULL; m_pCallback = NULL; strFilePath.clear(); } }AsyncFileData; static deque<AsyncFileData*> s_dataFileRequent; typedef struct tagFileBufferData { AsyncFileData *m_pAsyncFileData; char *m_pBuffer; tagFileBufferData() { m_pAsyncFileData = NULL; m_pBuffer = NULL; } }AsyncFileBufferData; static deque<AsyncFileBufferData*> s_dataFileContent; #ifdef WINOS unsigned __stdcall AsyncLoad(void *pParameter) #else static void* AsyncLoad(void *pParameter) #endif { while(1) { AsyncFileData *pFileData = NULL; s_mutexRequent.lock(); if(s_dataFileRequent.empty()) { // 如果没有数据过来那么释放当前的锁,挂起CPU等待 printf("子线程,因没有请求的文件而等待!\n"); s_mutexFileContent.unlock();// 不加这句linux的pthread库会导致死锁。 s_mutexRequent.unlock(); // 不加这句,windows下的WaitForSingleObject不会先释放互斥量锁,也会导致死锁。 s_mutexRequent.waite(); continue; } else { pFileData = s_dataFileRequent.front(); s_dataFileRequent.pop_front(); } s_mutexRequent.unlock(); // 得到数据处理 if(pFileData != NULL) { // 异步加载数据,此次mmap还是fread方式略去,直接设置加载OK //fopen(pFileData->strFilePath.c_str(), "rb"); Sleep(1000); pFileData->m_bLoadOK = true; //pFileData.m_pTarget AsyncFileBufferData *pAsyncBuffer = new AsyncFileBufferData; pAsyncBuffer->m_pAsyncFileData = pFileData; char *pContent = "data from pFileData's strFilePath..."; int nContenLen = strlen(pContent) + 1; pAsyncBuffer->m_pBuffer = new char[nContenLen]; strcpy_s(pAsyncBuffer->m_pBuffer, nContenLen, pContent); printf("子线程 读取文件: %s\n", pAsyncBuffer->m_pAsyncFileData->strFilePath.c_str()); // 异步处理锁 s_mutexFileContent.lock(); // 解析好的数据放置进来 s_dataFileContent.push_back(pAsyncBuffer); s_mutexFileContent.unlock(); s_mutexFileContent.unwaite(); } } #ifdef WINOS _endthreadex( 0 );// 释放_beginthreadex分配的堆资源,且还要用CloseHandle释放 return 0; #endif } int main(int argc, char* argv[]) { s_mutexRequent.init(); s_mutexFileContent.init(); #ifdef WINOS unsigned int uiThreadID; s_loadingThread = (HANDLE)_beginthreadex(NULL, 0, AsyncLoad, NULL, CREATE_SUSPENDED, &uiThreadID); /*_CRTIMP uintptr_t __cdecl _beginthreadex(_In_opt_ void * _Security, _In_ unsigned _StackSize, _In_ unsigned (__stdcall * _StartAddress) (void *), _In_opt_ void * _ArgList, _In_ unsigned _InitFlag, _In_opt_ unsigned * _ThrdAddr);*/ if(s_loadingThread == NULL) { printf("pthread_create error!"); return 0; } ResumeThread(s_loadingThread); #else if( pthread_create(&s_loadingThread, NULL, AsyncLoad, NULL) != 0) { printf("pthread_create error!"); return 0; } pthread_detach(s_loadingThread); #endif // 在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死。 // 在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的, // 它的存储器资源在它终止时由系统自动释放。默认情况下,线程被创建成可结合的。为了避免存储器泄漏, // 每个可结合线程都应该要么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。 // pthread_detach(s_loadingThread);分离,运行结束后子线程会自动释放自己资源,不需要pthread_join也可以完全释放资源。 // void* ret = NULL; // pthread_join(_subThreadInstance, &ret);主线程一直等待直到等待的线程结束自己才结束,主线程可以清理其它线程的栈寄存器。 // pthread_self()获取自身线程的id. // 线程的被动结束分为两种,一种是异步终结,另外一种是同步终结。异步终结就是当其他线程调用 pthread_cancel的时候, // 线程就立刻被结束。而同步终结则不会立刻终结,它会继续运行,直到到达下一个结束点(cancellation point)。 // 当一个线程被按照默认的创建方式创建,那么它的属性是同步终结。 static int fileCount = 0; while(1) { s_mutexRequent.lock(); AsyncFileData* m_pFileData = new AsyncFileData(); m_pFileData->m_bLoadOK = false; m_pFileData->m_pCallback = NULL; m_pFileData->m_pTarget = NULL; fileCount++; char szFileBuffer[256]; sprintf_s(szFileBuffer,"文件名 %d.", fileCount); m_pFileData->strFilePath = szFileBuffer; printf("主线程,请求读取文件: %s\n", m_pFileData->strFilePath.c_str()); s_dataFileRequent.push_back(m_pFileData); s_mutexRequent.unlock(); s_mutexRequent.unwaite(); // 其它逻辑 Sleep(1000); while(1) { AsyncFileBufferData *pAsyncBuffer = NULL; s_mutexFileContent.lock(); if(s_dataFileContent.empty()) { printf("主线程,因没有解析好的数据等待!\n"); s_mutexRequent.unlock();// 请求锁需要释放,否则会导致问题 s_mutexFileContent.unlock(); s_mutexFileContent.waite();// 读取线程还没解析好等待 continue; } pAsyncBuffer = s_dataFileContent.front(); s_dataFileContent.pop_front(); s_mutexFileContent.unlock(); if(pAsyncBuffer != NULL) { printf("主线程,得到读取线程解析后的文件:%s, 数据: %s\n", pAsyncBuffer->m_pAsyncFileData->strFilePath.c_str(), pAsyncBuffer->m_pBuffer); delete pAsyncBuffer->m_pAsyncFileData; delete [] pAsyncBuffer->m_pBuffer; delete pAsyncBuffer; pAsyncBuffer = NULL; // 其它逻辑 Sleep(1000); break; } }// end while 2 } // end while 1 s_mutexRequent.release(); s_mutexFileContent.release(); #ifdef WINOS CloseHandle(s_loadingThread); #else // 设置了pthread_detach(s_loadingThread),退出时会自动释放, // 否则需要pthread_join()等待可结合的线程终止被释放它的栈寄存器资源. #endif return 0; }