多线程笔记2

发信站: 饮水思源 (2004年06月11日06:54:38 星期五)

多线程

闭门造车,大家指正

1.建立多线程

1.1 C runtime library 与 多线程 

C runtime library 诞生在上世纪70年代。那会多任务还是个新奇的东西,就是压根没想
到为以后的多线程考虑罗。
没有支持多线程当然叫单线程版罗。

支持多线程是个很泛的概念,支持多线程需要要干些什么事呢?

C runtime library 里有些全局变量,静态变量。race condition,对的,会有同步问题
,但绝对不止是同步这么简单,
仔细推敲一下,这些变量就应该每个线程各持一份然后老死不相往来么。

有哪些变量呢?比方说errno...所以你不能在线程中用fopen/fclose fwrite/fread new/
malloc free/delete..

但你把这些都换成CreateFile/ReadFile/WriteFile HeapAlloc/HeapFree

这些个WIN API函数可是可以的,没什么道理好讲,是操作系统的函数。

所以你用C runtime library的单线版就得,

所以说C runtime library单线程版并非就不能用到多线程上。

不过这堆api里也没有一个可替代Stream I/O的,自己写一个?(笑)。

臭规矩是多!有没有别的办法?我把那些个该死的全局变量,静态变量给每个线程留个副
本不就行拉。

实际上ms就是把这些个C runtime library的全局变量,静态变量放到一个叫_tiddata的结
构里边,当然也不光是这些变量

在线程建立的时候每个线程分到一个实例(CreateThread可干不了这事,后面会说到)


好现在每个线程都有这么一个结构了。

但跑fopen/flose等敏感函数要跟那些全局变量,静态变量打交道时,

C runtime library怎么知道你每个线程的那个放全局变量,静态变量的结构实例在什么地
方,

还有编译器怎么知道要连接多线程版还是单线程版

看看编译是的命令行可以看出单线程版是/ML 多线程版是/MT 或是/MD

(用vc的朋友可在setting->c/c++->code generation->use run-time libary中切换)

用个#defined #else #endif区别是单线程版还是多线程版,是单线程版把那个进程共享
的全局变量,静态变量

声明出来么,就像: extern int errno;

多线程版的话就用个函数返回个指向本线程(线程内共享)errno的指针出来:

extern int* __cdcel _errno(void);
#define errno (*__errno())

这就是多线程版的C runtime library。

最后一个问题,每个线程发配一个包含结构的内存块,要用的时候随便从内存块中的变量
set/get。怎么个实现的拉...


TLS(线程本地存储器)就是用来实现这么个东东的,

每个线程都有那么一份TLS(这个提法不好),有1000个槽(win2000),每个槽四个字节,
正好放个指针呃

刚刚说到每个线程不是有个数据结构的实例(_tiddata)么,它的指针就放在TLS中,

不过这些槽可不是随便能放的,得向进程申请,调用 DWORD TlsAlloc(),返回的那个DWOR
D就是可放的槽号拉,

用BOOL TlsSetVale(DWORD,LPVOID) / LPVOID TlsGetValue(DWORD) get/set指针。


加载dll时,如果dll有全局变量,静态变量,

你的dll不大可能只有一个线程加载吧,那些个全局变量,静态变量可是每个线程要一份的


每个线程需要一个指向这块内存区的指针,他们占用的槽号是一样的.


让进程调用DWORD TlsAlloc()/TlsFree() 来分配,管理这些槽。(在dll attach/detach
的时候)



每个线程可以用多个槽,不过占用的槽越少越好,毕竟整个进程就能分配出来1000个,nt
下只有64个。very的宝贵啊

好在这些个问题ms都用封装的api搞得服服帖帖,是不是很faint,好像全是废话

1.2 开线程了

开线程可用CreateThread/_beginthread/_beginthreadex/AfxBeginThread

/*CreateThread*/
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier
);
无视C runtime library的存在,就是没有这么个包含全局变量,静态变量的结构实例分配
出来。

当然你可用压根不用C runtime library中那些“敏感的”函数,或自己处理TLS。呵呵


/*_beginthread && _beginthreadex*/
unsigned long _beginthread( void( __cdecl *start_address )( void * ), unsigned
stack_size, void *arglist )

