青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

elva

[轉]ACE的ACE_Logging_Strategy類中的一個多線程安全問題的發現及解決過程

/************************************
*              版權聲明
*   本文為本人原創,本人擁有此文的版權。鑒于本人持續受益于開源軟件社區,
* 本人聲明:任何個人及團體均可不受限制的轉載和復制本文,無論是否用于盈利
* 之目的,但必須在轉載及復制時同時保留本版權聲明,否則為侵權行為,本人保
* 留追究相應法律責任之權利。
*                 speng2005@gmail.com
*                      2007-12
************************************/  

    近日在使用ACE進行開發的工作中遇到一個導致程序崩潰的問題。我們是在Linux平臺上使用ACE 5.5.1以及g++ 4.1.2進行開發的。
    問題是這樣的:我們的程序中有多個線程并發調用ACE_Log_Msg::log()方法進行日志輸出操作,這些日志將被輸出到同一個位于本地磁盤上的日志文件中。但為防止日志文件長度增長過大,我們使用了ACE_Logging_Strategy類來進行定時的日志文件大小檢查和文件切換。這種方式在一般情況下運行良好,但是在大規模壓力測試中,程序常常因訪問非法內存而崩潰。發生崩潰時的調用棧總是這樣的:

(gdb) bt
#0  0xb7c1bb4a in memcpy () from /lib/tls/i686/cmov/libc.so.6
#1  0xb7d9c006 in std::basic_streambuf<char, std::char_traits<char> >::xsputn () from /usr/lib/libstdc++.so.6
#2  0xb7d66d59 in std::basic_filebuf<char, std::char_traits<char> >::xsputn () from /usr/lib/libstdc++.so.6
#3  0xb7d911df in std::operator<< <std::char_traits<char> > () from /usr/lib/libstdc++.so.6
#4  0xb7ed8f15 in ACE_Log_Record::print (this=0x2befe228, host_name=0x0, verbose_flag=4, s=@0x80521c8) at Log_Record.cpp:302
#5  0xb7ed70a6 in ACE_Log_Msg::log (this=0x8588a58, log_record=@0x2befe228, suppress_stderr=0) at Log_Msg.cpp:2197
#6  0xb7ed749b in ACE_Log_Msg::log (this=0x8588a58, format_str=0xb7f9ef3d "", log_priority=LM_TRACE, argp=0xb7f9ef3c "") at Log_Msg.cpp:2047
#7  0xb7ed8055 in ACE_Log_Msg::log (this=0x8588a58, log_priority=LM_TRACE, format_str=0xb7f9ef2a "%*s(%t) leaving %s") at Log_Msg.cpp:961
#8  0xb7f9d9b3 in ~JAWS_Trace (this=0x2befe338) at Trace.cpp:139
#9  0xb70ff7b3 in StorageWriteState::service (this=0x85aaa28, ec=0x291b4d70, data=0x291b4d70) at StorageWriteState.cpp:63
#10 0xb7f94739 in JAWS_Protocol_Handler::service (this=0x291b4d70) at Protocol_Handler.cpp:38
#11 0xb7f86e0e in JAWS_Concurrency_Impl::svc (this=0x8057a34) at Concurrency.cpp:38
#12 0xb7f1c794 in ACE_Task_Base::svc_run (args=0x8057a34) at Task.cpp:258
#13 0xb7f1d108 in ACE_Thread_Adapter::invoke_i (this=0x8057ca8) at Thread_Adapter.cpp:151
#14 0xb7f1d2d6 in ACE_Thread_Adapter::invoke (this=0x8057ca8) at Thread_Adapter.cpp:95
#15 0xb7eb0c51 in ace_thread_adapter (args=0x8057ca8) at Base_Thread_Adapter.cpp:137
#16 0xb7e04240 in start_thread () from /lib/tls/i686/cmov/libpthread.so.0
#17 0xb7c7a4ae in clone () from /lib/tls/i686/cmov/libc.so.6

    因為多次崩潰的調用棧都一樣,這說明這個問題是一個比較確定的可再現bug,這為找到問題原因并最后解決提供了有利條件。
    問題出現后,我們首先定位問題的原因與應用程序的日志輸出有關系。經過代碼復查,沒有發現這方面的任何問題。接下來,我們只好懷疑ACE的日志相關類的源碼存在問題。我們又徹底閱讀了與日志輸出有關的類的源代碼:ACE_Log_Msg,ACE_Log_Record,ACE_Logging_Strategy,包括ACE_Logging_Strategy::handle_timeout()方法的實現代碼,重點關注代碼中關于內存操作和多線程安全互斥的操作,依然沒有發現可疑的地方。這下問題就復雜了,總不能懷疑C++標準庫的流類庫有問題吧?這不太可能,但是問題的直接爆發點是出在C++標準庫里的,所以只能從標準庫出發尋找線索。有過一定程序開發尤其是c/c++程序開發經驗的人都知道,一些復雜問題直接暴露出來的現象都是問題的表象,根本的原因可能跟表象差的很遠,但是順著表象提供的線索去尋找根源畢竟是解決問題的正道。于是我們開始從程序崩潰點尋找問題的癥結。
    首先,我們分析程序為什么在調用memcpy()這個標準c的庫函數時發生崩潰的。沒有源代碼,怎么分析?學一點匯編語言的基礎知識吧,這是對一個資深程序員的必要的技術要求。在gdb里使用如下命令:
