简体   繁体   English

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

[英]Difference in boost::asio::io_service behaviour between GCC and MSVC: cannot cancel posted jobs

When implementing a basic thread pool using boost::asio::io_service , I am observing some differences in how queued tasks are handled when stopping the io_service . 当使用boost::asio::io_service实现基本线程池时,我观察到在停止io_service时如何处理排队任务的一些差异。

On MSVC 14 (MS Visual Studio 2015), for some reason the queued tasks which were not started yet are not dropped when stopping the io_service but are run nonetheless. 在MSVC 14(MS Visual Studio 2015)上,出于某种原因,在停止io_service时仍未启动尚未启动的排队任务,但仍在运行。 These tasks are dropped when running this on Ubuntu 16.04 (GCC 5.4.0). 在Ubuntu 16.04(GCC 5.4.0)上运行此任务时,将删除这些任务。

I have simplified and cleaned up the original tests and put them in a single file (also listed below) which only depends on boost and uses some sleeps to demonstrate the problem. 我已经简化并清理了原始测试并将它们放在一个文件中 (也在下面列出),它只依赖于boost并使用一些睡眠来演示问题。 You can build it with the CMakeLists.txt (also listed below) if you wish or use the online compilers linked below. 如果您希望或使用下面链接的在线编译器,您可以使用CMakeLists.txt (也在下面列出)构建它。
Notice that the thread pool uses only one worker thread so that the jobs are run sequentially. 请注意,线程池仅使用一个工作线程,以便按顺序运行作业。

The output with GCC is as expected ( Here on an online compiler ): GCC的输出符合预期( 在线编译器上 ):

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

This is the output on MSVC 14 (Visual Studio 2015) ( Here on an online VC++ compiler ): 这是MSVC 14(Visual Studio 2015)上的输出( 这里是在线VC ++编译器 ):

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

Am I doing something wrong? 难道我做错了什么?

I have also filled a bug to boost but got no response so far: #13317 我还填写了一个bug来提升但到目前为止没有得到任何回应: #13317


Source code: ThreadPoolTests.cpp 源代码: 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();
}

Here the CMakeLists.txt for compilation ease. 这里有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})

Again the question: Am I doing something wrong? 问题是: 我做错了吗?

At first I saw a problem with your tests. 起初我看到你的测试有问题。

Let's start with that first. 让我们从第一个开始。

Cancel, Racy Tests 取消,Racy测试

ThreadPool::cancel does two things: ThreadPool::cancel做了两件事:

  • reset work (allowing io_service::run() to complete) 重置work (允许io_service::run()完成)
  • stop the service stop服务

The problem is, resetting work doesn't affect any work already in progress. 问题是,重置work不会影响任何正在进行的工作。 Neither does stop . 也没有stop So, any work already posted will be completed. 因此,任何已发布的作品都将完成。

The only situation in which you get your "expected" behaviour is in the off chance that you stop the io_service even before the first thread in the pool started running one of the posted tasks. 您获得“预期”行为的唯一情况是,即使在池中的第一个线程开始运行其中一个已发布的任务之前,您仍然stop io_service

The fact that you seem to get this behaviour on GCC consistently is a fluke. 事实上,你似乎一直在GCC上获得这种行为是一种侥幸。 In reality it's a race, and that's easily demonstrated by adding the tiniest delay before cancel() . 实际上这是一场比赛,通过在cancel()之前添加最小的延迟,可以很容易地证明这一点。

In fact, even with the code as is, repeated runs showed spurious failures like: 事实上,即使代码是原样,重复运行显示虚假的失败,如:

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

Enabling handler tracking confirms that only one handler was abandoned: 启用处理程序跟踪确认只放弃了一个处理程序:

在此输入图像描述

Even just enabling address sanitizer made the failure become pretty much always reproducible. 即使只是启用地址消毒剂,失败也变得非常可重复。

Digging Deeper: io_service::stop 深入挖掘: io_service::stop

If it's a race, and the tests are limited to a single service thread, surely the subtle race window would not be large enough to allow the second task to run even though io_service::stop() had been called? 如果它是一个竞赛,并且测试仅限于一个服务线程,那么即使io_service::stop() ,微妙的竞争窗口肯定不会足够大以允许第二个任务运行?

I struggled with this one, so I instrumented the Functor function with some more timing sampling. 我为这个问题苦苦挣扎,所以我通过更多的时序采样来对Functor函数进行了检测。 Let's also record the invocation of a task so we can distinguish tasks that were never started from those that merely didn't complete: 让我们记录一个任务的调用,这样我们就可以区分从未开始的任务和那些仅仅完成的任务:

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;
};

Now, runTests can be extended to keep timings of all tasks as well as those of the actions/pool shutdown: 现在,可以扩展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();
};

And our GCC run prints: 我们的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

It shows that the action function ( cancel in this case) got invoked at the same time that our service thread was invoking functor1 . 它表明在我们的服务线程调用functor1的同时调用了action函数(在这种情况下cancel )。 And functor2 was never invoked. 并且从未调用过functor2

On MSVC , the same test prints: 在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

Like with GCC, the cancel action ran at time 0ms, but strangely both tasks completed even though they got invoked after that action ran. 与GCC一样, cancel操作在0ms时运行,但奇怪的是两个任务都已完成,即使它们该操作运行后被调用。

This indicates that on Windows, Asio will not prevent existing tasks being dispatched to threads if io_service::stop() is invoked. 这表明在Windows上,如果调用io_service::stop() ,则Asio不会阻止将现有任务调度到线程。 Increasing the load to 9 tasks shows consistent results: 将负载增加到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 

Interrupt 打断

Interruptions work fine on Linux/GCC and MSVC . 中断在Linux / GCCMSVC运行良好。

Uninterruptables Uninterruptables

The last scenario is effectively identical to the second (because the tasks are immune to interruption). 最后一个场景实际上与第二个场景相同(因为任务不受中断影响)。

Summary: 摘要:

On windows, the behaviour of io_service::stop() contradicts the documentation, so that would be a bug: 在Windows上, io_service::stop()的行为与文档相矛盾,因此这将是一个错误:

在此输入图像描述

Here's a much simpler reproducer: 这是一个更简单的复制器:

#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();
}

Which prints 1 on GCC and 1 2 3 on MSVC. 其中在GCC上打印1 ,在MSVC上打印1 1 2 3

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM