棋牌类网站开发,专门做二手书的网站,门户网站建设重要性,深圳平台设计一、数据容器
在前面分析过#xff0c;不管是线程任务的封装还是同步数据队列的封装#xff0c;都是需要一个数据结构的。一用来说#xff0c;如果没有什么特殊的原因#xff0c;开发者都是使用STL中数据结构。比如前面经常见到的std::queue,std::deque,std::vector,std::…一、数据容器
在前面分析过不管是线程任务的封装还是同步数据队列的封装都是需要一个数据结构的。一用来说如果没有什么特殊的原因开发者都是使用STL中数据结构。比如前面经常见到的std::queue,std::deque,std::vector,std::unordered_map,std::list等等。 但是大家都知道标准库中的容器一般都是非线程安全的所以在线程池中如果想使用这些容器就需要自己处理一下相关的线程安全相关的控制。
二、线程的安全性
所谓线程的安全数据访问其实在前面学习了很多方法和技术。在不同的平台上可能也有不小的差异。但标准库的出现还是让这种差异弥合了很多。一般来说在保证线程数据安全访问也就是同步控制上使用锁。也就是lock来锁住mutex。 但如何安全的使用锁这就有很多技巧了。包括锁的粒度的大小锁使用的时机和应用的地方。在下面的例子中大家可以分析一下如何进行更好的优化。
三、接口封装
数据结构如果使用标准库的容器就会有一个问题标准库的不同的容器其对外的接口的名称是不相同的。当然如何不对这些容器进行泛化只是单纯的使用某种容器就没有这种问题。但是如何想使用模板来整体使用这些容器呢 这就需要一些辅助的技术来控制既要达到在上层的接口统一性又要保证在调用下层的库接口时的安全。这时就用到了模板的SFINAE技术当然在高版本中也可以使用Concepts概念。
四、实例
通过上面的分析基本明白了这个抽象的数据结构封装的方式其实就是在STL库的上层再封装一层同时要保证多线程访问时的安全性看下面的例子 1、首先对原TaskQueue进行改造
#ifndef __TASKBUCKET_H__
#define __TASKBUCKET_H__#include common.h
#include iostream
#include mutex
#include queuetemplate typename T, typename Con std::queueT class TaskBucket final : public NoCopy {
public:TaskBucket() {}~TaskBucket() {}public:void Pop() { this-queue_.pop(); }void Push(T t) {std::cout Push bool: HasMemFuncCpp11Con::value std::endl;std::unique_lock lock(this-mu_);if constexpr (HasMemFuncCpp11Con::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: HasMemFuncCon::value std::endl;std::unique_lock lock(this-mu_);if constexpr (HasMemFuncCon::value) {T t this-queue_.front();this-queue_.pop_front();return t;}}T PopBack() {std::cout PopBack bool: has_member_pop_backCon::value std::endl;if constexpr (hasmem_pop_backCon::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进行了判断。这也是模板技术在实际场景中应用的一种方式下面会对其进行分析。需要注意的是这里使用的c17以上的编译环境。下面看一下相关的代码
#ifndef __COMMON_H__
#define __COMMON_H__#include functional
#include utility
using CallBackMsg std::functionvoid(int *, int);
using Task std::functionvoid(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::declvalU().pop_front(), std::true_type());template typename U static std::false_type Check(...);public:enum { value std::is_samedecltype(CheckT(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(CheckT(0)) 1;
};//第二种方式
template typename T using Type void;//处理无参
template typename T, typename V void struct HasPop : std::false_type {};
template typename T struct HasPopT, Typedecltype(std::declvalT().pop_front()) : std::true_type {};
//第二种方式-c11后
template typename T, typename V void struct HasMemFuncCpp11 : std::false_type {};template typename T
struct HasMemFuncCpp11T, Typedecltype(std::declvalT().emplace_back(std::declvaltypename 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::declvalU().mem(std::declvalArgs()...), std::true_type()); \template typename U static std::false_type Check(...); \\public: \enum { value std::is_samedecltype(CheckT(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等编译器对c20支持的逐渐普及化。估计明后年可能就可以普遍的使用c20编程了ubuntu22默认的g11对c20已经支持了大部分这样就可以使用Concept以及其它相关的新特性开发工作会变得更简单一些。
五、总结
在分析完成数据结构的封装后基本也就明白了在多线程中如何使用STL中的容器。但是一般对线程安全的操作控制都是使用锁。不管锁的粒度是大是小对性能的影响一般来说都是比较大的。而多线程特别是线程池恰恰又都是在高性能的场景下应用所以这时就需要开发者认真考虑加锁引起的性能损失。 当然在后面可以考虑在某些情况下使用无锁编程技术让数据的处理更快捷。但无锁编程也不是万能的它也是有其相对的应用场景。万流归宗还是需要开发者对整体技术的把握和实际应用场景进行综合考虑。