GCC和MSVC之间的boost :: asio :: io_service行为的差异:无法取消已发布的作业

时间:2017-12-06 15:14:23

标签: c++ visual-c++ boost boost-asio threadpool

使用boost::asio::io_service实现基本线程池时,我观察到在停止io_service时排队任务的处理方式存在一些差异。

在MSVC 14(MS Visual Studio 2015)上,由于某种原因,在停止io_service但尚未运行时,尚未启动尚未启动的排队任务。在Ubuntu 16.04(GCC 5.4.0)上运行时,这些任务将被删除。

我已经简化并清理了原始测试,并将它们放在single file(也在下面列出)中,它只依赖于boost并使用一些睡眠来证明问题。如果您希望或使用下面链接的在线编译器,您可以使用CMakeLists.txt(也在下面列出)构建它 请注意,线程池仅使用一个工作线程,以便按顺序运行作业。

GCC的输出符合预期(Here on an online compiler):

 checkAllWorkIsProcessedBeforeDestruction  
     passed.  
     passed.  
     passed.  
checkWorkCanBeCancelled
     passed.
     passed.
     passed.
checkWorkCanBeInterrupted
     passed.
     passed.
     passed.
checkUninterruptableWorkIsNotInterruptedButCanBeDropped
     passed.
     passed.
     passed.
     passed.

这是MSVC 14(Visual Studio 2015)(Here on an online VC++ compiler)上的输出:

checkAllWorkIsProcessedBeforeDestruction
     passed.
     passed.
     passed.
checkWorkCanBeCancelled
     Error: functor 1 call expected: false current: true
     Error: functor 2 call expected: false current: true
     Error: running time expected: 150 current: 402
checkWorkCanBeInterrupted
     passed.
     passed.
     passed.
checkUninterruptableWorkIsNotInterruptedButCanBeDropped
     passed.
     Error: functor 2 call expected: false current: true
     passed.
     Error: running time expected: 250 current: 404

我做错了吗?

我还填写了一个bug来提升但到目前为止没有得到任何回复:#13317

源代码:ThreadPoolTests.cpp

// Copyright (c) 2017 Diego Barrios Romero <eldruin@gmail.com>

#include <iostream>
#include <string>
#include <memory>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>
#include <boost/chrono.hpp>

class ThreadPool
{
public:
  ThreadPool(const size_t threadCount = boost::thread::hardware_concurrency())
    : work(new boost::asio::io_service::work(service))
  {
    for (size_t i = 0; i < threadCount; ++i)
    {
      threads.create_thread(boost::bind(&boost::asio::io_service::run, &service));
    }
  }
  template<typename FunctionType>
  void post(FunctionType f)
  {
    service.post(f);
  }

  void interrupt()
  {
    threads.interrupt_all();
  }

  void cancel()
  {
    work.reset();
    service.stop();
  }

  ~ThreadPool()
  {
    work.reset();
    threads.join_all();
  }
private:
  boost::asio::io_service service;
  boost::thread_group threads;
  std::unique_ptr<boost::asio::io_service::work> work;
};


struct Functor
{
  void operator()()
  {
    boost::this_thread::sleep(boost::posix_time::milliseconds(200));
    boost::lock_guard<boost::mutex> lock(mutex);
    wasCalled_ = true;
  }
  bool wasCalled() const
  {
    boost::lock_guard<boost::mutex> lock(mutex);
    return wasCalled_;
  }

private:
  bool wasCalled_ = false;
  mutable boost::mutex mutex;
};

struct UninterruptableFunctor : public Functor
{
  void operator()()
  {
    boost::this_thread::disable_interruption disableInterruptions;
    Functor::operator()();
  }
};

template<typename F, typename T1, typename T2>
void check(F compare, T1 expected, T2 current, const std::string& msg)
{
  if (compare(expected, current))
  {
    std::cout << "\tpassed." << std::endl;
  }
  else
  {
    std::cout << std::boolalpha
              << "\tError: " << msg << " expected: " << expected
              << " current: " << current << std::endl;
  }
}