(gdb) set disassembly-flavor intel
(gdb) disass
gdb就給出了memcpy()函數的匯編指令:
Dump of assembler code for function memcpy:
0xb7c1bb30 <memcpy+0>:  mov    ecx,DWORD PTR [esp+12]
0xb7c1bb34 <memcpy+4>:  mov    eax,edi
0xb7c1bb36 <memcpy+6>:  mov    edi,DWORD PTR [esp+4]
0xb7c1bb3a <memcpy+10>: mov    edx,esi
0xb7c1bb3c <memcpy+12>: mov    esi,DWORD PTR [esp+8]
0xb7c1bb40 <memcpy+16>: cld   
0xb7c1bb41 <memcpy+17>: shr    ecx,1
0xb7c1bb43 <memcpy+19>: jae    0xb7c1bb46 <memcpy+22>
0xb7c1bb45 <memcpy+21>: movs   BYTE PTR es:[edi],BYTE PTR ds:[esi]
0xb7c1bb46 <memcpy+22>: shr    ecx,1
0xb7c1bb48 <memcpy+24>: jae    0xb7c1bb4c <memcpy+28>
0xb7c1bb4a <memcpy+26>: movs   WORD PTR es:[edi],WORD PTR ds:[esi]
0xb7c1bb4c <memcpy+28>: rep movs DWORD PTR es:[edi],DWORD PTR ds:[esi]
0xb7c1bb4e <memcpy+30>: mov    edi,eax
0xb7c1bb50 <memcpy+32>: mov    esi,edx
0xb7c1bb52 <memcpy+34>: mov    eax,DWORD PTR [esp+4]
0xb7c1bb56 <memcpy+38>: ret
再使用如下命令:
(gdb) p $eip
gdb就打印出:
$1 = (void (*)(void)) 0xb7c1bb4a <memcpy+26>
這就是gdb精確地告訴我們程序是在執行“movs WORD PTR es:[edi],WORD PTR ds:[esi]”指令時發生了錯誤,操作系統產生了“signal 11, Segmentation fault”信號,也就是說程序訪問了非法的內存地址。我們觀察movs指令,很有可能edi寄存器中的地址是非法的,于是在gdb中輸入命令:
(gdb) p/x $edi
gdb就打印出:
$2 = 0x0
這就證明的確是因為edi寄存器被賦予了NULL指針導致問題的出現的。有經驗的程序員立刻可以意識到進一步的問題癥結是調用棧中的上層函數向memcpy()傳遞了錯誤的目標地址參數進而導致程序崩潰的。觀察調用棧,位于memcpy()之上的3層函數都是C++標準庫的流操作。我想絕大多數程序員都是只知道怎么正確使用標準流類庫,而未研究過其是如何實現的吧?我們也是。但如果要解決問題,就必須弄清楚其內部實現。好在標準流類庫都是模板實現,有源碼可讀。不讀不知道,標準流類庫的實現代碼還是很復雜的,晦澀難懂。可是我們也不是無目的的閱讀全部代碼,我們的近期目標是找到傳遞給memcpy()的指針是從哪來的,以及這個地址是如何被設成NULL的。我們找到最直接調用memcpy()的C++標準庫源碼(看起來在這里我只寫了一句話,但是的確費了很多腦細胞才確定這里的源代碼跟調用棧里顯示的那些被調用的函數是同一個版本的,確定這一點很重要):
template<typename _CharT, typename _Traits>
streamsize
basic_streambuf<_CharT, _Traits>::xsputn(const char_type* __s, streamsize __n)
{
      streamsize __ret = 0;
      while (__ret < __n)
     {
      const streamsize __buf_len = this->epptr() - this->pptr();
      if (__buf_len)
        {
          const streamsize __remaining = __n - __ret;
          const streamsize __len = std::min(__buf_len, __remaining);
          traits_type::copy(this->pptr(), __s, __len);
          __ret += __len;
          __s += __len;
          this->pbump(__len);
        }

      if (__ret < __n)
        {
          int_type __c = this->overflow(traits_type::to_int_type(*__s));
          if (!traits_type::eq_int_type(__c, traits_type::eof()))
        {
          ++__ret;
          ++__s;
        }
          else
        break;
        }
    }
      return __ret;
}
   上面紅色顯示的部分就是對memcpy()函數的調用語句。根據前面的分析,我們知道出問題的時候“this->pptr()”必然返回了NULL,所以memcpy()執行時才會出錯。“this->pptr()”是內聯方法:
