Debug 模式 和 Release 模式下,一个线程在"编译"上引发的问题

本文详细阐述了C++线程类在退出机制上的问题,特别是CWinThread派生类如何正常退出,包括使用PostThreadMessage、WaitForSingleObject等函数解决死循环问题,并引入volatile关键字避免寄存器优化带来的错误。通过实例分析,提供了解决方案,确保线程在Release模式下也能正确退出。

1.由CWinThread派生的CTestThread线程类

      声明一点:CTestThread类是由CWinThread派生的用户界面线程(即一个带消息循环的工作者线程)!!!

        CTestThread.h文件

#pragma once
// CTestThread

class CTestThread : public CWinThread
{
	DECLARE_DYNCREATE(CTestThread)

public:
	CTestThread();           // 动态创建所使用的受保护的构造函数
	virtual ~CTestThread();

public:
	virtual BOOL InitInstance();
	virtual int ExitInstance();

protected:
	DECLARE_MESSAGE_MAP()
        // Handler	虚拟、按钮、TTL控制		量产消息
	afx_msg void OnHandlerTest(WPARAM wParam, LPARAM lParam);
        // ... ...
        // 其他消息,略过。	
};

      CTestThread.cpp文件

// TestThread.cpp : 实现文件
//

#include "stdafx.h"
#include "..\DynamicATE.h"
#include "..\MainFrm.h"
#include "..\DynamicATEDoc.h"
#include "..\DynamicATEView.h"
#include "..\MessageInfo.h"
#include "TestThread.h"

#define PMANAGER(x)	((CDynamicATEApp*)(AfxGetApp()))->Get##x##ManagerPointer()
// CTestThread

IMPLEMENT_DYNCREATE(CTestThread, CWinThread)

CTestThread::CTestThread()
{
}

CTestThread::~CTestThread()
{
}

BOOL CTestThread::InitInstance()
{
	// TODO: 在此执行任意逐线程初始化
	return TRUE;
}

int CTestThread::ExitInstance()
{
	// TODO: 在此执行任意逐线程清理
	return CWinThread::ExitInstance();
}

BEGIN_MESSAGE_MAP(CTestThread, CWinThread)
	ON_THREAD_MESSAGE(WM_HANDLER_TEST,	OnHandlerTest)
	// ... ...
        // 其他消息,略过。
END_MESSAGE_MAP()


// CTestThread 消息处理程序

// Handler	虚拟、按钮、TTL控制		量产消息
void CTestThread::OnHandlerTest(WPARAM wParam, LPARAM lParam)
{
	PMANAGER(Handler)->HandlerTest();
}
// ... ...
// 其他处理消息,略过。

2.Handler接口管理类中的Handler测试线程的开始、结束控制以及线程执行体

      在HandlerManager.h中,一些本文需要的变量、函数声明,如下:

protected:
	BOOL		m_bHandlerThread;	// 标志是否处于Handler线程中
	//BOOL		m_bCanStopThread;	// 标志是否可关闭Handler线程

	// Handler类型 -1 - 初始 0 - Virtual 1 - ControlPad 2 - TTL
	int			m_iHandlerType;		
	int			m_iBtnType;			// 按键类型 0 - 左键 1 - 右键 2 - 双键 3 - 任意单键
	int			m_iSigType;			// 信号类型	0 - 电平 1 - 脉冲沿

	WORD		m_iBinValue;		// BIN分箱值

	CTestThread* m_pHandlerThread;	// Handler线程指针

public:
	// TRUE - 开启Handler测试线程; FALSE - 关闭Handler测试线程;
	void		HandlerEnable(BOOL bEnable); 
	void		HandlerTest(void);			 // Handler 测试处理过程

	bool		StartOfTest(void);			 // 接受测试开始信号 SOT
	void		EndOfTest(void);			 // 发送测试结束信号 EOT
      在HandlerManager.cpp文件中的HandlerEnable()函数、HandlerTest()函数,如下:

void CHandlerManager::HandlerEnable(BOOL bEnable)
{
	if(bEnable){
		if(m_pHandlerThread){
			delete m_pHandlerThread;	m_pHandlerThread = NULL;
		}
		m_bHandlerThread = TRUE;
		m_bCanStopThread = FALSE;
		m_pHandlerThread = new CTestThread;
		m_pHandlerThread->CreateThread();
                // 发送Handler测试消息,开始线程循环体HandlerTest()
		m_pHandlerThread->PostThreadMessageA(WM_HANDLER_TEST, NULL, NULL);
	}
	else{
		m_bHandlerThread = FALSE;
		PMANAGER(Test)->SetTesting(FALSE);
/*断点1*/while(!m_bCanStopThread){};// !!! Release下死循环点:问题1
		DWORD exitCode = 0;
		GetExitCodeThread(m_pHandlerThread->m_hThread, &exitCode);
		TerminateThread(m_pHandlerThread->m_hThread, exitCode); //线程终止:问题2
		delete m_pHandlerThread;	m_pHandlerThread = NULL;
	}
}

void CHandlerManager::HandlerTest()
{
	// 若信号类型为脉冲沿,则开始前初始化NSTn、STnEN设置
	if(m_iSigType == 1){
		WORD nst = m_bSotSlope ? 0x0000 : 0x00c0;// 决定SOT有效沿的方向
		WORD eot = m_bEotSlope ? 0x0000 : 0x0030;// 设置EOT为非有效状态
		POINTER(SYSINTF)->Write(0x8087, eot | nst | 0x0000);
		POINTER(SYSINTF)->Write(0x8087, eot | nst | 0x0003);
	}
	int _test_cnt = 0;
	// 线程循环过程
    // 若Handler线程标志为TRUE,线程一直执行
    // 该标志也可用事件Event和WaitSingleObject来处理
	while(m_bHandlerThread){
		if(StartOfTest()){// 若接收到SOT信号
			// 若信号类型为电平,则接收到SOT信号后结束上次测试的BIN信号
			if(m_iSigType == 0){
				// 结束BIN信号,m_bBinLevel:true - 0x0000; false - 0xffff
				POINTER(SYSINTF)->Write(0x8086, m_bBinLevel ? 0x0000 : 0xffff);	
			}
			// 设置正在测试中标志为TRUE
			PMANAGER(Test)->SetTesting(TRUE);
			// 发送测试消息,进行测试
			PMANAGER(Test)->GetTestThreadPointer()->PostThreadMessageA(
				(WM_TRTF_TEST + PMANAGER(Test)->GetTestHead()), NULL, NULL);
			// 循环等待测试中标志变为FALSE
			while(PMANAGER(Test)->IsTesting()){}
			// 发送BIN信号、EOT信号
			EndOfTest();

			if(m_iHandlerType == 0) _test_cnt++; // Virtual Handler模式下 测试次数自加1
		}
                // Virtual Handler模式下 测试次数超过m_iTestCnt线程退出
		if(m_iHandlerType == 0 && _test_cnt >= m_iTestCnt)
			break;
	}
/*断点2*/m_bCanStopThread = TRUE;
}

void CHandlerManager::HandlerTest() // 简化版本。
{
	while(m_bHandlerThread){
		if(StartOfTest()){// 若接收到SOT信号
		    // ... ...
            // 发送测试消息,具体略过。
            // ... ...
			EndOfTest();
		}
		if(condition1)
			break;
	}
/*断点2*/m_bCanStopThread = TRUE;
}

3.问题描述及解决思路

      Debug模式下,HandlerEnable函数、HandlerTest函数 运行良好,断点1、断点2 逻辑正常,从未出错!!!      Release模式下,出现问题,当HandlerEnable(FALSE)函数执行完以下两句

    m_bHandlerThread = FALSE;
    PMANAGER(Test)->SetTesting(FALSE);
      可以让HandlerTest()函数的循环体顺利执行到 “断点2” 处(m_bCanStopThread = TRUE),并使标志发生变化,可是当程序继续执行,运行到 “断点1” ( while(!m_bCanStopThread) { }; )时陷入了死循环!!!

      1.请教别人

      “断点2”处标志和“断点1”处的循环,是为了知道HandlerTest线程已正常退出循环过程,正常执行完毕。                   有人说1:你怎么能用TerminateThread函数强制终止线程呢?为什么不让它自然退出?                                         有人说2:你怎么能写这么多死循环呢?想知道线程正常退出为什么不用WaitForSingleObject等函数呢?

      仅说明以上两种比较多的建议和思路,他们的建议都是好的,再次感谢!!但最终他们都没有解释出,为什么Debug下逻辑可行运行正常而Release下会在“断点1”处出现死循环?

      2.解决CWinThread派生类正常退出问题

      CTestThread类是由CWinThread类派生的用户界面线程,就是一个带消息循环的工作者线程工作者线程是以线程函数执行完毕return返回作为线程结束的判断,一般认为线程函数return就是线程退出了。而用户界面线程的结束机制是不同于工作者线程的,一般用户界面线程是响应消息来执行消息处理函数,但是消息处理函数执行完毕,并不代表线程结束;发现一个问题就是用 TerminateThread 函数强制终止线程,并不能执行到CTestThread的 ExitInstance 函数,那是不是想正常退出CTestThread就需要让线程执行到ExitInstance函数?

      CTestThread线程被创建后,就处于CWinThread::Run里的消息循环中了,先看CWinThread::Run的实现机制 ( 如下 ) 。可以看出想正常退出线程,先要正常退出Run函数,在Run函数 do...while...循环中第一个if分支:if ( ! PumpMessage( ) ) return ExitInstance( ); 如果线程接收到WM_QUIT消息,则执行ExitInstance函数退出Run函数。

// main running routine until thread exits
int CWinThread::Run()
{
	ASSERT_VALID(this);
	_AFX_THREAD_STATE* pState = AfxGetThreadState();

	// for tracking the idle time state
	BOOL bIdle = TRUE;
	LONG lIdleCount = 0;

	// acquire and dispatch messages until a WM_QUIT message is received.
	for (;;)
	{
		// phase1: check to see if we can do idle work
		while (bIdle &&
			!::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE))
		{
			// call OnIdle while in bIdle state
			if (!OnIdle(lIdleCount++))
				bIdle = FALSE; // assume "no idle" state
		}

		// phase2: pump messages while available
		do
		{
			// pump message, but quit on WM_QUIT
			if (!PumpMessage())
				return ExitInstance();

			// reset "no idle" state after pumping "normal" message
			//if (IsIdleMessage(&m_msgCur))
			if (IsIdleMessage(&(pState->m_msgCur)))
			{
				bIdle = TRUE;
				lIdleCount = 0;
			}

		} while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE));
	}
}
      正常退出解决方法:CTestThread线程想正常退出,给线程发送WM_QUIT消息即可,则HandlerTest()函数中断点2处 m_bCanStopThread = TRUE修改为发送WM_QUIT消息,HandlerEnable函数 结束线程 部分用WaitForSingleObject函数等待WM_QUIT消息执行完毕。当然这样也就避免了m_bCanStopThread 标志的使用出现的问题。

// HandlerTest()函数中
/*断点2*/m_pHandlerThread->PostThreadMessageA(WM_QUIT, NULL, NULL);