unsigned long _beginthreadex( void *security, unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ), void *arglist,
unsigned initflag, unsigned *thrdaddr );

有关心C runtime library,其实它也是调用的CreateThread,

辅助api函数threadstartex(LPVOID)的指针作为lpStartAddress送到CreateThread,
start_address,arglist也放到_tiddata里作为lpParameter送到CreateThread

threadstartex(LPVOID)作为thread function跑起来

看到没有,这个函数的LPVOID吃进分配出来的包含arglist和start_address的_tiddata,
_tiddata的指针放进TLS中,

把start_address作为一个普通函数调用(这个提法不好),arglist作为参数.

总算把C runtime library搞定拉。

但是_beginthread还是不能用,它有个race condition的隐患,

个人揣度ms是想封装CloseHanle(不要windows.h了?方便移植?),反正这个函数公认的蠢
拉,

_beginthreadex没有这个问题,不过自己CloseHanle。能在启动时停下来检查handle,再接
着跑。

1.2 关线程了

既然CreateThread/_beginthread不在考虑之列了,配套的ExitThread/_endthread,也用
不着
_endthreadex与ExitThread/_endthread一样是自杀式函数,线程直接返回,刚返回时_ti
ddata没放掉。

跟CreateThread/_beginthread/_beginthreadex的过程相反

_endthreadex与_endthread都是释放掉_tiddata的内存再调用ExitThread

说到这插一句,你要是在主线程调用ExitThread,其他线程的资源是放不出来的.可能操作
系统会有办法,不过还是不用的好

比方你一个tcp server crash掉的时候,本地端口会进入TIME_WAIT状态,如果server程序重
开时不设定端口SO_REUSEADDR属性

就开不动服务器了.

还有种情况,从message窗口看明明主线程退出了,某个线程要是在WaitForSingleObject()
是退不出来得.

_endthreadex还是用得很少的,除非程序立马会crash。

我们经常需要(绝大多数情况)在另一个线程放个信号出来告诉计算机,你该关某某线程


1.有很多人喜欢用变量,而且不怎么喜欢加volatile,呵呵
而且这个东西有很多弊端,但也有用得着的地方。但前提是不能把这东西做成轮询,

该阻塞的地方就得阻塞住(这个提法不好)。
什么地方用后面会提到。
2.setevent+WaitFor....
注意这里的event可要个会手动重置的,要不消息可能丢失

//module.h
#ifndef _MODULE_
#define _MODULE_
#define RET_ABNORMAL 1L
#define RET_SUCCESS 0L
#define ERR_CREATE_EXITEVENT -1
#define ERR_CREATE_THREAD -2
class PlayerModule
{
public:
PlayerModule();
virtual ~PlayerModule();
//开启线程,注意返回值
int StartThread();
//关闭线程
void StopThread();
//测试是不是有线程attach
bool IsStart() {return NULL == hModuleThread;}
protected:
// 主线程函数
virtual void Run();
HANDLE hExitEvent; //用来给退出信号的句柄
private:
//注意这里的static UINT WINAPI
static UINT WINAPI ThreadProc(LPVOID pParam);
HANDLE hModuleThread;//线程句柄
} ;
#endif
//module.cpp
#include "module.h"

Module::Module()
{
hExitEvent = NULL;
hModuleThread = NULL;
}
PlayerModule::~PlayerModule()
{
StopThread();//清理
}
int Module::StartThread()
{
if(NULL == ( hExitEvent = CreateEvent(NULL,TRUE,FALSE,NULL))) //手动复位
{
return ERR_CREATE_EXITEVENT;
}
HANDLE hThread;
UINT uiThreadId = 0;

hThread = (HANDLE) _beginthreadex(NULL, // attrib
0, // stack
ThreadProc, // thread proc
this, // thread proc param
CREATE_SUSPENDED, // creation mode挂起
&uiThreadId); // thread ID记录下来可做调试依据
// verify the thread handle
if ( NULL != hThread)
{
//continue thread
ResumeThread( hThread );
hModuleThread = hThread;
return RET_SUCCESS;
}
else
{
CloseHandle(hExitEvent);
hExitEvent = NULL;
return ERR_CREATE_THREAD;
}
}