char_type* pptr() const { return _M_out_cur; }

這也就是說出問題的時候basic_streambuf<>對象的_M_out_cur成員被設成了NULL,所以才會引發連鎖反應。似乎我們快要找到問題根源了,但是,問題沒這么簡單。因為我們分析了上面的basic_streambuf<>::xsputn()源碼,發現只有__buf_len大于0的時候程序才可能會走到memcpy()函數調用中,而在我們的程序出問題的時候,“this->epptr()”居然返回的也是NULL!“this->epptr()”也是內聯方法:
char_type* epptr() const { return _M_out_end; }
這也就是說出問題的時候basic_streambuf<>對象的_M_out_end成員也被設成了NULL。這個時候計算得到的__buf_len值應該等于0,但程序怎么會進入到對memcpy()函數的調用中呢?對此我們百思不得其解,但有一個信念:CPU保證會按程序的機器指令代碼行事,不會亂來的。于是,我們分析在我們的應用程序是多線程的情形下,合理解釋只能是:當前線程執行到__buf_len的計算的代碼時返回的長度還是大于0的,于是會有后來的對memcpy()的調用;而同時可能有另外一個未做多線程同步的線程卻修改了“this->pptr()”及“this->epptr()”對應的指針值為NULL,這樣當前線程執行到memcpy()時就會出錯。分析到此,我們又有了新方向。下一步的目標就是確定哪個線程中的什么地方的代碼有可能會將basic_streambuf<>對象的_M_out_cur成員及_M_out_end成員設成NULL。說起來容易做起來難啊!我們閱讀了大量源代碼,隱約發現有數個地方可能會修改_M_out_cur成員值為NULL,但細細推敲起來都不應該導致出現上述問題。我們陷入僵局了。
     所以我這里再次強調,一個資深的c/c++程序員應該懂得一些匯編語言的基礎知識并掌握至少一個匯編調試器的使用技巧。在這里,gdb就夠用了。因為gdb支持“內存讀寫斷點”的設置。內存讀寫斷點是CPU為調試軟件設置的專用中斷器
,它能夠監視指定的內存段范圍內是否有內存讀,或內存寫,或內存讀寫操作,如果有則產生中斷,并由調試器接管程序的執行。在gdb里就可以設置這樣的斷點來監視basic_streambuf<>對象的_M_out_cur成員的值何時被修改從而使我們知道在哪個線程執行到哪個函數時修改了_M_out_cur成員的值,以便我們分析導致程序崩潰問題的根源。但困難在于,設置這樣的斷點時需要指定一個內存地址,也就是我們要監視的basic_streambuf<>對象的_M_out_cur成員的內存地址,這個如何確定呢?
     還是匯編!在我們的應用程序中,尋找