// HandlerEnable函数中 结束线程部分
m_bHandlerThread = FALSE;
PMANAGER(Test)->SetTesting(FALSE);
WaitForSingleObject(m_pHandlerThread->m_hThread, INFINITE);
delete m_pHandlerThread;	
m_pHandlerThread = NULL;
      经过上述修改后,在Release模式下线程在结束时,可以顺利执行到delete m_pHandlerThread; 恰恰这句话又出现了中断问题。设断点调试发现,原因是CWinThread线程类有个标志m_bAutoDelete可以决定在线程收到WM_QUIT结束线程后自行销毁CWinThread的句柄等资源,由于m_pHandlerThread是派生类CTestThread的指针,虽然m_pHandlerThread指针下原属于CWinThread的句柄等资源都已经被销毁了,并不代表m_pHandlerThread指针就为NULL,这样执行delete m_pHandlerThread;时是想删除一个已经不完整的指针,肯定中断报错。

      解决delete m_pHandlerThread; 中断问题方法:在线程创建之初,先Suspend挂起线程,把m_bAutoDelete函数设为FALSE,再Resume继续线程。这样CWinThread执行WM_QUIT消息后不执行自销毁过程,delete m_pHandlerThread;执行时就能顺利删除一个完整的CTestThread。

m_pHandlerThread = new CTestThread;
m_pHandlerThread->CreateThread(CREATE_SUSPENDED);
m_pHandlerThread->m_bAutoDelete = FALSE;
m_pHandlerThread->ResumeThread();
      至此,正常退出CWinThread派生线程的问题已解决。

    3.Release下会在“断点1”处出现死循环的原因和解决方法

      为了找到最最初始问题:为什么Debug下逻辑可行运行正常而Release下会在“断点1”处出现死循环? 对“断点1”处代码,在Debug和Release模式下反汇编

      Debug模式下,断点1的反汇编如下

// BOOL m_bCanStopThread; 标志声明,Debug模式下“断点1”反汇编
		while(!m_bCanStopThread){};
005E320C  mov         eax,dword ptr [ebp-14h] 
005E320F  cmp         dword ptr [eax+8],0 
005E3213  jne         CHandlerManager::HandlerEnable+197h (5E3217h) 
005E3215  jmp         CHandlerManager::HandlerEnable+18Ch (5E320Ch)
      Release模式下,断点1的反汇编如下

// BOOL m_bCanStopThread; 标志声明,Release模式下“断点1”反汇编
0047F83E  mov         eax,dword ptr [esi+8] 
		while(!m_bCanStopThread){};
0047F841  cmp         eax,ebx 
0047F843  je          CHandlerManager::HandlerEnable+0E1h (47F841h) 
      对比 Debug模式 和 Release模式 下,断点1的反汇编,发现Debug模式下while循环内判断每次处理实际地址ptr [eax + 8]的m_bCanStopThread标志,而Release模式下,被优化为早在执行while语句前先把实际地址ptr [esi + 8] 的m_bCanStopThread 先存到了寄存器eax中,虽然在 “断点2” 处m_bCanStopThread已变为TRUE,但是寄存器eax的值并未得到更新,while语句每次循环时,是跟寄存器eax中的数据比较,因此陷入死循环!!!
      出现问题的原因:
Release模式下,优化程序为了使程序性能提高,常把一些变量放在寄存器中(类似于 register 关键字),而其他进程只能对该变量所在的内存进行修改,而寄存器中的值没变。尤其是多线程的,或者你发现某个变量的值与预期的不符而你确信已正确的设置了,则很可能遇到这样的问题。这种错误有时也会表现为程序在最快优化出错而最小优化正常。

      解决问题的方法:使用关键字 volatile 声明,volatile BOOL m_bCanStopThread;                                             C++中关键字volatile详解:http://blog.csdn.net/monkey07118124/article/details/50536729                                     多线程下的volatile:当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。

      Release模式下,断点1处的反汇编。显示表明用volatile关键字后,while循环每次都从内存中取值比较运算。

// volatile BOOL m_bCanStopThread; 标志声明,Release模式下反汇编
		while(!m_bCanStopThread){};
0047F840  mov         eax,dword ptr [esi+8] 
0047F843  test        eax,eax 
0047F845  je          CHandlerManager::HandlerEnable+0E0h (47F840h) 
      至此,Release下会在”断点1“处出现死循环的问题已解决,但最终未使用m_bCanStopThread标志。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GnakIewiy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值