你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

CString 类线程不安全问题和解决过程

[复制链接]
gaosmile 发布时间:2020-8-5 21:06
1 问题描述

CString 类线程不安全问题和解决过程,测试运行一段时间后,后台软件崩了,软件重启后,恢复正常,隔三四小时又出现异常,Debug模式下调用堆栈,发现问题出现在strname = pSystemInfo-> szName 这一行。

程序中定义结构体(相关的成员变量):

typedef struct _SYSTEMINFO_CONTEXT
{
     CString szMac;  //mac地址
     CString szName;       //工程名称

     //构造
     _SYSTEMINFO_CONTEXT()
    {
       tAutoTime=COleDateTime::
      GetCurrentTime();//初始化当前时间
    }

    //析构
    ~_SYSTEMINFO_CONTEXT()
    {
   
    }
}SYSTEMINFO_CONTEXT,*PSYSTEMINFO_CONTEXT;

strname = pSystemInfo-> szName;      //异常报错地方

异常报错地方 strname = pSystemInfo-> szName ,调试指向:

const CString& CString:perator=(const CString& stringSrc)
{
  if (m_pchData != stringSrc.m_pchData)
  {
    if ((GetData()->nRefs < 0 && GetData() != _afxDataNil) ||
      stringSrc.GetData()->nRefs < 0)
    {
      // actual copy necessary since one of the strings is locked
      AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);
    }
    else
    {
      // can just copy references around
      Release();
      ASSERT(stringSrc.GetData() != _afxDataNil);
      m_pchData = stringSrc.m_pchData;
      InterlockedIncrement(&GetData()->nRefs);
    }
  }
  return *this;
}

2 问题分析

报异常处strName = pSystemInfo-> szName,strName 内存无法读取,pSystemInfo-> szName 为一物联网设备的名称,多次测试都是同一设备出问题,刚开始怀疑与这一设备有关,于是屏蔽该设备继续测试。

if(pSystemInfo-> szMac == 这个设备的实际mac地址)
{
    Break;
}

运行三五小时过去了,没有出现异常,以为这样就解决了, 三四天过去了软件又报异常,这次发现异常还在同一地方 pSystemInfo-> szName 换其他设备了,意识到异常与设备无关,但为什么以前测试总是同一设备出问题呢。

经抓包软件抓取数据包发现,其他设备发送数据和心跳数据包都比较稳定,经常出问题的这台设备发送数据的频率异常,一秒发好几条数据,这就解释了为什么出问题的时候总是这个设备名称,其实和设备没有关系,只是因为这台设备频率快出错的概率更大一些,于是,写了测试代码对后台软件狂发测试数据,果然不到 20 分钟报异常了,异常还在这里。

经过分析以及查资料得知,导致这个问题的根本原因是因为 CString 类不是线程安全的,在线程中进行 CString 字符串的拷贝会导致内存泄露。

CString 的 “=” 拷贝操作的源码如下:

inline const CString& CString:perator=(const CString& stringSrc)
{
    if (m_pchData != stringSrc.m_pchData) //防止自拷贝
    {
        //如果目标对象的数据空间已经分配,而且没有别的CString实例指向它,则把stringSrc的数据拷贝过来;
        //或者,如果stringSrc的数据的引用次数小于零,则把它的数据拷贝过来
        if ((GetData()->nRefs < 0 && GetData() != afxDataNil) ||
            stringSrc.GetData()->nRefs < 0)
        {
            // actual copy necessary since one of the strings is locked
            AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);
        }
        else
        {
            //其他情况,就先自己的数据空间释放掉,然后再指向stringSrc的数据空间
            // can just copy references around
            Release();
            ATLASSERT(stringSrc.GetData() != afxDataNil);
            m_pchData = stringSrc.m_pchData;
            InterlockedIncrement(&GetData()->nRefs);  //引用次数增1,这里为了防止多线程情况下出错,使用了原子性自增
        }
    }
    return *this;
}

CString 的拷贝构造函数没有使用内存分配,而是使用的引用,它内部保存了一个引用的计数器(这是错误的根源)。

比如:

CString str1="aaa";

CString str2=str1;   //注意,这时候str2并没有调用 new ,而是使用str1的引用同时,str1中保存的引用记数++。

str2="abcd1234";     //好了,现在str2才分配内存,str1引用记数--。这同时也是为什么str2.Empty()就没有内存泄露的原因。因为str1的引用记数也--了。

另外,它分配内存的方法是 4 字节对齐的 64 字节的倍数 + sizeof (内部结构)(超过 64 的时候)。

