core: parallel backends API

- allow to replace parallel_for() backend
pull/19365/head
Alexander Alekhin 4 years ago
parent 84676fefe3
commit b73bf03bfc
  1. 1
      cmake/templates/opencv_abi.xml.in
  2. 1
      doc/Doxyfile.in
  3. 7
      modules/core/CMakeLists.txt
  4. 4
      modules/core/include/opencv2/core.hpp
  5. 72
      modules/core/include/opencv2/core/parallel/backend/parallel_for.openmp.hpp
  6. 153
      modules/core/include/opencv2/core/parallel/backend/parallel_for.tbb.hpp
  7. 73
      modules/core/include/opencv2/core/parallel/parallel_backend.hpp
  8. 19
      modules/core/include/opencv2/core/utility.hpp
  9. 105
      modules/core/src/parallel.cpp
  10. 9
      samples/cpp/CMakeLists.txt
  11. 26
      samples/cpp/tutorial_code/core/parallel_backend/CMakeLists.txt
  12. 44
      samples/cpp/tutorial_code/core/parallel_backend/example-openmp.cpp
  13. 43
      samples/cpp/tutorial_code/core/parallel_backend/example-tbb.cpp

@ -26,6 +26,7 @@
opencv2/core/hal/*.impl.*
opencv2/core/cuda*
opencv2/core/opencl*
opencv2/core/parallel/backend/*
opencv2/core/private*
opencv2/core/quaternion*
opencv/cxeigen.hpp

@ -227,6 +227,7 @@ INCLUDE_PATH =
INCLUDE_FILE_PATTERNS =
PREDEFINED = __cplusplus=1 \
CVAPI(x)=x \
CV_API_CALL= \
CV_DOXYGEN= \
CV_EXPORTS= \
CV_EXPORTS_W= \

@ -58,10 +58,15 @@ file(GLOB_RECURSE module_opencl_hdrs
source_group("Include\\Cuda Headers" FILES ${lib_cuda_hdrs})
source_group("Include\\Cuda Headers\\Detail" FILES ${lib_cuda_hdrs_detail})
file(GLOB_RECURSE core_parallel_hdrs
"${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/parallel/*.hpp"
"${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/parallel/*.h")
ocv_source_group("Include" DIRBASE "${CMAKE_CURRENT_LIST_DIR}/include" FILES ${core_parallel_hdrs})
source_group("Src" FILES "${OPENCV_MODULE_opencv_core_BINARY_DIR}/version_string.inc")
ocv_glob_module_sources(SOURCES "${OPENCV_MODULE_opencv_core_BINARY_DIR}/version_string.inc"
HEADERS ${module_opencl_hdrs} ${lib_cuda_hdrs} ${lib_cuda_hdrs_detail})
HEADERS ${core_parallel_hdrs} ${module_opencl_hdrs} ${lib_cuda_hdrs} ${lib_cuda_hdrs_detail})
ocv_module_include_directories(${the_module} ${ZLIB_INCLUDE_DIRS} ${OPENCL_INCLUDE_DIRS})
if(ANDROID AND HAVE_CPUFEATURES)

@ -97,6 +97,10 @@
@}
@defgroup core_lowlevel_api Low-level API for external libraries / plugins
@}
@defgroup core_parallel Parallel Processing
@{
@defgroup core_parallel_backend Parallel backends API
@}
@}
*/