_M_out_cur成員的內存地址的思路是這樣的:我們觀察前面給出的調用棧,可以看到在第4層函數ACE_Log_Record::print()中的最后一個參數的是"s=@0x80521c8",結合ACE源碼知道參數s的類型是ostream<> &,這其實就是告訴我們一個ostream<>對象的地址在0x80521c8。而實際上這個ostream<>對象的子類型就是ofstream<>,也就是說在我們的應用程序中多個線程操作著地址在0x80521c8的同一個ofstream<>對象進行日志輸出操作。閱讀C++標準庫源碼我們知道,如果知道了一個ofstream<>對象的首地址,就可以計算其_M_filebuf對象成員的首地址,該成員的類型為basic_filebuf<>;知道了一個basic_filebuf<>對象的首地址,就可以知道其繼承自基類basic_streambuf<>的M_out_cur成員的地址了。這些地址的計算都是根據一個原理:對象的每個成員的首地址相對于該對象的首地址的偏移量是一個在編譯期就確定的常量,對象的首地址加上這個特定的偏移量而得到的值就是對應成員的首地址。那么這些偏移量具體數值如何知道?匯編吧!我們寫了一個簡單的測試程序專門用于獲取我們所關心的兩個偏移量的數值。測試程序是這樣的:
#include <iostream>
#include <fstream>
using namespace std;
void work(ostream & s)
{
        cout<<"pos="<<s.tellp()<<endl;
        s<<"hello,world!"<<endl;
        cout<<"pos="<<s.tellp()<<endl;
        return;
}

int main()
{
        ofstream out;
        out.open("123.txt");
        if( !out.is_open() )
        {
                cout<<"open file failed!"<<endl;
                return 0;
        }

        work(out);
        return 0;
}
    經過一翻外科手術式的剖析,我們找到了這兩個偏移量的數值(雖然這里只寫了一句話,但這絕不僅僅是一句話),并且弄清楚了一個ofstream<>對象的各個成員在內存中的布局(僅在g++ 4.1.2編譯的目標代碼中進行了驗證)。如果一個ofstream<>對象的首地址在0x8049240,則此對象的內存布局為:
address   :   hex value dump
0x8049240 :   0xb7f6a9cc      0xb7f6aac8      0x0804a170      0x0804a170
0x8049250 :   0x0804a170      0x00000000      0x00000000      0x00000000
0x8049260 :   0xb7f6dbbc      0x00000000      0x00000000      0x00000000
0x8049270 :   0x00000000      0x00000000      0x00000000      0x0804a008
0x8049280 :   0x00000001      0x00000030      0x00000000      0x00000000
0x8049290 :   0x00000000      0x00000000      0x00000000      0x00000000
0x80492a0 :   0x0804a170      0x00002000      0x00000001      0x00000000
0x80492b0 :   0x00000000      0x00000000      0xb7f6df88      0x00000000
0x80492c0 :   0x00000000      0x00000000      0x00000000      0xb7f6a9e0
0x80492d0 :   0x00000006      0x00000000      0x00001002      0x00000000
0x80492e0 :   0x00000000      0x00000000      0x00000000      0x00000000
0x80492f0 :   0x00000000      0x00000000      0x00000000      0x00000000
0x8049300 :   0x00000000      0x00000000      0x00000000      0x00000000
0x8049310 :   0x00000000      0x00000000      0x00000000      0x00000000
0x8049320 :   0x00000000      0x00000000      0x00000000      0x00000000
0x8049330 :   0x00000008      0x080492f0      0xb7f6dbbc      0x00000000
0x8049340 :   0x00000000      0x08049244      0xb7f6dd40      0xb7f6df80
0x8049350 :   0xb7f6df78      0x00000000      0x00000000      0x00000000
..................
在上面,我們知道0xb7f6aac8開始的內存就是ofstream<>對象所聚合的basic_filebuf<>對象成員_M_filebuf的內存映像;而0x00000000就是該_M_filebuf對象的M_out_cur成員的內存映像,也就是說我們可以直接根據ofstream<>對象的首地址然后加上偏移量0x18而得到M_out_cur成員的內存地址。關于ofstream<>對象及basic_filebuf<>對象在內存中的布局我們還研究了更多的東西。例如,0xb7f6a9cc指向的地址就是ofstream<>類的虛擬函數表的首地址;0xb7f6aac8指向的地址就是basic_filebuf<>類的虛擬函數表的首地址;0x00000030就是basic_filebuf<>類的_M_mode成員的值,其值等價于(ios_base::out | ios_base::trunc);0x0804a170指向的地址就是basic_filebuf<>對象內部的一個真正內部緩沖區的首地址,對應于該對象的_M_buf成員;0x00002000則指出這個緩沖區的長度是8192字節,對應于該對象的_M_buf_size成員。
     有了上面得到的內存地址計算方法,下一步就是在我們的應用程序中獲得
