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

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

[复制链接]
gaosmile 发布时间:2020-7-7 18:24
1 问题描述

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

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

typedef struct _SYSTEMINFO_CONTEXT/ Y6 e' ]1 o0 o
{
. X1 c) S9 f3 D( w; y     CString szMac;  //mac地址, t2 P& @* P6 w" l9 R
     CString szName;       //工程名称9 d' F6 F7 L( h

  H4 C: w: i4 Z. X     //构造) P& n2 o) [$ J* R
     _SYSTEMINFO_CONTEXT()% H* X2 X$ h. @4 @6 L9 B$ t
    {' l+ O9 ]/ _. `4 {
       tAutoTime=COleDateTime::
3 y* o. M. {" y1 n" O' f% H! ~      GetCurrentTime();//初始化当前时间
5 u. y- E1 S$ ]  d& ^: z- ?    }3 f. M& H6 f; D0 f* ?3 b
9 C8 r! J0 @' K* U
    //析构
: F9 ~* |& p" }' t    ~_SYSTEMINFO_CONTEXT()9 U. S: t7 b0 u+ W
    {  U$ I- M& o1 ~3 X
   
8 N# u4 t/ ~3 E. n, G    }
% P5 I" E( [) j8 k+ E}SYSTEMINFO_CONTEXT,*PSYSTEMINFO_CONTEXT;
' h) r. `- n: c$ d, g+ Z9 E0 d% y. [: r% n8 D- Z7 t
strname = pSystemInfo-> szName;      //异常报错地方

6 F$ e6 S' l1 d& \! P4 n3 K

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

const CString& CString:perator=(const CString& stringSrc)
9 [. `- T) R3 k2 I& K% @  b0 E{: P+ `: n0 ~' g+ P
  if (m_pchData != stringSrc.m_pchData)
- o/ V! g3 f  K6 C6 n7 Z  {
/ q0 c) F: {0 V5 u0 }9 D: x! V    if ((GetData()->nRefs < 0 && GetData() != _afxDataNil) ||
; P' D5 |9 @: ?/ I  p2 M2 [      stringSrc.GetData()->nRefs < 0)
4 o( E( i( d/ f! `5 f8 g# ?  X    {8 u+ J" z. g  N5 z
      // actual copy necessary since one of the strings is locked
/ W* D# b" {1 s& g+ m- w      AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);7 u  N- i, f. @* o. q
    }2 h$ G+ O8 @- \" ]. V
    else
; V' ?: y, [- W( K% f+ X0 _    {3 R  f" r5 T3 n3 f" t6 x, S& _9 I' |
      // can just copy references around! h' {7 h* U  P" u# C. F
      Release();9 B$ W) s' O4 _. q$ I$ D+ L
      ASSERT(stringSrc.GetData() != _afxDataNil);
& i- h. Y  y2 x9 k! ^- v      m_pchData = stringSrc.m_pchData;# K/ G& q1 D) \: H5 r  ^8 n4 S% I
      InterlockedIncrement(&GetData()->nRefs);% o1 \3 U0 O# u1 b
    }, O/ \3 D7 ~% c; E& e& N( n
  }
7 ?4 X6 Q+ g* E* M0 j% c  return *this;
4 X* _/ V* o! b+ n}
* h: d5 W# Q; O4 }9 s9 h$ }+ w
2 问题分析

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

if(pSystemInfo-> szMac == 这个设备的实际mac地址): w, Y3 m0 w" X
{
5 I' n4 H* S7 f    Break;
2 y9 _# n8 }1 b. Z}

! i1 F8 M2 ?) T8 N* _6 ~

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

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

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

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

inline const CString& CString:perator=(const CString& stringSrc)
5 Z( m' [" d; ]3 j' K# X{
$ v; |" R) h, [6 I; E, A    if (m_pchData != stringSrc.m_pchData) //防止自拷贝8 _& i% p1 E3 S" }% O# n
    {& a; ~1 }) m  j5 n6 g+ }  g/ N
        //如果目标对象的数据空间已经分配,而且没有别的CString实例指向它,则把stringSrc的数据拷贝过来;- J7 O' [4 e' h4 g1 I& J
        //或者,如果stringSrc的数据的引用次数小于零,则把它的数据拷贝过来
% @7 E9 e. D! _" E- T( N        if ((GetData()->nRefs < 0 && GetData() != afxDataNil) ||
% f$ G7 O+ q8 |            stringSrc.GetData()->nRefs < 0)
' T. _* W- B2 p6 J6 r- M- C        {# W! H! B* t* T* @' Y/ t
            // actual copy necessary since one of the strings is locked
3 j4 Y) E7 L& Q5 ]5 H8 @            AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);
6 F1 d. [( }5 ?0 ~1 }        }
4 \7 i( v8 |9 p5 I, X3 h        else
! d5 x) b' u6 u5 B1 M) Q+ ^. |        {0 K4 S, X  u* p* \. Z4 y
            //其他情况,就先自己的数据空间释放掉,然后再指向stringSrc的数据空间
2 E/ U, n3 |: q* `9 e            // can just copy references around1 z2 h6 L3 @7 @3 L6 v+ B8 v. S! s
            Release();* G# c, D; l" `3 s
            ATLASSERT(stringSrc.GetData() != afxDataNil);
7 `  [" A" Y% @4 q. D            m_pchData = stringSrc.m_pchData;# @4 ]2 d2 l! E: i
            InterlockedIncrement(&GetData()->nRefs);  //引用次数增1,这里为了防止多线程情况下出错,使用了原子性自增
' V9 q: B+ o& K/ `+ a        }/ O" G8 L( A7 a, d
    }
2 |" |  V; q% d) X. K( [    return *this;* K8 J$ I) V; k: [
}
" a* v9 V# S+ ~

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

比如:

CString str1="aaa";
) o7 X& w3 E; b" F4 _  u& ~& l
" X. E0 q( [  O. \, fCString str2=str1;   //注意,这时候str2并没有调用 new ,而是使用str1的引用同时,str1中保存的引用记数++。
, D! [* \0 _+ o" f, ^$ A, `3 u1 j+ v
str2="abcd1234";     //好了,现在str2才分配内存,str1引用记数--。这同时也是为什么str2.Empty()就没有内存泄露的原因。因为str1的引用记数也--了。
8 i: f7 r0 H3 ~5 d, J% ]! l; E" l

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

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

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

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

修改后的结构体定义:

typedef struct _SYSTEMINFO_CONTEXT
7 y4 T* y- `* T{
/ Y; {& Q$ ~+ s! [" N6 S2 M  Char szMac[20];       //mac地址,对应macAddr
* X3 @' o( W0 F5 q5 F  Char szName[50];     //工程名称
3 x  G4 Y5 q. s0 U6 O9 k
6 H6 U1 o7 n+ v1 _  //构造
! G' D* S1 N0 _. Y* B( f$ J. O  _SYSTEMINFO_CONTEXT()& U1 x0 d2 `; W! v
  {; U9 M5 M. C- w3 a
    tAutoTime = COleDateTime::GetCurrentTime();//初始化当前时间5 H# G) b2 {: [9 s( M# I/ }
  }
8 l! m! J6 l$ |9 @5 K6 X$ l+ w  //析构% J1 V; s& h! X+ L" T
  ~_SYSTEMINFO_CONTEXT()
7 s2 |3 M4 Z7 T7 {7 S! s  {2 B! U3 c, ?9 x$ }
  # f7 }: {+ o  u6 h
  }
) X) V- h% m+ I9 _; H0 U1 s}SYSTEMINFO_CONTEXT,*PSYSTEMINFO_CONTEXT;! W5 A( M, Z  {8 W4 ]
- W& D0 G7 a3 T, D* {& N
strncpy(pSystemInfo->szName,(LPCTSTR)strName,sizeof(pSystemInfo->szName)); //这样就不会出错了

  d) s$ b) [7 F( I9 D1 E

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

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

再看另外一个例子:

//在线程函数中定义CString局部变量并进行操作
6 [) b7 b3 N# B+ l# `
; D0 K  T7 c& V0 h2 U7 XCString strstate;  * `( W+ `. A  H. @+ L% H; P
strstate.Format("正在解密,请稍后... (共 %d 张地图)",p->m_countmap);

7 N2 ]! c& d- c) }* T: I. N( I  d1 |

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

微信图片_20200707182244.png

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

CWin32HeapstringHeap(HEAP_NO_SERIALIZE, 0, 0 );
' e/ L) |7 y; }5 \* g" ~CAtlStringMgr stringMgr(&stringHeap );
" K& u; K5 a- K" A$ X: }CString strstate(&stringMgr );3 {5 T- h* `8 B& Z
strstate.Format("正在解密,请稍后... (共 %d 张地图)",p->m_countmap);

' C+ ?: C0 q" A; j7 X+ ^/ s

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

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)));& u! L9 E& z# a/ Y
: i' |  L' j) b3 W/ ~
2AFX_STATICCFixedAlloc _afxAlloc128(ROUND4(129*sizeof(TCHAR)+sizeof(CStringData)));3 V6 n  r0 ~" G. W3 S

4 M5 g- q. _* V( I- G( X# V7 e3AFX_STATICCFixedAlloc _afxAlloc256(ROUND4(257*sizeof(TCHAR)+sizeof(CStringData)));$ P/ I% e8 {! w2 y3 Y; m- H/ u, V: B
4 L& h( b, r: ]% [0 ^3 @
4AFX_STATICCFixedAlloc _afxAlloc512(ROUND4(513*sizeof(TCHAR)+sizeof(CStringData)));
* B; [* o. c( @% Q! A! c

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

  • 当在多线程中使用共享 CString 变量时,遇到异常问题最简单的解决办法就是不用 CString改为 char * 或 char[] 。
  • 当在线程中使用局部变量时候,可以使用 ATL 提供的 CAtlStringMgr 类对字符串数据进行自定义内存分配,保证线程的中 CString 变量的安全性。
    8 [% u/ u. ^  p: {# e. P
// Declare a non-thread-safe heap just for this thread
7 a6 _3 i1 m$ r/ y5 uCWin32HeapstringHeap( HEAP_NO_SERIALIZE, 0, 0 );
& R% b* s" G; e9 u// Declare a string manager that uses the thread's heap
" [6 C( S5 F% }  A7 E2 `, zCAtlStringMgrstringMgr( &stringHeap );
4 G- F+ n1 d& r- n1 Q//do anything you want* h# V' S/ K9 I# H$ S. o  R$ q

例如:

{
; n8 G, J% f3 J/ D) @& G    CString strstate(&stringMgr );
2 F$ w4 Q5 V& ]" C    strstate.format(……);; r: Y! u, u- D9 \
}
. u& T; J' t! w4 w' G! ~6 j
赞 收藏 评论0 发布时间:2020-7-7 18:24

举报

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