9

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

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

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

以下是 operator<< 和 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 组件被分别锁定,我会很好,但事实并非如此。

我可以使用流运算符的替代实现吗?我可以指示它不锁定吗?我应该实现自己的(不知道该怎么做)?

任何建议,将不胜感激。

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

4

5 回答 5

1

根据 boost 文档 [1],使用两个线程访问一个没有互斥锁的对象是“不安全的”。仅仅因为它可以在 Unix 平台上运行并不能保证它可以在 Windows 平台上运行。

所以你的选择是:

  1. 重写你的代码,这样你的线程就不会同时访问对象
  2. 修补 boost 库并将更改发回
  3. 很好地询问 Chris 是否会为 Windows 平台进行更改

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

于 2009-07-24T05:30:52.800 回答
1

这个问题已经搁置了足够长的时间。我会报告我最终做了什么,即使我有可能会被嘲笑。

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

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

通过上述验证,我选择简单地绕过此应用程序的锁定。为此,我使用项目特定的预处理器定义来排除锁定哨兵的 basic_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
            }

优势:

  • 有用
  • 只有我的项目(具有适当的定义)受到影响

缺点:

  • 感觉有点hacky
  • 构建它的每个平台都需要进行此修改

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

我意识到可能有一个更优雅的解决方案,但为了权宜之计,我在尽职调查了解影响后选择了直接解决方案。

于 2009-07-30T23:57:40.793 回答
0

也许您可以自己实现一个锁定层?IE,有一个单独的istreamostream当它们被调用时你自己锁定。定期检查两者是否都已解锁,然后从一个读入另一个。

于 2009-07-16T23:13:38.070 回答
0

您是否在写入流后显式刷新流? 这篇博文暗示您的数据可能只是“卡在”缓冲区中。如果这是真的,那么您可能会出现死锁,因为目前还没有可供阅读的内容。添加stream << std::flush到发送操作的末尾。

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

stream.rdbuf()->pubsetbuf(0, 0);
于 2009-07-23T03:20:22.383 回答
0

我知道这是一个老问题......但我不得不自己做这个!

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

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

它优先于 std:: 版本被调用。

您当然必须为每个调用 _Lock 的 Windows operator<< 和(在我的情况下)您的 streambuf 中的所有读/写调用执行此操作。

于 2012-12-19T18:10:27.823 回答