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

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

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

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

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

typedef struct _SYSTEMINFO_CONTEXT
) A8 \$ p4 ^0 K0 s9 Z{& M/ x, W3 _% a% F' |5 e
     CString szMac;  //mac地址
  y" n' i. Y# M. L9 M- p  I) K     CString szName;       //工程名称3 \$ @1 w/ `4 b% s+ N

0 H; i# A# D0 }4 Z) g' F! ^3 p0 p     //构造
0 ]# r* s: Z, c4 L( U: F) m9 L( A     _SYSTEMINFO_CONTEXT()9 L, |4 }# V1 g5 Z( u- D
    {0 t3 Y& }& K& x( ~
       tAutoTime=COleDateTime::$ N$ V) Z" P! J% \$ u9 y7 v
      GetCurrentTime();//初始化当前时间* N' k3 z" r6 B# B6 C$ H
    }
1 D6 X4 O1 W+ H9 P+ u! W  L8 T/ ?9 Q# N9 ?  @6 J- `
    //析构
* b; p2 l/ _# x/ d8 p- T    ~_SYSTEMINFO_CONTEXT()9 {( k1 N' S+ K+ D
    {
% I0 i! [. C. e) o   
% }8 ?7 [0 X& L' a" z- @    }
' a% J8 p6 N/ B6 g8 t) g}SYSTEMINFO_CONTEXT,*PSYSTEMINFO_CONTEXT;
1 Q& g! L: L' z" K! `  c) M- M" W% D3 K2 s$ h8 A; j0 d
strname = pSystemInfo-> szName;      //异常报错地方
8 b; o- D3 F3 l. l  a7 J$ Z8 P

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

const CString& CString:perator=(const CString& stringSrc)0 o# z2 B; Z( Y" [5 q" {1 m
{
3 f7 C- Q/ Y: F6 v  D: d8 j8 t  if (m_pchData != stringSrc.m_pchData)
  I+ L9 B) E# p  p& q) X2 a2 I2 O  {
: z: Q% _- D' T8 D0 c7 ^" G% D, q    if ((GetData()->nRefs < 0 && GetData() != _afxDataNil) ||
9 M2 J5 L4 i) p2 \8 U. j      stringSrc.GetData()->nRefs < 0)1 @  m% Z% I' A. j' m6 V
    {+ u' @' l1 e1 R/ R6 e! e
      // actual copy necessary since one of the strings is locked( h* G4 G' }( [% J1 Z0 \! t
      AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);9 m$ ]& H6 l4 W" u
    }3 `$ Z. x6 h- Q  w$ f1 Q5 @
    else+ S( w/ s2 P! w2 P. @+ B
    {
( I, ^; R2 H4 S& ^, ]4 g& v      // can just copy references around
8 k+ B7 G$ U$ \8 P      Release();
8 W" _8 [* K3 s5 w$ L0 {      ASSERT(stringSrc.GetData() != _afxDataNil);& x; A5 A( t: j
      m_pchData = stringSrc.m_pchData;
5 b+ J; I/ d8 S' R2 o. y: B. k6 Y      InterlockedIncrement(&GetData()->nRefs);9 {) u; \. U% m# I4 K; O
    }, U( q% T3 L( |8 k( c3 n$ y8 g# f
  }
  Z* |1 i0 P1 Y( G* A2 b  return *this;3 b9 _0 f+ p" p: s, k3 c0 l
}

; z9 `) S+ o0 o& k9 K0 A2 K2 问题分析

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

if(pSystemInfo-> szMac == 这个设备的实际mac地址)
& g% p* x3 H9 G( P1 h1 t8 t; W{
; M( ~2 b" K$ K    Break;7 k. a( Z5 m) \0 ~! h- w+ c. p; y  y) O
}

7 C# z2 B1 Q6 K9 i% u

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

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

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

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

inline const CString& CString:perator=(const CString& stringSrc)' ?, V, B) f" ~0 j
{
1 t/ V, f1 \6 }# }    if (m_pchData != stringSrc.m_pchData) //防止自拷贝
4 Z2 @  B* J/ U    {. Z" \3 s5 u1 A* ^
        //如果目标对象的数据空间已经分配,而且没有别的CString实例指向它,则把stringSrc的数据拷贝过来;
# U( v5 G5 d4 s' I) O. J        //或者,如果stringSrc的数据的引用次数小于零,则把它的数据拷贝过来& x3 `1 p0 J: Q' A! e
        if ((GetData()->nRefs < 0 && GetData() != afxDataNil) ||
  G& m- N$ g& y            stringSrc.GetData()->nRefs < 0)% j& W+ G- F& b8 x" B
        {; g, P( j' \$ A  f' P
            // actual copy necessary since one of the strings is locked
7 A: A7 X! s2 w            AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);: ?! ^9 v$ B6 h% t$ k9 Q: d% y, d
        }
' t' e9 j: o8 A5 i        else
- a# e( U/ k, a: x9 A        {
! P' Z/ A; e2 f7 g2 B; L) e5 O7 I  |' E            //其他情况,就先自己的数据空间释放掉,然后再指向stringSrc的数据空间
4 g' I) V) [8 v8 L1 g( x9 _! B            // can just copy references around, f5 S6 b0 z3 u7 K
            Release();
6 q: N3 A! q) j& G0 R            ATLASSERT(stringSrc.GetData() != afxDataNil);& a* r  i# o. _
            m_pchData = stringSrc.m_pchData;
# |$ N/ S) t" A, W7 ^0 n            InterlockedIncrement(&GetData()->nRefs);  //引用次数增1,这里为了防止多线程情况下出错,使用了原子性自增
1 t  w2 B  ^% M4 W. h- E4 D        }" i8 w: Y- o- d  s) g' D
    }# ^: Q$ g) u7 j$ s* z. w+ c* X
    return *this;' b4 N1 y" Y. s. P) z4 U* G
}
7 K9 ]5 V9 D2 @" u5 i% M

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

