Chapter06C/C++运行库

在上一篇关于线程的讲解中,有提到一般我们都不应该直接调用CreateThread函数去创建新线程,而是调用_beginthreadex函数创建新线程。

以下是_beginthreadex函数的伪代码:

uintptr_t __cdecl _beginthreadex (
	void *psa,
	unsigned cbStackSize,
	unsigned (__stdcall * pfnStartAddr) (void *),
	void * pvParam,
	unsigned dwCreateFlags,
	unsigned *pdwThreadID) {
		_ptiddata ptd; // Pointer to thread's data block
		uintptr_t thdl; // Thread's handle
		// Allocate data block for the new thread.
		if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL)
			goto error_return;
		// Initialize the data block.
		initptd(ptd);
		// Save the desired thread function and the parameter
		// we want it to get in the data block.
		ptd->_initaddr = (void *) pfnStartAddr;
		ptd->_initarg = pvParam;
		ptd->_thandle = (uintptr_t)(-1);
		// Create the new thread.
		thdl = (uintptr_t) CreateThread((LPSECURITY_ATTRIBUTES)psa, cbStackSize,
			_threadstartex, (PVOID) ptd, dwCreateFlags, pdwThreadID);
		if (thdl == 0) {
			// Thread couldn't be created, cleanup and return failure.
			goto error_return;
		}
		// Thread created OK, return the handle as unsigned long.
		return(thdl);
error_return:
		// Error: data block or thread couldn't be created.
		// GetLastError() is mapped into errno corresponding values
		// if something wrong happened in CreateThread.
		_free_crt(ptd);
		return((uintptr_t)0L);
}


关于_beginthreadex函数我们需要注意:

  • 每个线程从C/C++运行库堆中获取自己的_tiddata内存块。
  • 传递给_beginthreadex的线程函数地址记录在_tiddata内存块中。
  • _beginthreadex函数内部调用了CreateThread函数,因为这是操作系统知道的创建新线程的唯一方法。
  • 当CreateThread函数被调用时,它会被告知要开始执行_threadstartex函数(而不是pfnStartAddr)去开始一个新线程。同时也需要注意,此时传递过去的参数是_tiddata结构体地址而不是pvParam.
  • 如果一切顺利的话,就会像CreateThread函数一样返回线程句柄。如果操作失败,则返回0值。


////////////////////////////////////////////////////////////////


上面提到了重要的_threadstartex函数,下面是它的伪代码:

static unsigned long WINAPI _threadstartex (void* ptd) {
	// Note: ptd is the address of this thread's tiddata block.
	// Associate the tiddata block with this thread so
	// _getptd() will be able to find it in _callthreadstartex.
	TlsSetValue(__tlsindex, ptd);
	// Save this thread ID in the _tiddata block.
	//Windows via C/C++, Fifth Edition by Jeffrey Richter and Christophe Nasarre
		((_ptiddata) ptd)->_tid = GetCurrentThreadId();
	// Initialize floating-point support (code not shown).
	// call helper function.
	_callthreadstartex();
	// We never get here; the thread dies in _callthreadstartex.
	return(0L);
}
static void _callthreadstartex(void) {
	_ptiddata ptd; /* pointer to thread's _tiddata struct */
	// get the pointer to thread data from TLS
	ptd = _getptd();
	// Wrap desired thread function in SEH frame to
	// handle run-time errors and signal support.
	__try {
		// Call desired thread function, passing it the desired parameter.
		// Pass thread's exit code value to _endthreadex.
		_endthreadex(
			((unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr))
			(((_ptiddata)ptd)->_initarg)) ;
	}
	__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){
		// The C run-time's exception handler deals with run-time errors
		// and signal support; we should never get it here.
		_exit(GetExceptionCode());
	}
}

关于_threadstartex函数,需要注意:

  • 一个新线程最开始执行RtlUserThreadStart函数,然后跳到_threadstartex。
  • 新线程的_tiddata数据块是_threadstartex函数唯一的参数。
  • TlsSetValue函数是一个将一个数值关联到线程的系统函数。
  • 在无参数的_callthreadstartex函数中,有一个SEH帧,它将预期要执行的线程函数包围起来。这个SEH帧处理许多鱼运行库相关的事情(比如运行错误等)。
  • 接下来就执行预期函数pfnStartAddr,并传递预期参数pvParam。pfnStartAddr和pvParam之前被记录在_tiddata块中。
  • 预期的线程函数返回值就是线程的退出码。值得注意的是:_callthreadstartex不仅仅是返回至_threadstartex然后再返回到RtlUserThreadStart;如果真的这样做,线程会消亡,退出码也能正确设置,但是线程的_tiddata内存块不会被销毁。这样将会导致你的程序出现内存泄漏。为了防止这种情况,就需要调用C/C++运行库函数--_endthreadex。



////////////////////////////////////////////////////////////////


下面就来介绍_endthreadex函数,下面就是其伪代码:

void __cdecl _endthreadex (unsigned retcode) {
	_ptiddata ptd; // Pointer to thread's data block
	// Clean up floating-point support (code not shown).
	// Get the address of this thread's tiddata block.
	ptd = _getptd_noexit ();
	// Free the tiddata block.
	if (ptd != NULL)
		_freeptd(ptd);
	// Terminate the thread.
	ExitThread(retcode);
}

对于_endthreadex函数,我们需要注意的是:

  • 当你的线程函数返回时,_beginthreadex函数会调用_endthreadex函数。
  • C运行库的_getptd_noexit函数内部调用操作系统的TlsGetValue函数,TlsGetValue函数获取调用线程的tiddata内存块地址。
  • 这个_tiddata数据块之后被释放,然后调用操作系统ExitThread函数去真正销毁线程。

在实际编程过程中,我们也不应该直接调用ExitThread函数去退出一个线程,而是调用_endthreadex函数。原因有两个:

  • 调用ExitThread会杀死线程,而不会让执行的线程函数返回。如果线程函数没有返回,则函数内的C++对象就不会被销毁。
  • 调用ExitThread退出程序后,不会回收_tiddata内存块。这样你的应用程序存在内存泄漏。

原文地址:https://www.cnblogs.com/java20130722/p/3207148.html