使用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})
同样问题:我做错了吗?
答案 0 :(得分:1)
起初我看到你的测试有问题。
让我们从第一个开始。
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
启用处理程序跟踪确认只放弃了一个处理程序:
即使只是启用地址消毒剂,失败也会变得非常可重复。
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
最后一个场景实际上与第二个场景相同(因为任务不受中断影响)。
在Windows上,io_service::stop()
的行为与文档相矛盾,因此这将是一个错误:
这是一个更简单的复制器:
#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
。