struct ThreadPoolTest
{
  boost::int_least64_t getRunningTimeInMS() const
  {
    auto executionTime = boost::chrono::high_resolution_clock::now() - start;
    return boost::chrono::duration_cast<boost::chrono::milliseconds>(executionTime).count();    
  }

  template<typename FunctorType, typename F>
  void runTest(F f, bool shouldFunctor1BeCalled, bool shouldFunctor2BeCalled)
  {
    FunctorType functor1, functor2;
    {
      ThreadPool pool(1);
      pool.post(boost::bind(&FunctorType::operator(), &functor1));
      pool.post(boost::bind(&FunctorType::operator(), &functor2));
      f(pool);
    }

    auto eq = [](bool a, bool b) { return a == b; };
    check(eq, shouldFunctor1BeCalled, functor1.wasCalled(), "functor 1 call");
    check(eq, shouldFunctor2BeCalled, functor2.wasCalled(), "functor 2 call");
  }

private:
  boost::chrono::high_resolution_clock::time_point start = boost::chrono::high_resolution_clock::now();
};

void doNothing(ThreadPool&) { }
void cancel(ThreadPool& pool)
{
  pool.cancel();
}
void waitForStartThenInterruptThenCancel(ThreadPool& pool)
{
  boost::this_thread::sleep(boost::posix_time::milliseconds(100));
  pool.interrupt();
  pool.cancel();
}

bool lessEq (const boost::int_least64_t a, const boost::int_least64_t b) { return a <= b; }
bool greaterEq (const boost::int_least64_t a, const boost::int_least64_t b) { return a >= b; }

void checkAllWorkIsProcessedBeforeDestruction()
{
  ThreadPoolTest test;
  std::cout << "checkAllWorkIsProcessedBeforeDestruction\n";
  test.runTest<Functor>(doNothing, true, true);
  check(lessEq, 350, test.getRunningTimeInMS(), "running time");
}

void checkWorkCanBeCancelled()
{
  ThreadPoolTest test;
  std::cout << "checkWorkCanBeCancelled\n";
  test.runTest<Functor>(cancel, false, false);
  check(greaterEq, 150, test.getRunningTimeInMS(), "running time");
}

void checkWorkCanBeInterrupted()
{
  ThreadPoolTest test;
  std::cout << "checkWorkCanBeInterrupted\n";
  test.runTest<Functor>(waitForStartThenInterruptThenCancel, false, false);
  check(greaterEq, 150, test.getRunningTimeInMS(), "running time");
}

void checkUninterruptableWorkIsNotInterruptedButCanBeDropped()
{
  ThreadPoolTest test;
  std::cout << "checkUninterruptableWorkIsNotInterruptedButCanBeDropped\n";
  test.runTest<UninterruptableFunctor>(waitForStartThenInterruptThenCancel, true, false);
  check(lessEq, 150, test.getRunningTimeInMS(), "running time");
  check(greaterEq, 250, test.getRunningTimeInMS(), "running time");
}

int main(int, char*[])
{
  checkAllWorkIsProcessedBeforeDestruction();
  checkWorkCanBeCancelled();
  checkWorkCanBeInterrupted();
  checkUninterruptableWorkIsNotInterruptedButCanBeDropped();
}

这里CMakeLists.txt可以轻松编译。

cmake_minimum_required (VERSION 2.8.11)
project (ThreadPoolTests)

set(CMAKE_CXX_STANDARD 11)

find_package(Boost COMPONENTS thread)
if (Boost_FOUND)
  include_directories(${Boost_INCLUDE_DIR})
else()
  message(FATAL_ERROR "No Boost found")
endif()

add_executable (ThreadPoolTests ThreadPoolTests.cpp)
target_link_libraries(ThreadPoolTests ${Boost_LIBRARIES})

同样问题:我做错了吗?

1 个答案:

答案 0 :(得分:1)

起初我看到你的测试有问题。

让我们从第一个开始。

取消,Racy测试

ThreadPool::cancel做了两件事:

  • 重置work(允许io_service::run()完成)
  • stop服务

问题是,重置work不会影响任何正在进行的工作。 stop也没有。因此,任何已发布的作品都将完成。

唯一可以获得&#34;预期&#34;即使在池中的第一个线程开始运行其中一个已发布的任务之前,您stop io_service的行为也很有可能。

似乎 在GCC上持续获取此行为的事实是一个侥幸。实际上它是一场比赛,通过在cancel()之前添加最微小的延迟,可以很容易地证明这一点。

事实上,即使使用代码,重复运行也显示虚假失败,如:

checkWorkCanBeCancelled
    Error: functor 1 call expected: false actual: true
    passed (functor 2 call).
    Error: running time expected: 150 actual: 200

启用处理程序跟踪确认只放弃了一个处理程序:

enter image description here

即使只是启用地址消毒剂,失败也会变得非常可重复。

深入挖掘:io_service::stop

如果它是竞赛,并且测试仅限于一个服务线程,那么即使调用了io_service::stop(),微妙的竞赛窗口肯定不会足够大以允许第二个任务运行?

我在努力解决这个问题,所以我使用更多的时序采样对Functor函数进行了检测。让我们记录一个任务的调用,这样我们就可以区分从未开始的任务和那些仅仅完成的任务:

struct Functor {
    void operator()() {
        {
            boost::lock_guard<boost::mutex> lock(mutex);
            state_.invocation = Clock::now();
        }
        boost::this_thread::sleep(boost::posix_time::milliseconds(200));
        {
            boost::lock_guard<boost::mutex> lock(mutex);
            state_.completion = Clock::now();
        }
    }

    struct State {
        TP start, invocation, completion;

        friend std::ostream& operator<<(std::ostream& os, State const& s) {
            return os << "[" << relative(s.invocation, s.start) << "," << relative(s.completion, s.start) << "]";
        }
    };

    State sample() const {
        boost::lock_guard<boost::mutex> lock(mutex);
        return state_;
    }

  private:
    State state_ = { TP::min(), TP::min(), TP::min() };
    mutable boost::mutex mutex;
};


struct Functor {
    void operator()() {
        {
            boost::lock_guard<boost::mutex> lock(mutex);
            state_.wasInvoked = true;
        }
        boost::this_thread::sleep(boost::posix_time::milliseconds(200));
        {
            boost::lock_guard<boost::mutex> lock(mutex);
            state_.wasCompleted = true;
        }
    }

    struct State {
        bool wasInvoked, wasCompleted;

        friend std::ostream& operator<<(std::ostream& os, State const& s) {
            if (s.wasInvoked && s.wasCompleted) return os << "[invoked,completed]";
            if (s.wasInvoked) return os << "[invoked]";
            assert(!s.wasCompleted);
            return os << "[]";
        }
    };

    State sample() const {
        boost::lock_guard<boost::mutex> lock(mutex);
        return state_;
    }

  private:
    State state_ = { false, false };
    mutable boost::mutex mutex;
};

现在,可以扩展runTests以保持所有任务的时间以及操作/池关闭的时间:

struct ThreadPoolTest {
    boost::int_least64_t getRunningTimeInMS() const { return relative(Clock::now(), start); }