ofstream<>對象的首地址了。這有很多方法,我們可以直接在gdb中找到ACE_Log_Msg::log()方法的源碼并設置斷點,然后啟動程序,待斷點激活后,在gdb中打印調用棧而取得ofstream<>對象的首地址。這里我們使用了另外一種方法:我們根據前面給出的調用棧中的第1層的函數返回地址0xb7d9c006,在gdb中進行反匯編:
(gdb) disass 0xb7d9c006
Dump of assembler code for function _ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci:
0xb7d9bfa0 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+0>: push   ebp
0xb7d9bfa1 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+1>: mov    ebp,esp
0xb7d9bfa3 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+3>: push   edi
0xb7d9bfa4 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+4>: push   esi
0xb7d9bfa5 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+5>: push   ebx
0xb7d9bfa6 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+6>: sub    esp,0x2c
0xb7d9bfa9 <_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci+9>: mov    eax,DWORD PTR [ebp+16]
......
我們可以看到,gdb打印出了調用棧中第1層的函數的C++名稱經過編碼后所對應的C名稱字符串為“_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci”,這個C名稱與其對應的C++名稱“std::basic_streambuf<char, std::char_traits<char> >::xsputn”是等價的。接下來,我們啟動一個新的gdb程序,并在gdb中加載應用程序文件,在啟動應用程序運行之前,先設置斷點:
(gdb) break _ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci
這就是要在前述的調用棧的第1層函數入口處設置斷點。然后我們啟動應用程序的執行并激活日志輸出操作,當第一條日志輸出時,剛才所設斷點就激活了,gdb將中斷程序的執行,此時我們打印調用棧,gdb將輸出與前述類似的調用棧,此時我們可以輕松地從調用棧中取得ofstream<>對象的首地址。我們將這個地址加上偏移量0x18,就得到了我們要監視的內存地址。在我們的應用程序中計算得到的地址是0x80521e0。
    有了要監視的內存地址以后,我們就可以在gdb里設置“內存寫”斷點了,輸入如下命令:
(gdb) watch *(int *)0x80521e0
Hardware watchpoint 1: *(int *) 134554080
這表明一個“內存寫”斷點設置成功。接下來讓gdb執行c命令繼續執行應用程序,就可以開始監視basic_filebuf<>對象的M_out_cur成員何時被修改成NULL了。當斷點激活時,gdb打?。?/span>
Hardware watchpoint 3: *(int *) 134554080

Old value = 134555784
New value = 0
0xb7ce3088 in std::basic_filebuf<char, std::char_traits<char> >::_M_seek () from /usr/lib/libstdc++.so.6
此時打印調用棧,gdb顯示:
(gdb) bt
#0  0xb7ce3088 in std::basic_filebuf<char, std::char_traits<char> >::_M_seek () from /usr/lib/libstdc++.so.6
#1  0xb7ce4e2a in std::basic_filebuf<char, std::char_traits<char> >::seekoff () from /usr/lib/libstdc++.so.6
#2  0xb7d0be5d in std::ostream::tellp () from /usr/lib/libstdc++.so.6
#3  0xb7e560a4 in ACE_Logging_Strategy::handle_timeout (this=0xb7f24ac0) at Logging_Strategy.cpp:404
#4  0xb7e24cb1 in ACE_Event_Handler_Handle_Timeout_Upcall<ACE_Recursive_Thread_Mutex>::timeout (this=0x80515d0,
    timer_queue=@0x8051528, event_handler=0xb7f24ac0, act=0x0, recurring_timer=1, cur_time=@0x2ccff2e8)
    at /data/jinwei/svn_root/ireport/common/ace/ACE_wrappers/ace/Timer_Queue_T.cpp:408