@ -0,0 +1,72 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef OPENCV_CORE_PARALLEL_FOR_OPENMP_HPP
#define OPENCV_CORE_PARALLEL_FOR_OPENMP_HPP
#include "opencv2/core/parallel/parallel_backend.hpp"
#if !defined(_OPENMP) && !defined(OPENCV_SKIP_OPENMP_PRESENSE_CHECK)
#error "This file must be compiled with enabled OpenMP"
#endif
#include <omp.h>
namespace cv { namespace parallel { namespace openmp {
/** OpenMP parallel_for API implementation
*
* @sa setParallelForBackend
* @ingroup core_parallel_backend
*/
class ParallelForBackend : public ParallelForAPI
{
protected:
int numThreads;
int numThreadsMax;
public:
ParallelForBackend()
{
numThreads = 0;
numThreadsMax = omp_get_max_threads();
}
virtual ~ParallelForBackend() {}
virtual void parallel_for(int tasks, FN_parallel_for_body_cb_t body_callback, void* callback_data) CV_OVERRIDE
{
#pragma omp parallel for schedule(dynamic) num_threads(numThreads > 0 ? numThreads : numThreadsMax)
for (int i = 0; i < tasks; ++i)
body_callback(i, i + 1, callback_data);
}
virtual int getThreadNum() const CV_OVERRIDE
{
return omp_get_thread_num();
}
virtual int getNumThreads() const CV_OVERRIDE
{
return numThreads > 0
? numThreads
: numThreadsMax;
}
virtual int setNumThreads(int nThreads) CV_OVERRIDE
{
int oldNumThreads = numThreads;
numThreads = nThreads;
// nothing needed as numThreads is used in #pragma omp parallel for directly
return oldNumThreads;
}
const char* getName() const CV_OVERRIDE
{
return "openmp";
}
};
}}} // namespace
#endif // OPENCV_CORE_PARALLEL_FOR_OPENMP_HPP

@ -0,0 +1,153 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef OPENCV_CORE_PARALLEL_FOR_TBB_HPP
#define OPENCV_CORE_PARALLEL_FOR_TBB_HPP
#include "opencv2/core/parallel/parallel_backend.hpp"
#include <opencv2/core/utils/logger.hpp>
#ifndef TBB_SUPPRESS_DEPRECATED_MESSAGES // supress warning
#define TBB_SUPPRESS_DEPRECATED_MESSAGES 1
#endif
#include "tbb/tbb.h"
#if !defined(TBB_INTERFACE_VERSION)
#error "Unknows/unsupported TBB version"
#endif
#if TBB_INTERFACE_VERSION >= 8000
#include "tbb/task_arena.h"
#endif
namespace cv { namespace parallel { namespace tbb {
using namespace ::tbb;
#if TBB_INTERFACE_VERSION >= 8000
static tbb::task_arena& getArena()
{
static tbb::task_arena tbbArena(tbb::task_arena::automatic);
return tbbArena;
}
#else
static tbb::task_scheduler_init& getScheduler()
{
static tbb::task_scheduler_init tbbScheduler(tbb::task_scheduler_init::deferred);
return tbbScheduler;
}
#endif
/** OpenMP parallel_for API implementation
*
* @sa setParallelForBackend
* @ingroup core_parallel_backend
*/
class ParallelForBackend : public ParallelForAPI
{
protected:
int numThreads;
int numThreadsMax;
public:
ParallelForBackend()
{
CV_LOG_INFO(NULL, "Initializing TBB parallel backend: TBB_INTERFACE_VERSION=" << TBB_INTERFACE_VERSION);
numThreads = 0;
#if TBB_INTERFACE_VERSION >= 8000
(void)getArena();
#else
(void)getScheduler();
#endif
}
virtual ~ParallelForBackend() {}
class CallbackProxy
{
const FN_parallel_for_body_cb_t& callback;
void* const callback_data;
const int tasks;
public:
inline CallbackProxy(int tasks_, FN_parallel_for_body_cb_t& callback_, void* callback_data_)
: callback(callback_), callback_data(callback_data_), tasks(tasks_)
{
// nothing
}
void operator()(const tbb::blocked_range<int>& range) const
{
this->callback(range.begin(), range.end(), callback_data);
}
void operator()() const
{
tbb::parallel_for(tbb::blocked_range<int>(0, tasks), *this);
}
};
virtual void parallel_for(int tasks, FN_parallel_for_body_cb_t body_callback, void* callback_data) CV_OVERRIDE
{
CallbackProxy task(tasks, body_callback, callback_data);
#if TBB_INTERFACE_VERSION >= 8000
getArena().execute(task);
#else
task();
#endif
}
virtual int getThreadNum() const CV_OVERRIDE
{
#if TBB_INTERFACE_VERSION >= 9100
return tbb::this_task_arena::current_thread_index();
#elif TBB_INTERFACE_VERSION >= 8000
return tbb::task_arena::current_thread_index();
#else
return 0;
#endif
}
virtual int getNumThreads() const CV_OVERRIDE
{
#if TBB_INTERFACE_VERSION >= 9100
return getArena().max_concurrency();
#elif TBB_INTERFACE_VERSION >= 8000
return numThreads > 0
? numThreads
: tbb::task_scheduler_init::default_num_threads();
#else
return getScheduler().is_active()
? numThreads
: tbb::task_scheduler_init::default_num_threads();
#endif
}
virtual int setNumThreads(int nThreads) CV_OVERRIDE
{
int oldNumThreads = numThreads;
numThreads = nThreads;
#if TBB_INTERFACE_VERSION >= 8000
auto& tbbArena = getArena();
if (tbbArena.is_active())
tbbArena.terminate();
if (numThreads > 0)
tbbArena.initialize(numThreads);
#else
auto& tbbScheduler = getScheduler();
if (tbbScheduler.is_active())
tbbScheduler.terminate();
if (numThreads > 0)
tbbScheduler.initialize(numThreads);
#endif
return oldNumThreads;
}
const char* getName() const CV_OVERRIDE
{
return "tbb";
}
};
}}} // namespace
#endif // OPENCV_CORE_PARALLEL_FOR_TBB_HPP

