在练习现代c ++ 17最佳实践的同时管理线程

时间:2018-03-22 23:13:53

标签: c++ multithreading mutex c++17 class-design

最初我曾考虑设计一个ThreadManager类来存储threads以及他们可以使用的data type objectsfunction type objects。该类负责管理标准多线程库中相关类型的内存,访问,传输,释放,锁定,解锁,连接和其他典型的常见功能。它最初旨在将包含线程及其id与特定线程可以访问的特定资源集相关联。

在阅读cppreference关于mutexshared_mutexlock_guardshared_lockstd::function<...>等的文档后,现在知道了mutexeslock_guards是不可复制的,如果我将类设置为模板以存储任意function objectsfunction pointerslambdasstd::function<>这一事实s作为此类容器中的std::function<>,该预期单例的类实例化只能存储特定的函数签名,从而限制它无法实​​例化任何其他声明签名。

关于mutexesshared_mutexes lock_guardsthreadspromises,{{1}的标准库中多线程库的这些行为和属性我想起我已经过度思考了这门课程的整体设计。

你可以通过我之前提到过的这个问题来参考我最初的设计尝试。 Storing arbitrary function objects into a class member container without knowing their declaration signature,这可以让你知道我在做什么。

了解更多关于他们的行为,属性和责任我想知道以下内容是否适合预期的设计过程。

而不是存储任何futuresmutexeslock_guardsthreadsdata type objects;仅仅存储function objects生成的ids会更有意义,让我的经理类更像是监控,录制和报告类型吗?

我的新意图是容器将线程的ID存储在关联的映射中作为其键以及相关的公共结构。结构将包含组合资源的所有职责和操作的属性列表。这可能允许支持以下某些功能:优先级队列,任务调度程序,发送和获取资源的命令调度程序,知道线程是否可用,这些类型的操作不会直接由此类完成但是通过通用功能模板。

例如:

threads

我打算在尝试维护面向c ++ 17的现代c ++的最佳实践的同时实现这种设计;这种设计是否适合通用,模块化,便携和高效使用?

2 个答案:

答案 0 :(得分:1)

线程和其他std原语就像原始指针。您应该构建一个不会暴露任何低级别的并发模型。 std线程原语为您提供了足够的工具。

了解管道中的一些新东西 - 执行器,流,协同程序,rangev3,monadic期货等。围绕它建模您的库。

尝试根据互斥体的原始使用,线程进入睡眠和唤醒,阻塞,原子和共享数据来制作表现良好的代码是一个陷阱。

举个例子:

struct thread_pool;
template<class T>
struct my_future:std::future<T> {
  template<class F>
  auto then( F&& f )&&
  -> std::future< std::result_of_t<F(T&&)> >;
  thread_pool* executor = 0;
};
template<>
struct my_future<void>:std::future<void> {
  template<class F>
  auto then( F&& f )&&
  -> std::future< std::result_of_t<F()> >;
  thread_pool* executor = 0;
};
struct thread_pool {
  template<class F=do_nothing>
  my_future<std::result_of_t<F()>> do_task(F&& f={});
};

这里我们讨论从任务到任务到任务的管道数据,并以增强的future<T>结束。通过分割(通过shared_future)和合并(future<X>加入future<Y>来生成future<X, Y>)来扩充它。

可能更进一步,建立一个基于流的系统:

template<class In>
using sink = std::function<void(In)>;
template<class Out>
using source = std::function<sink<Out>>;
template<class In, class Out>
using pipe = std::function< source<In>, sink<Out> >;

然后支持将源转换为异步源。

而不是建立一个巨大的抽象城堡,并希望它是完整的,阅读这些事情,当你遇到一个问题,其中一个解决实施只是足以解决你的问题。你没有在第一次尝试时从头开始编写be-all end-all线程系统,所以不要尝试。写一些有用的东西,然后下次写一个更好的。

答案 1 :(得分:1)

在收到用户Yakk's建议并对互斥锁,lock_guard,线程等行为进行更多研究后,我在ThreadPool video上找到https://www.youtube.com类。

这是一段代码:

ThreadPool.h

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <functional>
#include <condition_variable>
#include <thread>
#include <future>

namespace linx {

class ThreadPool final {
public:
    using Task = std::function<void()>;

private:    
    std::vector<std::thread> _threads;
    std::queue<Task>         _tasks;

    std::condition_variable  _event;
    std::mutex               _eventMutex;
    bool                     _stopping = false;    

public:
    explicit ThreadPool( std::size_t numThreads ) {
        start( numThreads );
    }    

    ~ThreadPool() {
        stop();
    }

    ThreadPool( const ThreadPool& c ) = delete;
    ThreadPool& operator=( const ThreadPool& c ) = delete;

    template<class T>
    auto enqueue( T task )->std::future<decltype(task())> {
        auto wrapper = std::make_shared<std::packaged_task<decltype(task()) ()>>( std::move( task ) );

        {
            std::unique_lock<std::mutex> lock( _eventMutex );
            _tasks.emplace( [=] {
                (*wrapper)();
            } );
        }

        _event.notify_one();
        return wrapper->get_future();
    }

private:
    void start( std::size_t numThreads ) {
        for( auto i = 0u; i < numThreads; ++i ) {
            _threads.emplace_back( [=] {
                while( true ) {
                    Task task;

                    {
                        std::unique_lock<std::mutex> lock{ _eventMutex };
                        _event.wait( lock, [=] { return _stopping || !_tasks.empty(); } );

                        if( _stopping && _tasks.empty() )
                            break;

                        task = std::move( _tasks.front() );
                        _tasks.pop();
                    }

                    task();
                }
            } );
        }
    }

    void stop() noexcept {
        {
            std::unique_lock<std::mutex> lock{ _eventMutex };
            _stopping = true;
        }

        _event.notify_all();

        for( auto& thread : _threads )
            thread.join();

    }
};

} // namespace linx

#endif // !THREAD_POOL_H

main.cpp

#include <iostream>
#include <sstream>
#include "ThreadPool.h"

int main() {
    {
        ThreadPool pool{ 4 }; // 4 threads

        auto f1 = pool.enqueue( [] {
            return 2;
        } );

        auto f2 = pool.enqueue( [] {
            return 4;
        } );

        auto a = f1.get();
        auto b = f2.get();

        auto f3 = pool.enqueue( [&] {
            return a + b;
        } );

        auto f4 = pool.enqueue( [] {
           return a * b;
        } );

        std::cout << "f1 = " << a << '\n' <<
                  << "f2 = " << b << '\n' <<
                  << "f3 = " << f3.get() << '\n' <<
                  << "f4 = " << f4.get() << '\n';
    }                 

    std::cout << "\nPress any key and enter to quit.\n";
    std::cin.get();
    return 0;
}

我认为这可能符合我的目的。我在这里做了一个基本的简单示例,但在我自己的IDE中使用了我的其他几个类,我将执行计时器包裹在我的线程池对象中,该对象有4个lambdas,如上所述,除了第一个lambda使用我的其他类生成100万个随机整数值使用mt19937用[1,1000]之间的随机设备播种,我的第二个lambda做的与上面相同,只是它使用mt19937chrono::high_resolution_clock$dateFromString一起用于浮点数来自(0,1.0)。第三个和第四个lambdas,按照上面的模式获取结果并将它们保存到ostringstream并返回该流。然后我打印出结果。在我的电脑上执行时间:英特尔四核至尊3.0Ghz,8GB内存,运行Win 7 64位家庭高级大约花费1720毫秒,为每个案例使用4个线程生成一个随机的百万值,而我的电脑正在使用所有四个核心。 / p>