#5  0xb7e4220d in ACE_Dev_Poll_Reactor::dispatch_timer_handler (this=0x8051470, guard=@0x2ccff394)
    at /data/jinwei/svn_root/ireport/common/ace/ACE_wrappers/ace/Timer_Queue_T.inl:170
#6  0xb7e4492c in ACE_Dev_Poll_Reactor::dispatch (this=0x8051470, guard=@0x2ccff394) at Dev_Poll_Reactor.cpp:1230
#7  0xb7e44a56 in ACE_Dev_Poll_Reactor::handle_events_i (this=0x8051470, max_wait_time=0x0, guard=@0x2ccff394)
    at Dev_Poll_Reactor.cpp:1211
#8  0xb7e44b21 in ACE_Dev_Poll_Reactor::handle_events (this=0x8051470, max_wait_time=0x0) at Dev_Poll_Reactor.cpp:1166
#9  0xb7e82f02 in ACE_Reactor::run_reactor_event_loop (this=0x80518f0, eh=0) at Reactor.cpp:233
#10 0xb7f0dbe7 in JAWS_Event_Dispatcher::JAWS_Event_Dispatcher_Reactor_Event_Loop () at Event_Dispatcher.cpp:32
#11 0xb7e99108 in ACE_Thread_Adapter::invoke_i (this=0x85b3918) at Thread_Adapter.cpp:151
#12 0xb7e992d6 in ACE_Thread_Adapter::invoke (this=0x85b3918) at Thread_Adapter.cpp:95
#13 0xb7e2cc51 in ace_thread_adapter (args=0x85b3918) at Base_Thread_Adapter.cpp:137
#14 0xb7d80240 in start_thread () from /lib/tls/i686/cmov/libpthread.so.0
#15 0xb7bf64ae in clone () from /lib/tls/i686/cmov/libc.so.6
此時,我們
終于有了重大發現!原來ACE_Logging_Strategy::handle_timeout()方法中執行了如下代碼:
int
ACE_Logging_Strategy::handle_timeout (const ACE_Time_Value &,
                                      const void *)
{
#if defined (ACE_LACKS_IOSTREAM_TOTALLY)
  if ((size_t) ACE_OS::ftell (this->log_msg_->msg_ostream ()) > this->max_size_)
#else
  if ((size_t) this->log_msg_->msg_ostream ()->tellp () > this->max_size_)
#endif /* ACE_LACKS_IOSTREAM_TOTALLY */
    {
      // Lock out any other logging.
      if (this->log_msg_->acquire ())
        ACE_ERROR_RETURN ((LM_ERROR,
                           ACE_LIB_TEXT ("Cannot acquire lock!")),
                          -1);
        .....
     }
     .....
}
可以看到,對ostream<>::tellp()方法的調用居然會導致ofstream<>對象內部的basic_streambuf<>對象成員的M_out_cur成員的值被修改為NULL!而ACE_Logging_Strategy::handle_timeout()方法在調用ostream<>::tellp()方法時居然沒有先獲得鎖!知道這個根本原因了,問題就好解決了,至于如何修改ACE_Logging_Strategy::handle_timeout()的代碼,明眼人一看便知了。我們修改代碼后并進行了長期的壓力測試的驗證,最后確認了這就是問題的根源。
    這個問題被解決后,我們查看了ACE最新版本5.6.2中的代碼,發現源碼中仍然存在此問題。于是我們給ACE開發社區發了一個帖子:A bug about ACE_Logging_Strategy 來報告這個ACE中的bug,算是為ACE開發社區做個貢獻吧。
  細細推敲起來,開發ACE的大牛們為什么會犯這種有點低級的錯誤呢?而且這么長時間都沒有發現這個問題?也許他們沒有進行過像我們這么大壓力的長期測試吧?也許他們也和我們當初所認為的一樣,以為ostream<>::tellp()即便不是一個const方法,也應該是個“準”const方法吧?然而事實卻并非如此。其實從C++程序員的角度來說,我們的確需要一個const版本的ostream<>::tellp()接口,不知道將來C++標準會不會在這個問題上有所改變。對于這個訴求,不只我們有,這里也有: why is ostream::tellp not const。

 

