多线程同步 STD cout 输出

10

最近我一直在使用多线程编码,写了一段时间后,我意识到如果我在不同的boost::threads中使用std::cout,输出将没有逻辑顺序。我正在测试的程序类似于:

#include <boost/thread/thread.hpp>
#include <iostream>

int my01( void )
{
    std::cout << "my01" << std::endl;
    return 0;
}
/* my02, my03 and my04 are the same with different outputs*/
[...]
int main( void )
{
    boost::thread t1(&my01);
    boost::thread t2(&my02);
    boost::thread t3(&my03);
    boost::thread t4(&my04);

    while(!t1.joinable() || !t2.joinable() || !t3.joinable() || !t4.joinable());

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    std::cout << "The end!" << std::endl;
    getchar();
    return 0;
}


通常的输出结果如下所示(可能会发生变化):

my02my01
my04
my03
BLANK LINE
The end!

考虑到这个问题,我想创建一个单一的线程来管理所有的输出,以便它们按顺序排列,如下所示:

my01
my02
my03
my04
The end!

哪种是编写此类线程或管理这些输出的最佳方法?
请阅读本问题的答案:cout是否同步/线程安全?

附注:我使用Visual C++ 2010 Express,并且我的CPU有8个不同的内核。

感谢您的时间!


我有完全相同的问题,但我无法控制使用boost::threads的代码,因此对我来说,下面发布的解决方案不是一个选项。如何在不重写boost::thread的使用的情况下管理输出同步? - Michahell
5个回答

10

首先,您可以考虑避免所有显式的线程管理,而是使用std:async在任意数量的单独线程中启动任务。

其次,不要在线程本身中执行I/O操作,而是要创建结果,并将输出本身串行化。这意味着线程函数只是创建了一些数据,并把它留给调用者来实际写出:

std::string process(int value) {
     std::ostringstream buffer;
     buffer << "my" << std::setfill('0') << std::setw(2) << value;
     return buffer.str();
}

然后我们需要异步地启动四个副本:

std::vector<std::future<std::string> > results;

for (int i=0; i<4; i++)
    results.push_back(std::async(std::launch::async, process, i));

然后我们获取结果并按顺序打印出来:
for (auto &r : results)
    std::cout << r.get() << "\n";

将它们组合起来,我们可以得到以下代码:
#include <string>
#include <iostream>
#include <thread>
#include <future>
#include <sstream>
#include <vector>
#include <iomanip>

std::string process(int value) {
     std::ostringstream buffer;
     buffer << "my" << std::setfill('0') << std::setw(2) << value;
     return buffer.str();
}

int main() { 
    std::vector<std::future<std::string>> rets;

    for (int i=0; i<4; i++)
        rets.push_back(std::async(std::launch::async, process, i));

    for (auto & t : rets) {
        t.wait();
        std::cout << t.get() << "\n";
    }
}

我应该补充一点:这是基于标准的C++11 future。我相信这个基本思想也适用于Boost future(标准是基于它构建的),但我没有测试过。我希望能够做出一些小的调整(例如名称),以适应Boost的futures。


5
我通过编写一个薄包装器来解决这个问题,该包装器在开始向流写入时锁定互斥锁,并在完成写入语句后释放它以及刷新流。
用法:将 std::cout 替换为 safe_cout。
请记住,它不支持像 std::endl 这样的高级 std::cout 功能。
请参阅下面的代码或从此处获取:https://github.com/dkorolev/felicity/blob/master/safe_ostream.h
#include <cassert>
#include <iostream>
#include <mutex>
#include <memory>

struct safe_ostream {
  struct guarded_impl {
    guarded_impl() = delete;
    guarded_impl(const guarded_impl&) = delete;
    void operator=(const guarded_impl&) = delete;
    guarded_impl(std::ostream& ostream, std::mutex& mutex) : ostream_(ostream), guard_(mutex) {
    }
    ~guarded_impl() {
      ostream_.flush();
    }
    template<typename T> void write(const T& x) {
      ostream_ << x;
    }
    std::ostream& ostream_;
    std::lock_guard<std::mutex> guard_;
  };
  struct impl {
    impl() = delete;
    void operator=(const impl&) = delete;
    impl(std::ostream& ostream, std::mutex& mutex) : unique_impl_(new guarded_impl(ostream, mutex)) {
    }
    impl(const impl& rhs) {
      assert(rhs.unique_impl_.get());
      unique_impl_.swap(rhs.unique_impl_);
    }
    template<typename T> impl& operator<<(const T& x) {
      guarded_impl* p = unique_impl_.get();
      assert(p);
      p->write(x);
      return *this;
    }
    mutable std::unique_ptr<guarded_impl> unique_impl_;
  };
  explicit safe_ostream(std::ostream& ostream) : ostream_(ostream) {
  }
  template<typename T> impl operator<<(const T& x) {
    return impl(ostream_, mutex_) << x;
  }
  std::ostream& ostream_;
  std::mutex mutex_;
};
safe_ostream safe_cout(std::cout);
safe_ostream safe_cerr(std::cerr);

2
Dima,锁定是邪恶的。 - Super-intelligent Shade

2
你需要对线程进行排序,以便输出的顺序符合你的要求(可能通过将线程实例或事件传递给适当的线程,使它们只能按照你的顺序执行),或者你可以为所有输出分配一个线程序列号,将所有输出排队到一个“打印”线程中,在那里,保留任何顺序不正确的行的列表,以便打印输出符合你的要求。
在“真正”的应用程序中(即不是误用线程的简单测试应用程序),其中线程在必须保留顺序的连续缓冲区上并行执行大量工作时,强制线程相互等待通常不是一个合理的选择。通常使用序列号,并在之后重新组装缓冲流。

1

给每个线程一个std::ostringstream来写输出。在程序结束时,按顺序打印每个线程的输出。

考虑到线程4可能比线程1先完成,你还有什么其他方法可以做到这一点吗?


程序将长时间运行,因此我考虑编写一个线程来管理线程安全缓冲区,输出所有内容。 - k3oy
1
你的要求看起来是希望线程1的整个输出在线程2的输出之前打印出来。你可以像Zan建议的那样,直接从线程1打印到cout,并在所有其他线程中使用std::ostringstream - Andre Holzner

0
使用锁定。如果可以使用boost,请执行例如。
int my01(boost::mutex *coutGuard)
{
  {
     // lock cout until the closing brace
     boost::mutex::scoped_lock lock(*coutGuard);

     std::cout << "my01" << std::endl;
  }

  return 0;
}

int main( void )
{
   boost::mutex coutGuard;

   boost::thread t1(boost::bind(&my01, &coutGuard));
   ...
}

可以使用lock_guard代替scoped_lock


1
我认为它已经接近完成了,但是使用这个互斥锁模式无法保证顺序,必须使用其他方法。 - Julio Raffaine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接