代码之家  ›  专栏  ›  技术社区  ›  Adam Holmberg

有没有办法在Windows的基本iostream上进行非锁定流插入/提取?

  •  9
  • Adam Holmberg  · 技术社区  · 15 年前

    我是一个C++开发人员,主要是在Solaris和Linux上编程,直到最近我被迫创建一个面向Windows的应用程序。

    我一直在使用基于TC+Socket支持的C++ I/O流的通信设计。该设计基于从流中连续读取单个线程(大多数时间阻塞在套接字读取中,等待数据),而其他线程通过同一个流发送(由互斥同步)。

    在转到Windows时,我选择使用boost::asio::ip::tcp::iostream来实现套接字流。我沮丧地发现,上述多线程设计导致了窗口死锁。似乎 operator<<(std::basic_ostream<...>,std::basic_string<...>) 声明一个“sentry”,它为输入和输出操作锁定整个流。由于我的读取线程总是在流上等待,因此当创建此岗哨时,来自其他线程的发送操作会死锁。

    以下是在操作员和岗哨施工期间调用堆栈的相关部分:

        ...
        ntdll.dll!7c901046()    
        CAF.exe!_Mtxlock(_RTL_CRITICAL_SECTION * _Mtx=0x00397ad0)  Line 45  C
        CAF.exe!std::_Mutex::_Lock()  Line 24 + 0xb bytes   C++
        CAF.exe!std::basic_streambuf<char,std::char_traits<char> >::_Lock()  Line 174   C++
        CAF.exe!std::basic_ostream<char,std::char_traits<char> >::_Sentry_base::_Sentry_base(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...})  Line 78   C++
        CAF.exe!std::basic_ostream<char,std::char_traits<char> >::sentry::sentry(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...})  Line 95 + 0x4e bytes  C++
    >   CAF.exe!std::operator<<<char,std::char_traits<char>,std::allocator<char> >(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...}, const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & _Str="###")  Line 549 + 0xc bytes   C++
        ...
    

    如果IStream和OStream组件被单独锁定,我会很好的,但事实并非如此。

    我是否可以使用流运算符的其他实现?我能指示它不要锁吗?我应该实现我自己的(不确定如何实现)?

    任何建议都将不胜感激。

    (平台是32位和64位的Windows。使用Visual Studio 2003 Pro和2008 Express观察到的行为)

    5 回复  |  直到 12 年前
        1
  •  1
  •   teambob    15 年前

    根据Boost文档[1],使用两个线程访问一个对象而不使用互斥是“不安全的”。仅仅因为它在Unix平台上工作,并不能保证它在Windows平台上工作。

    所以你的选择是:

    1. 重写代码,这样线程就不会同时访问对象。
    2. 修补Boost库并将更改发送回
    3. 好好问问克里斯,他是否会为Windows平台做些修改

    〔1〕 http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/overview/core/threads.html

        2
  •  1
  •   Adam Holmberg    15 年前

    这个问题已经拖得够久了。我要报告我最后做了什么,尽管有可能我会被嘲笑。

    我已经确定了问题是,当试图在单独的读写操作中访问iostream对象时,两个线程出现了死锁。我可以看到,字符串流插入和提取运算符的Visual Studio实现都声明了一个Sentry,它锁定了与正在操作的流相关联的流缓冲区。

    我知道,对于这个死锁的相关流,流缓冲区实现是boost::asio::basic_socket_streambuf。我检查了实现,以查看读写操作(下溢和溢出)实际上在不同的缓冲区(get和put)上运行。

    经过以上验证,我选择简单地绕过这个应用程序的锁定。为此,我使用了特定于项目的预处理器定义来排除锁定岗哨的基本\u istream实现中的锁定代码:

        class _Sentry_base
            {   // stores thread lock and reference to input stream
        public:
            __CLR_OR_THIS_CALL _Sentry_base(_Myt& _Istr)
                : _Myistr(_Istr)
                {   // lock the stream buffer, if there
    #ifndef MY_PROJECT
                if (_Myistr.rdbuf() != 0)
                    _Myistr.rdbuf()->_Lock();
    #endif
                }
    
            __CLR_OR_THIS_CALL ~_Sentry_base()
                {   // destroy after unlocking
    #ifndef MY_PROJECT
                if (_Myistr.rdbuf() != 0)
                    _Myistr.rdbuf()->_Unlock();
    #endif
                }
    

    上攻:

    • 它工作
    • 只有我的项目(有适当的定义)受到影响

    下降趋势:

    • 感觉有点不舒服
    • 建造此平台的每个平台都需要进行此修改。

    我计划通过在代码和项目文档中大声记录这一点来缓和后一点。

    我意识到这可能有一个更优雅的解决方案,但为了方便起见,我在尽职调查后选择了一个直接的解决方案来理解影响。

        3
  •  0
  •   rlbond    15 年前

    也许您可以自己实现一个锁层?也就是说,有一个单独的 istream ostream 当它们被调用时,您自己锁定它。定期检查两个是否都解锁,然后从一个读取到另一个。

        4
  •  0
  •   Michael Kristofik    15 年前

    在写入流之后是否显式地刷新了它? This blog post 意味着您的数据可能只是“卡”在缓冲区中。如果这是真的,那么也许你看起来像死锁了,因为还没有什么可供阅读的。添加 stream << std::flush 直到发送操作结束。

    博客文章建议的另一种(尽管效率较低)解决方案是关闭流的输出缓冲:

    stream.rdbuf()->pubsetbuf(0, 0);
    
        5
  •  0
  •   James Reynolds    12 年前

    我知道这是个老问题…但我必须自己做!

    我的情况更复杂,因为这是我自己的streambuf,但您可以通过执行以下操作来解决此问题:

    std::ostream &operator<<(std::ostream &x, std::string &y)
    {
      x.rdbuf()->_Unlock();
      x << y.c_str();
    }
    

    它会优先调用std::version。

    当然,您必须为每个调用lock的Windows操作员以及(在我的情况下)您的streambuf中的所有读/写调用执行此操作。