@ -0,0 +1,73 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef OPENCV_CORE_PARALLEL_BACKEND_HPP
#define OPENCV_CORE_PARALLEL_BACKEND_HPP
#include <memory>
namespace cv { namespace parallel {
#ifndef CV_API_CALL
#define CV_API_CALL
#endif
/** @addtogroup core_parallel_backend
* @{
* API below is provided to resolve problem of CPU resource over-subscription by multiple thread pools from different multi-threading frameworks.
* This is common problem for cases when OpenCV compiled threading framework is different from the Users Applications framework.
*
* Applications can replace OpenCV `parallel_for()` backend with own implementation (to reuse Application's thread pool).
*
* @note This call is not thread-safe. Consider calling this function from the `main()` before any other OpenCV processing functions (and without any other created threads).
*
* #### Intel TBB usage example:
*
* - include header with simple implementation of TBB backend:
* @snippet parallel_backend/example-tbb.cpp tbb_include
* - execute backend replacement code:
* @snippet parallel_backend/example-tbb.cpp tbb_backend
* - configuration of compiler/linker options is responsibility of Application's scripts
*
* #### OpenMP usage example:
*
* - include header with simple implementation of OpenMP backend:
* @snippet parallel_backend/example-openmp.cpp openmp_include
* - execute backend replacement code:
* @snippet parallel_backend/example-openmp.cpp openmp_backend
* - Configuration of compiler/linker options is responsibility of Application's scripts
*/
/** Interface for parallel_for backends implementations
*
* @sa setParallelForBackend
*/
class CV_EXPORTS ParallelForAPI
{
public:
virtual ~ParallelForAPI();
typedef void (CV_API_CALL *FN_parallel_for_body_cb_t)(int start, int end, void* data);
virtual void parallel_for(int tasks, FN_parallel_for_body_cb_t body_callback, void* callback_data) = 0;
virtual int getThreadNum() const = 0;
virtual int getNumThreads() const = 0;
virtual int setNumThreads(int nThreads) = 0;
virtual const char* getName() const = 0;
};
/** @brief Replace OpenCV parallel_for backend
*
* Application can replace OpenCV `parallel_for()` backend with own implementation.
*
* @note This call is not thread-safe. Consider calling this function from the `main()` before any other OpenCV processing functions (and without any other created threads).
*/
CV_EXPORTS void setParallelForBackend(const std::shared_ptr<ParallelForAPI>& api, bool propagateNumThreads = true);
//! @}
}} // namespace
#endif // OPENCV_CORE_PARALLEL_BACKEND_HPP