在多数情况下,比较简单的使用过程中,MFC 的这个 BUG 不会发作,也就是不会有内存泄露。

那什么时候 CString 会暴露出 BUG 呢?如果多次调用带有CString 引用的参数的函数,在一定的时候,CString 的内部引用记数器发生记数混乱,造成内存泄露

所以尽量避免使用 CString ,例如可以改用 char 数组。

修改后的结构体定义:

typedef struct _SYSTEMINFO_CONTEXT
{
  Char szMac[20];       //mac地址,对应macAddr
  Char szName[50];     //工程名称

  //构造
  _SYSTEMINFO_CONTEXT()
  {
    tAutoTime = COleDateTime::GetCurrentTime();//初始化当前时间
  }
  //析构
  ~_SYSTEMINFO_CONTEXT()
  {
  
  }
}SYSTEMINFO_CONTEXT,*PSYSTEMINFO_CONTEXT;

strncpy(pSystemInfo->szName,(LPCTSTR)strName,sizeof(pSystemInfo->szName)); //这样就不会出错了

VC++7(.NET) 中,MFC 彻底修改了 CString ,把它写成了 CString T 形式的模板类。

CString 在线程处理中,稍有处理不当,极易引起内存泄漏。

再看另外一个例子:

//在线程函数中定义CString局部变量并进行操作

CString strstate;  
strstate.Format("正在解密,请稍后... (共 %d 张地图)",p->m_countmap);

可以看到非常简单,在 debug 下,很容易看到如下的内存泄漏。

微信图片_20200805210541.png

遇到这种问题可以使用 ATL 提供的 CAtlStringMgr 类对字符串数据进行自定义内存分配。

CWin32HeapstringHeap(HEAP_NO_SERIALIZE, 0, 0 );
CAtlStringMgr stringMgr(&stringHeap );
CString strstate(&stringMgr );
strstate.Format("正在解密,请稍后... (共 %d 张地图)",p->m_countmap);

如上代码才具有线程安全性。

3 总结

我们开发时经常会用到 CString 类,无可否认,CString 类是很好用,但很少人注意到 CString 类不是线程安全的。一般地,界面编程都是在主线程,很少用到多线程,所以不会遇到什么问题。但是,当我们多个线程同时操作同一个 CString类型变量时,就可能会出现内存地址错误,最终导致进程异常退出。内存错误导致的问题也很难调查,通常导致内存错误的地方没有马上报异常,而且在程序的其他地方才捕获异常。

CString 类的 Debug 版本和 Release 版本不完全一样,Debug 版本则直接分配( MFC 在 Debug 版本有内存管理,主要是为了排错,内存泄漏等),CString 类在 Release 版本会使用定长内存管理( CFixedAlloc 类),主要管理是4个长度的内存,

CString类的Debug版本和Release版本不完全一样,Debug版本则直接分配(MFC在Debug版本有内存管理,主要是为了排错,内存泄漏等),CString类在Release版本会使用定长内存管理(CFixedAlloc类),主要管理是 4个 长度的内存,如下:

1AFX_STATICCFixedAlloc _afxAlloc64(ROUND4(65*sizeof(TCHAR)+sizeof(CStringData)));

2AFX_STATICCFixedAlloc _afxAlloc128(ROUND4(129*sizeof(TCHAR)+sizeof(CStringData)));

3AFX_STATICCFixedAlloc _afxAlloc256(ROUND4(257*sizeof(TCHAR)+sizeof(CStringData)));

4AFX_STATICCFixedAlloc _afxAlloc512(ROUND4(513*sizeof(TCHAR)+sizeof(CStringData)));

这样做是防止内存碎片和提高效率,由于 CString 类都会重用分配的定长内存,所以一般异常的地方大多数也是在 CString 操作的地方。

  • 当在多线程中使用共享 CString 变量时,遇到异常问题最简单的解决办法就是不用 CString改为 char * 或 char[] 。
  • 当在线程中使用局部变量时候,可以使用 ATL 提供的 CAtlStringMgr 类对字符串数据进行自定义内存分配,保证线程的中 CString 变量的安全性。
// Declare a non-thread-safe heap just for this thread
CWin32HeapstringHeap( HEAP_NO_SERIALIZE, 0, 0 );
// Declare a string manager that uses the thread's heap
CAtlStringMgrstringMgr( &stringHeap );
//do anything you want

例如:

{
    CString strstate(&stringMgr );
    strstate.format(……);
}
收藏 评论0 发布时间:2020-8-5 21:06

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版