UINT Module::ThreadProc(LPVOID pParam)
{
Module* pThis = reinterpret_cast<Module*>( pParam );
_ASSERTE( pThis != NULL );
pThis->Run();//真正的主线程函数
return RET_SUCCESS;
}
void Module::StopThread()
{
if(hModuleThread)
{
SetEvent(hExitEvent);//放信号
if (WaitForSingleObject(hModuleThread, 5000L) == WAIT_TIMEOUT)//等线程退出

TerminateThread(hModuleThread, RET_ABNORMAL);
CloseHandle(hModuleThread);
CloseHandle(hExitEvent);
hExitEvent = NULL;
hModuleThread = NULL;
}
}
UINT Module::Run()
{
return 0;
}
看到了吧,就三板斧
SetEvent(hExitEvent);//放信号
if (WaitForSingleObject(hModuleThread, 5000L) == WAIT_TIMEOUT)//等线程退出

TerminateThread(hModuleThread, RET_ABNORMAL);//实在不行,恼羞成怒硬退
这样肯定是安全的,如果线程返回RET_ABNORMAL,那肯定是你的run中的循环
没能退出来,

这里的run在超类中重写的。

如果你的run函数本来就是要轮循的,用全局变量也可,不过这么写也不错:

while(true)
{
.......//do something
if(WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT)
break;
}

如果有阻塞用轮循代码效率可低多了

比方producer/comsumer模式下,comsumer这头

UINT DeriveFromModule::Run()
{
HANDLE handls[2];
handls[1] = hSem; //这个次序还是蛮重要的
handls[0] = hExitEvent;
while (true)
{

DWORD ret;
ret = WaitForMultipleObjects(2,handls,FALSE,INFINITE);
if (ret == WAIT_OBJECT_0) {
break;
}
....//use Semaphore do something
}
return 0;
}

但如果阻塞的是一个io,比方是个sock。情况又不那么一样拉
实际的办法把io关了,句柄置位,线程判断句柄后退出。
不重用Module,写个IOModule,框架一样的只是没有hExitEvent。
#define INVALID_HANDLE_VALUE NULL

bool RTPSocketComm::IsOpen() const
{
return ( INVALID_HANDLE_VALUE != hComm );
}

void IOModule::StopComm()
{
// Close Socket
if (IsOpen())
{
//调用sock关闭
shutdown(sock, SD_BOTH);
closesocket( sock );
hComm = INVALID_HANDLE_VALUE;
Sleep(50);//50ms
}

// Kill Thread
if (NULL != hIOModuleThread)
{
if (WaitForSingleObject(hIOModuleThread, 5000L) == WAIT_TIMEOUT)//等5s够长了

TerminateThread(hIOModuleThread, 1L);
CloseHandle(hIOModuleThread);
hIOModuleThread = NULL;
}
}
UINT IOModule::Run()
{
DWORD dwBytes = 0L;
....
while( IsOpen() )
{
// 采用阻塞式socket,等待事件通知
dwBytes = recvfrom(....);
or dwBytes = recv(....);
or select(...)+recvfrom(..)|recv(..)

if (dwBytes > 0)
{
...//process buffer
}
else{// 错误
break;
}

}
return 0;
}

在线程外边执行StopComm()关线程。

当然也可重用Module改写run,下面select停靠了三个sock,
对于使用中的sock一般不会在select停1s以上,所以也没什么大开销,回头把sock再关了
就行了

