一步一步写线程之八线程池的完善之二数据结构封装

一、数据容器

在前面分析过,不管是线程任务的封装还是同步数据队列的封装,都是需要一个数据结构的。一用来说,如果没有什么特殊的原因,开发者都是使用STL中数据结构。比如前面经常见到的std::queue,std::deque,std::vector,std::unordered_map,std::list等等。

但是大家都知道,标准库中的容器一般都是非线程安全的,所以在线程池中如果想使用这些容器,就需要自己处理一下相关的线程安全相关的控制。

二、线程的安全性

所谓线程的安全数据访问,其实在前面学习了很多方法和技术。在不同的平台上,可能也有不小的差异。但标准库的出现还是让这种差异弥合了很多。一般来说,在保证线程数据安全访问,也就是同步控制上,使用锁。也就是lock来锁住mutex。

但如何安全的使用锁,这就有很多技巧了。包括锁的粒度的大小,锁使用的时机和应用的地方。在下面的例子中,大家可以分析一下,如何进行更好的优化。

三、接口封装

数据结构如果使用标准库的容器,就会有一个问题,标准库的不同的容器,其对外的接口的名称是不相同的。当然,如何不对这些容器进行泛化,只是单纯的使用某种容器,就没有这种问题。但是如何想使用模板来整体使用这些容器呢?

这就需要一些辅助的技术来控制,既要达到在上层的接口统一性,又要保证在调用下层的库接口时的安全。这时,就用到了模板的SFINAE技术,当然,在高版本中,也可以使用Concepts(概念)。

四、实例

通过上面的分析,基本明白了这个抽象的数据结构封装的方式,其实就是在STL库的上层再封装一层,同时要保证多线程访问时的安全性,看下面的例子:

1、首先对原TaskQueue进行改造:

c 复制代码
#ifndef __TASKBUCKET_H__
#define __TASKBUCKET_H__

#include "common.h"
#include <iostream>
#include <mutex>
#include <queue>

template <typename T, typename Con = std::queue<T>> class TaskBucket final : public NoCopy {
public:
  TaskBucket() {}
  ~TaskBucket() {}

public:
  void Pop() { this->queue_.pop(); }
  void Push(T t) {
    std::cout << "Push bool:" << HasMemFuncCpp11<Con>::value << std::endl;
    std::unique_lock lock(this->mu_);
    if constexpr (HasMemFuncCpp11<Con>::value) {
      this->queue_.emplace_back(t);
    }
  }
  T Front() {
    std::unique_lock lock(this->mu_);
    return this->queue_.front();
  }
  T Back() {
    std::unique_lock lock(this->mu_);
    return this->queue_.back();
  }

  auto PopFront() {
    std::cout << "PopFront bool:" << HasMemFunc<Con>::value << std::endl;
    std::unique_lock lock(this->mu_);
    if constexpr (HasMemFunc<Con>::value) {
      T t = this->queue_.front();
      this->queue_.pop_front();
      return t;
    }
  }

  T PopBack() {
    std::cout << "PopBack bool:" << has_member_pop_back<Con>::value << std::endl;
    if constexpr (hasmem_pop_back<Con>::value) {
      std::unique_lock lock(this->mu_);
      T t = this->queue_.back();
      this->queue_.pop_back();
      return t;
    }
  }

private:
  std::mutex mu_;
  Con queue_;
};

上面的代码主动对三部分进行了处理,一部分是增加了类本身不允许拷贝;另外一个增加了对容器的再次抽象,即容器可以动态传入;第三部分就是增加了对容器的成员函数通过SFINAE进行了判断。这也是模板技术在实际场景中应用的一种方式,下面会对其进行分析。需要注意的是,这里使用的c++17以上的编译环境。下面看一下相关的代码:

c 复制代码
#ifndef __COMMON_H__
#define __COMMON_H__

#include <functional>
#include <utility>
using CallBackMsg = std::function<void(int *, int)>;
using Task = std::function<void(int)>;

class NoCopy {
protected:
  NoCopy() = default;
  ~NoCopy() = default;

public:
  NoCopy(const NoCopy &) = delete;
  NoCopy &operator=(const NoCopy &) = delete;
  NoCopy(NoCopy &&) = delete;
  NoCopy &operator=(NoCopy &&) = delete;
};
//第一种方式
template <typename T> struct HasMemFunc {
private:
  template <typename U> static auto Check(int) -> decltype(std::declval<U>().pop_front(), std::true_type());
  template <typename U> static std::false_type Check(...);

public:
  enum { value = std::is_same<decltype(Check<T>(0)), std::true_type>::value };
};