@ -570,6 +570,8 @@ static inline size_t getElemSize(int type) { return (size_t)CV_ELEM_SIZE(type);
/////////////////////////////// Parallel Primitives //////////////////////////////////
/** @brief Base class for parallel data processors
@ingroup core_parallel
*/
class CV_EXPORTS ParallelLoopBody
{
@ -579,17 +581,23 @@ public:
};
/** @brief Parallel data processor
@ingroup core_parallel
*/
CV_EXPORTS void parallel_for_(const Range& range, const ParallelLoopBody& body, double nstripes=-1.);
//! @ingroup core_parallel
class ParallelLoopBodyLambdaWrapper : public ParallelLoopBody
{
private:
std::function<void(const Range&)> m_functor;
public:
ParallelLoopBodyLambdaWrapper(std::function<void(const Range&)> functor) :
m_functor(functor)
{ }
inline
ParallelLoopBodyLambdaWrapper(std::function<void(const Range&)> functor)
: m_functor(functor)
{
// nothing
}
virtual void operator() (const cv::Range& range) const CV_OVERRIDE
{
@ -597,11 +605,14 @@ public:
}
};
inline void parallel_for_(const Range& range, std::function<void(const Range&)> functor, double nstripes=-1.)
//! @ingroup core_parallel
static inline
void parallel_for_(const Range& range, std::function<void(const Range&)> functor, double nstripes=-1.)
{
parallel_for_(range, ParallelLoopBodyLambdaWrapper(functor), nstripes);
}
/////////////////////////////// forEach method of cv::Mat ////////////////////////////
template<typename _Tp, typename Functor> inline
void Mat::forEach_impl(const Functor& operation) {

@ -45,6 +45,8 @@
#include <opencv2/core/utils/configuration.private.hpp>
#include <opencv2/core/utils/trace.private.hpp>
#include "opencv2/core/parallel/parallel_backend.hpp"
#if defined _WIN32 || defined WINCE
#include <windows.h>
#undef small
@ -145,9 +147,7 @@
# define CV_PARALLEL_FRAMEWORK "pthreads"
#endif
#ifdef CV_PARALLEL_FRAMEWORK
#include <atomic>
#endif
#include "parallel_impl.hpp"
@ -159,9 +159,36 @@ namespace cv {
ParallelLoopBody::~ParallelLoopBody() {}
namespace parallel {
static int numThreads = -1;
static
std::shared_ptr<ParallelForAPI>& getCurrentParallelForAPI()
{
static std::shared_ptr<ParallelForAPI> g_currentParallelForAPI;
return g_currentParallelForAPI;
}
ParallelForAPI::~ParallelForAPI()
{
// nothing
}
void setParallelForBackend(const std::shared_ptr<ParallelForAPI>& api, bool propagateNumThreads)
{
getCurrentParallelForAPI() = api;
if (propagateNumThreads && api)
{
setNumThreads(numThreads);
}
}
} // namespace
using namespace cv::parallel;
namespace {
#ifdef CV_PARALLEL_FRAMEWORK
#ifdef ENABLE_INSTRUMENTATION
static void SyncNodes(cv::instr::InstrNode *pNode)
{
@ -430,8 +457,6 @@ namespace {
typedef ParallelLoopBodyWrapper ProxyLoopBody;
#endif
static int numThreads = -1;
#if defined HAVE_TBB
#if TBB_INTERFACE_VERSION >= 8000
static tbb::task_arena tbbArena(tbb::task_arena::automatic);
@ -446,7 +471,7 @@ static inline int _initMaxThreads()
int maxThreads = omp_get_max_threads();
if (!utils::getConfigurationParameterBool("OPENCV_FOR_OPENMP_DYNAMIC_DISABLE", false))
{
omp_set_dynamic(maxThreads);
omp_set_dynamic(1);
}
return maxThreads;
}
@ -477,15 +502,11 @@ static SchedPtr pplScheduler;
#endif
#endif // CV_PARALLEL_FRAMEWORK
} // namespace anon
/* ================================ parallel_for_ ================================ */
#ifdef CV_PARALLEL_FRAMEWORK
static void parallel_for_impl(const cv::Range& range, const cv::ParallelLoopBody& body, double nstripes); // forward declaration
#endif
void parallel_for_(const cv::Range& range, const cv::ParallelLoopBody& body, double nstripes)
{
@ -500,7 +521,6 @@ void parallel_for_(const cv::Range& range, const cv::ParallelLoopBody& body, dou
if (range.empty())
return;
#ifdef CV_PARALLEL_FRAMEWORK
static std::atomic<bool> flagNestedParallelFor(false);
bool isNotNestedRegion = !flagNestedParallelFor.load();
if (isNotNestedRegion)
@ -519,16 +539,23 @@ void parallel_for_(const cv::Range& range, const cv::ParallelLoopBody& body, dou
}
}
else // nested parallel_for_() calls are not parallelized
#endif // CV_PARALLEL_FRAMEWORK
{
CV_UNUSED(nstripes);
body(range);
}
}
#ifdef CV_PARALLEL_FRAMEWORK
static
void parallel_for_cb(int start, int end, void* data)
{
CV_DbgAssert(data);
const cv::ParallelLoopBody& body = *(const cv::ParallelLoopBody*)data;
body(Range(start, end));
}
static void parallel_for_impl(const cv::Range& range, const cv::ParallelLoopBody& body, double nstripes)
{
using namespace cv::parallel;
if ((numThreads < 0 || numThreads > 1) && range.end - range.start > 1)
{
ParallelLoopBodyWrapperContext ctx(body, range, nstripes);
@ -540,6 +567,16 @@ static void parallel_for_impl(const cv::Range& range, const cv::ParallelLoopBody
return;
}
std::shared_ptr<ParallelForAPI>& api = getCurrentParallelForAPI();
if (api)
{
CV_CheckEQ(stripeRange.start, 0, "");
api->parallel_for(stripeRange.end, parallel_for_cb, (void*)&pbody);
ctx.finalize(); // propagate exceptions if exists
return;
}
#ifdef CV_PARALLEL_FRAMEWORK
#if defined HAVE_TBB
#if TBB_INTERFACE_VERSION >= 8000
@ -590,24 +627,25 @@ static void parallel_for_impl(const cv::Range& range, const cv::ParallelLoopBody
#endif
ctx.finalize(); // propagate exceptions if exists
return;
#endif // CV_PARALLEL_FRAMEWORK
}
else
{
body(range);
}
body(range);
}
#endif // CV_PARALLEL_FRAMEWORK
int getNumThreads(void)
{
#ifdef CV_PARALLEL_FRAMEWORK
std::shared_ptr<ParallelForAPI>& api = getCurrentParallelForAPI();
if (api)
{
return api->getNumThreads();
}
if(numThreads == 0)
if (numThreads == 0)
return 1;
#endif
#if defined HAVE_TBB
#if TBB_INTERFACE_VERSION >= 9100
@ -682,10 +720,15 @@ unsigned defaultNumberOfThreads()
void setNumThreads( int threads_ )
{
CV_UNUSED(threads_);
#ifdef CV_PARALLEL_FRAMEWORK
int threads = (threads_ < 0) ? defaultNumberOfThreads() : (unsigned)threads_;
numThreads = threads;
#endif
std::shared_ptr<ParallelForAPI>& api = getCurrentParallelForAPI();
if (api)
{
api->setNumThreads(numThreads);
}
#ifdef HAVE_TBB
@ -741,6 +784,12 @@ void setNumThreads( int threads_ )
int getThreadNum()
{
std::shared_ptr<ParallelForAPI>& api = getCurrentParallelForAPI();
if (api)
{
return api->getThreadNum();
}
#if defined HAVE_TBB
#if TBB_INTERFACE_VERSION >= 9100
return tbb::this_task_arena::current_thread_index();
@ -963,7 +1012,13 @@ int getNumberOfCPUs()
return nCPUs; // cached value
}
const char* currentParallelFramework() {
const char* currentParallelFramework()
{
std::shared_ptr<ParallelForAPI>& api = getCurrentParallelForAPI();
if (api)
{
return api->getName();
}
#ifdef CV_PARALLEL_FRAMEWORK
return CV_PARALLEL_FRAMEWORK;
#else

@ -30,6 +30,7 @@ if(NOT HAVE_opencv_cudaarithm OR NOT HAVE_opencv_cudafilters)
ocv_list_filterout(cpp_samples "/gpu/")
endif()
ocv_list_filterout(cpp_samples "real_time_pose_estimation/")
ocv_list_filterout(cpp_samples "parallel_backend/")
foreach(sample_filename ${cpp_samples})
set(package "cpp")
if(sample_filename MATCHES "tutorial_code")
@ -57,3 +58,11 @@ foreach(sample_filename ${cpp_samples})
endforeach()
include("tutorial_code/calib3d/real_time_pose_estimation/CMakeLists.txt" OPTIONAL)
# Standalone samples only
if(OpenCV_FOUND AND NOT CMAKE_VERSION VERSION_LESS "3.1")
add_subdirectory("example_cmake")
endif()
if(OpenCV_FOUND AND NOT CMAKE_VERSION VERSION_LESS "3.9")
add_subdirectory("tutorial_code/core/parallel_backend")
endif()

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.9)
find_package(OpenCV REQUIRED COMPONENTS opencv_core)
find_package(OpenMP)
if(OpenMP_FOUND)
project(opencv_example_openmp_backend)
add_executable(opencv_example_openmp_backend example-openmp.cpp)
target_link_libraries(opencv_example_openmp_backend PRIVATE
opencv_core
OpenMP::OpenMP_CXX
)
endif()
# TODO: find_package(TBB)
find_path(TBB_INCLUDE_DIR NAMES "tbb/tbb.h")
find_library(TBB_LIBRARY NAMES "tbb")
if(TBB_INCLUDE_DIR AND TBB_LIBRARY AND NOT OPENCV_EXAMPLE_SKIP_TBB)
project(opencv_example_tbb_backend)
add_executable(opencv_example_tbb_backend example-tbb.cpp)
target_include_directories(opencv_example_tbb_backend SYSTEM PRIVATE ${TBB_INCLUDE_DIR})
target_link_libraries(opencv_example_tbb_backend PRIVATE
opencv_core
${TBB_LIBRARY}
)
endif()

@ -0,0 +1,44 @@
#include "opencv2/core.hpp"
#include <iostream>
#include <chrono>
#include <thread>
//! [openmp_include]
#include "opencv2/core/parallel/backend/parallel_for.openmp.hpp"
//! [openmp_include]
namespace cv { // private.hpp
CV_EXPORTS const char* currentParallelFramework();
}
static
std::string currentParallelFrameworkSafe()
{
const char* framework = cv::currentParallelFramework();
if (framework)
return framework;
return std::string();
}
using namespace cv;
int main()
{
std::cout << "OpenCV builtin parallel framework: '" << currentParallelFrameworkSafe() << "' (nthreads=" << getNumThreads() << ")" << std::endl;
//! [openmp_backend]
//omp_set_dynamic(1);
cv::parallel::setParallelForBackend(std::make_shared<cv::parallel::openmp::ParallelForBackend>());
//! [openmp_backend]
std::cout << "New parallel backend: '" << currentParallelFrameworkSafe() << "'" << "' (nthreads=" << getNumThreads() << ")" << std::endl;
parallel_for_(Range(0, 20), [&](const Range range)
{
std::ostringstream out;
out << "Thread " << getThreadNum() << "(opencv=" << utils::getThreadID() << "): range " << range.start << "-" << range.end << std::endl;
std::cout << out.str() << std::flush;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
}

@ -0,0 +1,43 @@
#include "opencv2/core.hpp"
#include <iostream>
#include <chrono>
#include <thread>
//! [tbb_include]
#include "opencv2/core/parallel/backend/parallel_for.tbb.hpp"
//! [tbb_include]
namespace cv { // private.hpp
CV_EXPORTS const char* currentParallelFramework();
}
static
std::string currentParallelFrameworkSafe()
{
const char* framework = cv::currentParallelFramework();
if (framework)
return framework;
return std::string();
}
using namespace cv;
int main()
{
std::cout << "OpenCV builtin parallel framework: '" << currentParallelFrameworkSafe() << "' (nthreads=" << getNumThreads() << ")" << std::endl;
//! [tbb_backend]
cv::parallel::setParallelForBackend(std::make_shared<cv::parallel::tbb::ParallelForBackend>());
//! [tbb_backend]
std::cout << "New parallel backend: '" << currentParallelFrameworkSafe() << "'" << "' (nthreads=" << getNumThreads() << ")" << std::endl;
parallel_for_(Range(0, 20), [&](const Range range)
{
std::ostringstream out;
out << "Thread " << getThreadNum() << "(opencv=" << utils::getThreadID() << "): range " << range.start << "-" << range.end << std::endl;
std::cout << out.str() << std::flush;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
}
Loading…
Cancel
Save