UINT DeriveFromModule::Run()
{
fd_set fdset;
struct timeval tv;

UINT ASock;//接收端口1-3
UINT BSock;
UINT CSock;

GetASocket(&ASock);
GetBSocket(&BSock);
GetCSocket(&CSock);


while (true)
{
tv.tv_sec = 1;//1s
tv.tv_usec = 0;
FD_ZERO(&fdset);
FD_SET(ASock,&fdset);
FD_SET(BSock,&fdset);
FD_SET(CSock,&fdset);
int ret = select(FD_SETSIZE,&fdset,NULL,NULL,&tv);
if ( ret > 0) {
if(FD_ISSET(ASock,&fdset))
{
...
}
else if(FD_ISSET(BSock,&fdset))
{
...
}
else if(FD_ISSET(CSock,&fdset))
{

}
if(WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT)
break;
Sleep(0);
}
return 0;
}
select是种比较土的同步io办法拉,不过overlapped io,i/o completion ports又比较多
东西了,而且跟这篇帖子也不怎么靠边

ms恐吓我说用mfc来开发程序时得用AfxBeginThread,不好用_beginthreadex,偶知道mfc
也用Tls的,有AFX_MODULE_PROCESS_STATE,
_AFX_THREAD_STATE ,AFX_MODULE_PROCESS_STATE三个状态要线程局部化。

不过偶在工作线程里不用mfc,这些线程不加载mfc的dll,要不是图mfc做界面快....

所以偶一直非常固执的用_beginthreadex+winsock api,倒也没出过什么事。到现在为止
偶觉得用_beginthreadex也没什么大问题。

AfxBeginThread的用法类似。

1.3 媒体定时器

比windows api提供的定时器强大多了.定义查看:<mmsystem.h> winmm.lib

实际上每个定时器都是一个后台线程,可用来做采样和视频.做过的人都知道

下面是个包装类,非常之简单,resolution是要求的精度,internalTimerProc是个回调.
//mmTimers.h
#ifndef ___multimedia_timers___
#define ___multimedia_timers___
#include <mmsystem.h>
class CMMTimers
{
public:
CMMTimers(UINT resolution);
virtual ~CMMTimers();

UINT getTimerRes() { return timerRes; };

bool startTimer(UINT period,bool oneShot);//是周期的,还是只激发一次
bool stopTimer();

virtual void timerProc() {};//写处理函数

protected:
UINT timerRes;
UINT timerId;
};
#endif


//mmTimers.cpp
#include "StdAfx.h"
#include "mmTimers.h"


CMMTimers::CMMTimers(UINT resolution) : timerRes(0), timerId(0)
{
TIMECAPS tc;

if (TIMERR_NOERROR == timeGetDevCaps(&tc,sizeof(TIMECAPS)))
{
timerRes = min(max(tc.wPeriodMin,resolution),tc.wPeriodMax);
timeBeginPeriod(timerRes);
}
}


CMMTimers::~CMMTimers()
{
stopTimer();
if (0 != timerRes)
{
timeEndPeriod(timerRes);
timerRes = 0;
}
}


extern "C"
void
CALLBACK
internalTimerProc(UINT id,UINT msg,DWORD dwUser,DWORD dw1,DWORD dw2)
{
CMMTimers * timer = (CMMTimers *)dwUser;

timer->timerProc();
}


bool CMMTimers::startTimer(UINT period,bool oneShot)
{
bool res = false;
MMRESULT result;

result = timeSetEvent(period,timerRes,internalTimerProc,(DWORD)this,oneShot ?
TIME_ONESHOT : TIME_PERIODIC);
if (NULL != result)
{
timerId = (UINT)result;
res = true;
}

return res;
}


bool CMMTimers::stopTimer()
{
MMRESULT result;

result = timeKillEvent(timerId);
if (TIMERR_NOERROR == result)
timerId = 0;

return TIMERR_NOERROR == result;
}

2 互斥

2.1 互斥变量

CRITICAL_SECTION && Mutex
没什么好说的就是不跨进程的时候用CRITICAL_SECTION,CRITICAL_SECTION的速度比Mute
x快太多了
比方说锁个链表:
MYList::MYList()
{
...
InitializeCriticalSection(&criticalsection);
}

MYList::~MYList()
{

DeleteCriticalSection(&criticalsection);
}

inline void MYList::LockList()
{
EnterCriticalSection(&criticalsection);
}
inline void MYList::UnlockList()
{
LeaveCriticalSection(&criticalsection);
}

int MYList::Add(ListMember *mem)
{
EnterCriticalSection(&criticalsection);
....//调链表指针
LeaveCriticalSection(&criticalsection);
......
}
ListMember* MYList::Extract()
{
EnterCriticalSection(&criticalsection);
....//调链表指针
LeaveCriticalSection(&criticalsection);
.....
}

semaphore用来实现producer/consumer模型

比方说上面的链表就可用作一个管道,同步在内部都做好了,比方链表每add一个元素

用ReleaseSemaphore(hSem,1,NULL)发一个通知。consumer这边WaitForSingleObject()返
回,

并用Extract()接收一个

3. 同步

WaitForSingleObject/WaitForMultipleObjects前边都有。

你可能感兴趣的:(多线程)