//Muduo库的使用方式(和上面类似):
template <typename T> struct hasPopFront {
  template <typename C> static char Check(decltype(&C::pop_front));
  template <typename C> static int32_t Check(...);
  const static bool value = sizeof(Check<T>(0)) == 1;
};

//第二种方式
template <typename T> using Type = void;

//处理无参
template <typename T, typename V = void> struct HasPop : std::false_type {};
template <typename T> struct HasPop<T, Type<decltype(std::declval<T>().pop_front())>> : std::true_type {};
//第二种方式-c++11后
template <typename T, typename V = void> struct HasMemFuncCpp11 : std::false_type {};

template <typename T>
struct HasMemFuncCpp11<T, Type<decltype(std::declval<T>().emplace_back(std::declval<typename T::value_type>()))>>
    : std::true_type {};

//第三种方式
#define HAS_MEM(mem)                                                                                             \
  template <typename T, typename... Args> struct hasmem_##mem {                                                 \
  private:                                                                                                             \
    template <typename U>                                                                                              \
    static auto Check(int) -> decltype(std::declval<U>().mem(std::declval<Args>()...), std::true_type());           \
    template <typename U> static std::false_type Check(...);                                                           \
                                                                                                                       \
  public:                                                                                                              \
    enum { value = std::is_same<decltype(Check<T>(0)), std::true_type>::value };                                       \
  };
HAS_MEM(pop)
HAS_MEM(pop_back)
HAS_MEM(push)
#endif // __COMMON_H__

在上面代码中主要分成了三种形式,第一种和第二种是使用检查函数是否存在的普通方式,虽然看上去是使用了模板,但对成员函数名称还是进行了限制。所以如果需要限制多种成员函数还得手动写多个类;第三种使用宏的方式自动产生相关的模板代码,这样会少写不少的重复的类似的代码。

这三种SFINAE的原理基本是一致的,虽然实现的手段有细节的不同。但都是通过decltype和std::declval(二者的区别可以查看前面的相关文章),通过是否能够符合模板自动匹配(即能否正常产生相关特化模板)来返回true 或false的value来达到判断是否存在函数的目的,然后通过if constexpr在编译期进行处理。Muduo库的使用方式前面分析过("SFINAE的技巧应用"),基本原理也是如此,只是直接使用定义的函数匹配来得到值而非使用std::true_type这种形式,但逻辑是一样的。

至于是否使用 std::is_same_v来替代 std::is_same,就看个人的兴趣了。

大家分析的时候儿可以从第一种开始,简单的按分析说明一对就明白了,然后再到第二种,特别是Type类型能否获取,是value值的关键。到第三种基本就是前面的抽象版,理解了前面的两种方式,第三种就非常简单了。

此处的封装未必是要求把所有的容器都封装起来,但它可以适应所有容器,什么意思呢?开发者可以根据自己的情况来选取指定的一两种容器来使用。当然,在实际的应用场景中,可能更多的是直接使用容器。这里之所以封装起来,就是为了给出一个更搞抽象的思路和方法。这点对于自己写容器而不使用STL中的容器的情况下更有意义。

随着GCC等编译器对c++20支持的逐渐普及化。估计明后年,可能就可以普遍的使用c++20编程了(ubuntu22默认的g++11对c++20已经支持了大部分),这样就可以使用Concept以及其它相关的新特性,开发工作会变得更简单一些。

五、总结

在分析完成数据结构的封装后,基本也就明白了在多线程中如何使用STL中的容器。但是,一般对线程安全的操作控制,都是使用锁。不管锁的粒度是大是小,对性能的影响一般来说都是比较大的。而多线程特别是线程池恰恰又都是在高性能的场景下应用,所以这时就需要开发者认真考虑加锁引起的性能损失。

当然,在后面可以考虑在某些情况下使用无锁编程技术,让数据的处理更快捷。但无锁编程也不是万能的,它也是有其相对的应用场景。万流归宗,还是需要开发者对整体技术的把握和实际应用场景进行综合考虑。

相关推荐
别NULL2 小时前
机试题——疯长的草
数据结构·c++·算法
CYBEREXP20083 小时前
MacOS M3源代码编译Qt6.8.1
c++·qt·macos
ZSYP-S4 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos4 小时前
c++------------------函数
开发语言·c++
yuanbenshidiaos4 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习4 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA4 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
tianmu_sama4 小时前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
羚羊角uou4 小时前
【C++】优先级队列以及仿函数
开发语言·c++
姚先生974 小时前
LeetCode 54. 螺旋矩阵 (C++实现)
c++·leetcode·矩阵