    template <typename FunctorType, typename ScenarioAction>
    void runTest(ScenarioAction action, bool shouldFunctor1BeCalled, bool shouldFunctor2BeCalled) {
        struct Task {
            std::string name;
            Task(std::string name) : name(name) {}

            FunctorType functor;
            Functor::State before, after, final_;
        } tasks[] = { {"functor1"}, {"functor2"} };

        TP before_action, after_action, pool_final;

        {
            ThreadPool pool(1);
            for (auto& task : tasks) pool.post(std::ref(task.functor));

            for (auto& task : tasks) task.before = task.functor.sample();

            before_action = Clock::now();
            action(pool);
            after_action = Clock::now();

            for (auto& task : tasks) task.after = task.functor.sample();
        }

        pool_final = Clock::now();
        for (auto& task : tasks) task.final_ = task.functor.sample();

        // aids in pretty printing
        for (auto& task : tasks) for (auto sample : { &Task::before, &Task::after, &Task::final_ }) {
            (task.*sample).start = start;
        }

        for (auto& task : tasks)
            std::cout << "DEBUG: " << task.name << " before:" << task.before << " after:" << task.after << " final:" << task.final_ << "\n";
        std::cout << "DEBUG: pool/action before:" << relative(before_action, start) << " after:" << relative(after_action, start) << " final:" << relative(pool_final, start) << "\n";

        check(std::equal_to<>{}, shouldFunctor1BeCalled, is_set(tasks[0].final_.completion), "functor 1 call");
        check(std::equal_to<>{}, shouldFunctor2BeCalled, is_set(tasks[1].final_.completion), "functor 2 call");
    }

  private:
    TP start = Clock::now();
};

我们的GCC运行打印:

checkWorkCanBeCancelled
DEBUG: functor1 before:[-1,-1] after:[-1,-1] final:[0,200]
DEBUG: functor2 before:[-1,-1] after:[-1,-1] final:[-1,-1]
DEBUG: pool/action before:0 after:0 final:200
    Error: functor 1 call expected: false actual: true
    passed (functor 2 call).
    Error: running time expected: 150 actual: 200

它显示在我们的服务线程调用cancel的同时调用了动作函数(在这种情况下为functor1)。并且functor2从未被调用过。

On MSVC ,同样的测试打印:

checkWorkCanBeCancelled
DEBUG: functor1 before:[-1,-1] after:[-1,-1] final:[2,198]
DEBUG: functor2 before:[-1,-1] after:[-1,-1] final:[198,401]
DEBUG: pool/action before:0 after:0 final:404
    Error: functor 1 call expected: false actual: true
    Error: functor 2 call expected: false actual: true
    Error: running time expected: 150 actual: 405

与GCC一样,cancel操作在0毫秒时运行,但奇怪的是,即使在该操作运行后调用了,两个任务也完成了。

  

这表明在Windows上,如果调用io_service::stop(),则Asio不会阻止将现有任务分派给线程。将负载增加到9个任务会显示一致的结果:

DEBUG: functor1 before:[-1,-1] after:[-1,-1] final:[2,195]
DEBUG: functor2 before:[-1,-1] after:[-1,-1] final:[195,398]
DEBUG: functor3 before:[-1,-1] after:[-1,-1] final:[399,602]
DEBUG: functor4 before:[-1,-1] after:[-1,-1] final:[602,821]
DEBUG: functor5 before:[-1,-1] after:[-1,-1] final:[821,1024]
DEBUG: functor6 before:[-1,-1] after:[-1,-1] final:[1024,1228]
DEBUG: functor7 before:[-1,-1] after:[-1,-1] final:[1228,1431]
DEBUG: functor8 before:[-1,-1] after:[-1,-1] final:[1431,1634]
DEBUG: functor9 before:[-1,-1] after:[-1,-1] final:[1634,1837]
DEBUG: pool/action before:0 after:0 final:1838

中断

中断在 Linux/GCC MSVC 上正常工作。

Uninterruptables

最后一个场景实际上与第二个场景相同(因为任务不受中断影响)。

要点:

在Windows上,io_service::stop()的行为与文档相矛盾,因此这将是一个错误:

enter image description here

这是一个更简单的复制器:

#include <boost/asio.hpp>
#include <thread>
#include <iostream>
using namespace std::chrono_literals;

int main() {
    boost::asio::io_service s;

    s.post([] { std::this_thread::sleep_for(5ms); std::cout << "1\n"; });
    s.post([] { std::this_thread::sleep_for(5ms); std::cout << "2\n"; });
    s.post([] { std::this_thread::sleep_for(5ms); std::cout << "3\n"; });

    std::thread th([&] { s.run(); });

    std::this_thread::sleep_for(1ms);
    s.stop();

    th.join();
}

在GCC上打印1,在MSVC上打印1 2 3