比如:

CString str1="aaa";
9 Q8 E! r% Y" z9 n
0 G" W3 z; r) ?8 K$ A: J9 _: r4 w7 `CString str2=str1;   //注意,这时候str2并没有调用 new ,而是使用str1的引用同时,str1中保存的引用记数++。
( u; z* \+ u. m2 Q$ h4 [# A
/ @; a* s9 ^4 @2 B$ Cstr2="abcd1234";     //好了,现在str2才分配内存,str1引用记数--。这同时也是为什么str2.Empty()就没有内存泄露的原因。因为str1的引用记数也--了。

3 J6 w% l& V# a) V

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

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

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

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

修改后的结构体定义:

typedef struct _SYSTEMINFO_CONTEXT
  U& L+ d; |' X* T% ^; {{1 [5 |/ \6 ^+ P: \# o
  Char szMac[20];       //mac地址,对应macAddr, A% ^# J- ~7 N$ _5 e' x, i* t
  Char szName[50];     //工程名称& d5 q* l/ v6 L) {0 }! E5 i

  E4 r  j9 x  \8 s  //构造: e, ^8 ^* B' K7 F2 X7 L
  _SYSTEMINFO_CONTEXT()$ i( T9 [2 Z" e$ N3 M7 `# Q3 s4 {
  {2 t% k8 D: E' z+ r& i* w
    tAutoTime = COleDateTime::GetCurrentTime();//初始化当前时间3 J: I. U3 k" V$ K' m! e  F$ F" [
  }
2 V/ ]7 K7 k3 f. c7 }  //析构
# g# w$ S0 H6 E7 N  ~_SYSTEMINFO_CONTEXT()
; P, y+ f5 u' h3 B  {, a1 e0 g6 H) F7 Q0 W) p
  
3 M# T1 D: R; X0 K2 V4 I- m  }
( [. T/ s( k( a" T/ x}SYSTEMINFO_CONTEXT,*PSYSTEMINFO_CONTEXT;+ ?; ^0 O4 R. P8 }7 a

' S, ]% M4 r/ ]' |strncpy(pSystemInfo->szName,(LPCTSTR)strName,sizeof(pSystemInfo->szName)); //这样就不会出错了

7 |- b, V7 f8 b- j

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

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

再看另外一个例子:

//在线程函数中定义CString局部变量并进行操作5 ^, U4 R  T1 I
$ M" a# [8 m3 V5 P2 P$ N/ ~: _
CString strstate;  " e8 \* W7 c2 F, i) O3 \% O  `$ O
strstate.Format("正在解密,请稍后... (共 %d 张地图)",p->m_countmap);

* w1 z. O8 I, v3 L- v3 m

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

微信图片_20200805210541.png

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

CWin32HeapstringHeap(HEAP_NO_SERIALIZE, 0, 0 );
- k' O( T: t6 m/ g8 u5 GCAtlStringMgr stringMgr(&stringHeap );
" G6 Z0 L  \% q$ w7 w7 @& mCString strstate(&stringMgr );
- j9 s3 x7 G& a+ \: `2 Jstrstate.Format("正在解密,请稍后... (共 %d 张地图)",p->m_countmap);

/ j2 U4 S+ A6 F9 R( ^9 }' e: W' [$ {

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

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)));! o( N! o! U/ `  p
1 Z8 |" Z7 a. _7 d, J
2AFX_STATICCFixedAlloc _afxAlloc128(ROUND4(129*sizeof(TCHAR)+sizeof(CStringData)));5 {2 J% M# ]+ s0 e2 m  B. F) L8 c

, B  w0 v0 S( \, ]3AFX_STATICCFixedAlloc _afxAlloc256(ROUND4(257*sizeof(TCHAR)+sizeof(CStringData)));
! Y% j8 N, |3 o1 x7 o$ m# s# u0 f/ E* _0 x
4AFX_STATICCFixedAlloc _afxAlloc512(ROUND4(513*sizeof(TCHAR)+sizeof(CStringData)));

: V, n! @  Z# ]& P1 s  p

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

  • 当在多线程中使用共享 CString 变量时,遇到异常问题最简单的解决办法就是不用 CString改为 char * 或 char[] 。
  • 当在线程中使用局部变量时候,可以使用 ATL 提供的 CAtlStringMgr 类对字符串数据进行自定义内存分配,保证线程的中 CString 变量的安全性。
    $ R/ c1 W- x* \- L& w9 J0 Z$ D. D
// Declare a non-thread-safe heap just for this thread
5 t% O# |) S* ^9 m/ UCWin32HeapstringHeap( HEAP_NO_SERIALIZE, 0, 0 );! V6 L- V4 X" s* O! ~. W% s: N
// Declare a string manager that uses the thread's heap) g3 E2 x* g, D' r
CAtlStringMgrstringMgr( &stringHeap );
9 A1 l4 _# c) P+ z0 O$ @3 M//do anything you want5 ^) B) f# @2 `( F1 g  B

例如:

{
! g- L, T  z; r# z8 H    CString strstate(&stringMgr );% h. M6 w. k5 c7 |9 q( q
    strstate.format(……);
5 |. V2 G0 i# H) H; V7 G5 Q% u}
赞 收藏 评论0 发布时间:2020-8-5 21:06

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版