posted on 2008-07-30 17:45 葉子 閱讀(1669) 評論(4)  編輯 收藏 引用 所屬分類: C\C++

Feedback

# re: [轉]ACE的ACE_Logging_Strategy類中的一個多線程安全問題的發現及解決過程 2008-07-31 18:01 亨德列克

牛。。。  回復  更多評論   

# re: [轉]ACE的ACE_Logging_Strategy類中的一個多線程安全問題的發現及解決過程[未登錄] 2008-07-31 21:37 cppexplore

來拜拜牛人!
太牛了?。。。。。。。。。。。。。?!  回復  更多評論   

# re: [轉]ACE的ACE_Logging_Strategy類中的一個多線程安全問題的發現及解決過程 2008-09-15 13:02 hello

非常好,謝謝!  回復  更多評論   

# re: [轉]ACE的ACE_Logging_Strategy類中的一個多線程安全問題的發現及解決過程 2009-07-15 10:56 tmp

正好工作中碰到了這個bug,找了好久好久啊  回復  更多評論   

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            亚洲神马久久| 久久亚洲国产精品一区二区| 久久伊人亚洲| 亚洲国产乱码最新视频| 老司机一区二区| 亚洲电影在线看| 欧美成人一品| 另类综合日韩欧美亚洲| 在线观看国产日韩| 亚洲盗摄视频| 久久综合色婷婷| 91久久夜色精品国产网站| 欧美黄色日本| 欧美日韩成人一区二区| 在线一区二区三区做爰视频网站| 奶水喷射视频一区| 久久精品国产精品亚洲精品| 韩国成人福利片在线播放| 久久久之久亚州精品露出| 欧美在线免费观看| 亚洲国产日韩综合一区| 亚洲黄色成人网| 麻豆9191精品国产| 亚洲视频欧美在线| 亚洲欧美不卡| 亚洲国产精品久久91精品| 亚洲黄色精品| 国产精品久久久久一区二区| 久久riav二区三区| 美日韩精品视频免费看| 一本色道久久综合亚洲精品按摩 | 一区二区三区视频在线看| 亚洲人成网站在线播| 免费观看久久久4p| 欧美国产一区二区在线观看| 午夜精品久久久久久久久久久 | 国内精品久久久久影院优| 欧美成人精品不卡视频在线观看| 欧美—级在线免费片| 久久成人免费视频| 免费在线观看精品| 这里只有精品在线播放| 欧美中文字幕在线观看| 日韩视频在线观看一区二区| 亚洲午夜久久久久久久久电影院| 一区二区三区在线免费观看| 亚洲国产高潮在线观看| 国产欧美日韩麻豆91| 亚洲国产精品黑人久久久| 国产精品久久久久久久app | 亚洲激情欧美| 99精品国产99久久久久久福利| 国产亚洲精品久久久| 亚洲欧洲视频在线| 国产一区二区三区最好精华液| 亚洲精品久久久久久久久久久久| 国产欧美日韩在线观看| 亚洲精品欧美一区二区三区| 亚洲大胆女人| 亚洲欧美综合精品久久成人 | 欧美成人伊人久久综合网| 久久国产精品一区二区| 欧美日韩视频| 91久久国产自产拍夜夜嗨| 国产精品播放| 激情久久久久久| 欧美激情第一页xxx| 国产老肥熟一区二区三区| 99re国产精品| 亚洲第一精品电影| 久久一日本道色综合久久| 久久精品国产成人| 国产精品美女久久久免费| 久久综合色婷婷| 国产香蕉久久精品综合网| 亚洲免费在线精品一区| 亚洲一区二区三区激情| 欧美日韩妖精视频| 欧美不卡在线视频| 精品盗摄一区二区三区| 久久国产99| 免费亚洲网站| 国产日产欧产精品推荐色 | 国产日韩欧美一区二区三区在线观看| 亚洲免费成人av电影| 一本色道久久88综合日韩精品 | 亚洲一区欧美一区| 国产精品电影网站| 亚洲午夜在线视频| 亚洲一区国产一区| 国产精品午夜av在线| 亚洲欧美日韩精品久久亚洲区 | 国产精品美女久久久久av超清| 99国产精品久久| 久久国产精品亚洲77777| 在线观看欧美视频| 欧美精品在线视频观看| 亚洲欧美激情视频| 欧美黄色免费网站| 销魂美女一区二区三区视频在线| 伊人久久大香线| 欧美三级电影一区| 久久久久成人精品免费播放动漫| 91久久综合| 国产精品久久久久久户外露出| 久久国产精品毛片| 99精品国产99久久久久久福利| 欧美在线观看视频一区二区| 亚洲精品视频在线| 韩国av一区二区三区四区| 欧美理论在线| 久久精视频免费在线久久完整在线看 | 欧美性淫爽ww久久久久无| 久久精品免费播放| 亚洲自啪免费| 99成人精品| 亚洲国产精品传媒在线观看| 久久久久久久性| 在线亚洲免费| 亚洲精品一区中文| 亚洲第一在线综合网站| 国产热re99久久6国产精品| 欧美日韩国产三级| 免费不卡亚洲欧美| 久久久97精品| 欧美在线在线| 午夜精品久久久久久久99樱桃| 在线视频一区二区| 亚洲精品日韩激情在线电影 | 在线一区二区日韩| 亚洲精品小视频在线观看| 亚洲国产日韩在线| 亚洲高清视频在线观看| 狠狠色丁香久久综合频道| 国产日本欧美视频| 国产精品亚洲一区二区三区在线| 欧美日韩一区不卡| 欧美日韩亚洲精品内裤| 欧美激情一区二区三区蜜桃视频| 狼人社综合社区| 免费亚洲电影在线观看| 免费不卡在线视频| 欧美成人在线免费视频| 欧美好吊妞视频| 欧美激情1区2区| 欧美日韩不卡合集视频| 欧美日韩国产精品一区| 欧美午夜视频| 国产精品夜夜夜一区二区三区尤| 国产精品hd| 国产欧美日韩另类视频免费观看| 亚洲免费大片| 亚洲一级电影| 久久国产视频网站| 欧美不卡视频| 欧美日韩岛国| 国产精品一页| 在线精品国产欧美| 亚洲精品视频在线观看网站| 亚洲视频欧美在线| 欧美综合77777色婷婷| 久久久综合激的五月天| 欧美99久久| 一本色道久久88综合日韩精品| 国产欧美精品一区二区三区介绍| 国产午夜亚洲精品不卡| 亚洲国产精品t66y| 一区二区三欧美| 久久www成人_看片免费不卡| 欧美91福利在线观看| 99国产精品国产精品毛片| 欧美伊人久久久久久午夜久久久久| 久久久噜噜噜久久中文字幕色伊伊 | 9色国产精品| 午夜精品久久久久久久久久久| 久久人人97超碰精品888| 欧美精品观看| 国产一区二区三区免费在线观看| 亚洲国产精品传媒在线观看| 亚洲午夜小视频| 女人香蕉久久**毛片精品| 日韩亚洲欧美成人| 欧美在线亚洲一区| 欧美午夜在线视频| 亚洲国产成人午夜在线一区| 亚洲免费人成在线视频观看| 免费看成人av| 亚洲午夜羞羞片| 欧美黄免费看| 狠狠综合久久av一区二区老牛| 一区二区三区四区五区视频| 玖玖国产精品视频| 亚洲一区二区三区免费在线观看| 久久一区二区视频| 国产日韩欧美综合在线| 中文国产亚洲喷潮| 欧美国产日韩在线观看| 久久精品欧美日韩精品| 国产乱码精品1区2区3区| 亚洲美女视频网|