C++实现跳表:七、模块合并、压力测试

模块合并

通过前面的章节,我们已经将本 K-V 存储引擎拆分成的多个模块分别都介绍完了,但是会有一部份的内容没有涉及到,例如在跳表插入和删除数据的过程中,没有进行加锁,在多线程的情况下,可能会出现数据不一致的问题等。

现在我们将之前所有模块合并,同时补齐那些没有涉及到的内容。

不过因为篇幅原因,会有一部份的函数实现会省略,可以到 GitHub 上查看完整代码。

https://github.com/1412771048/skipList

合并过程:

  • 创建一个 skiplist.h 文件
  • 添加需要的头文件
#include <iostream>
#include <cstdlib>  // 随机函数
#include <cmath>
#include <cstring>
#include <mutex>    // 引入互斥锁
#include <fstream>  // 引入文件操作
  • 定义数据保存和加载时的文件路径
  • 定义互斥锁
#define STORE_FILE "store/dumpFile"  // 存储文件路径
std::mutex mtx; // 定义互斥锁
  • 对插入节点成员函数和删除节点成员函数进行加锁
// 只有在插入和删除的时候,才会进行加锁
template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
    mtx.lock();  // 在函数第一句加锁
    // ... 算法过程(省略)

    if (current != NULL && current->get_key() == key) {
        std::cout << "key: " << key << ", exists" << std::endl;
        // 在算法流程中有一个验证 key 是否存在的过程
        // 在此处需要提前 return,所以提前解锁
        mtx.unlock();
        return 1;
    }

    // ... 
    mtx.unlock();  // 函数执行完毕后解锁
    return 0;
}

template <typename K, typename V>
void SkipList<K, V>::delete_element(K key) {
    mtx.lock();  // 加锁
    // ... 算法过程(省略)
    mtx.unlock();  // 解锁
    return;
}

本章完整文件如下:

#include <iostream>
#include <cstdlib>  // 随机函数
#include <cmath>
#include <cstring>
#include <mutex>    // 引入互斥锁
#include <fstream>  // 引入文件操作

#define STORE_FILE "store/dumpFile"  // 存储文件路径

std::mutex mtx; // 定义互斥锁
std::string delimiter = ":";

template <typename K, typename V>
class Node {
public:
    Node() {}
    Node(K k, V v, int);
    ~Node();
    K get_key() const;
    V get_value() const;
    void set_value(V);
    Node<K, V> **forward;
    int node_level;
private:
    K key;
    V value;
};

// ... Node 类的所有方法实现(省略)

template <typename K, typename V>
class SkipList {
    public:
    SkipList(int);
    ~SkipList();
    int get_random_level();
    Node<K, V> *create_node(K, V, int);
    int insert_element(K, V);
    void display_list();
    bool search_element(K);
    void delete_element(K);
    void dump_file();
    void load_file();
    void clear(Node<K, V> *);
    int size();

private:
    void get_key_value_from_string(const std::string &str, std::string *key, std::string *value);
    bool is_valid_string(const std::string &str);

private:
    int _max_level;
    int _skip_list_level;
    Node<K, V> *_header;
    std::ofstream _file_writer;
    std::ifstream _file_reader;
    int _element_count;
};

// ... SkipList 类的大部分方法实现(省略)

// 只有在插入和删除的时候,才会进行加锁
template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
    mtx.lock();  // 在函数第一句加锁
    // ... 算法过程(省略)

    if (current != NULL && current->get_key() == key) {
        std::cout << "key: " << key << ", exists" << std::endl;
        // 在算法流程中有一个验证 key 是否存在的过程
        // 在此处需要提前 return,所以提前解锁
        mtx.unlock();
        return 1;
    }

    // ... 
    mtx.unlock();  // 函数执行完毕后解锁
    return 0;
}

template <typename K, typename V>
void SkipList<K, V>::delete_element(K key) {
    mtx.lock();  // 加锁
    // ... 算法过程(省略)
    mtx.unlock();  // 解锁
    return;
}

至此,我们已经将所有的分散的模块合并。

在将所有的模块合并了之后,可以将 skiplist.h include 到其他文件中,就可以使用该存储引擎了。

压力测试

将 “模块合并” 章节创建的 skiplist.h 包含到当前压力测试程序中。

测试程序主要的内容为编写在随机读写下,测试项目每秒可处理写请求数,和每秒可处理读请求数

具体可以通过多线程(pthread)以及计时(chrono)来执行插入和检索操作。

压力测试文件的内容如下:

// 引入必要的头文件
#include <iostream> // 用于输入输出流
#include <chrono> // 用于高精度时间测量
#include <cstdlib> // 包含一些通用的工具函数,如随机数生成
#include <pthread.h> // 用于多线程编程
#include <time.h> // 用于时间处理函数
#include "./skiplist.h" // 引入自定义的跳表实现

// 定义宏常量
#define NUM_THREADS 1 // 线程数量
#define TEST_COUNT 100000 // 测试用的数据量大小
SkipList<int, std::string> skipList(18); // 创建一个最大层级为18的跳表实例

// 插入元素的线程函数
void *insertElement(void* threadid) {
    long tid; // 线程ID
    tid = (long)threadid; // 将void*类型的线程ID转换为long型
    std::cout << tid << std::endl; // 输出线程ID
    int tmp = TEST_COUNT/NUM_THREADS; // 计算每个线程应该插入的元素数量
    // 循环插入元素
    for (int i=tid*tmp, count=0; count<tmp; i++) {
        count++;
        skipList.insert_element(rand() % TEST_COUNT, "a"); // 随机生成一个键,并插入带有"a"的元素
    }
    pthread_exit(NULL); // 退出线程
}

// 检索元素的线程函数
void *getElement(void* threadid) {
    long tid; // 线程ID
    tid = (long)threadid; // 将void*类型的线程ID转换为long型
    std::cout << tid << std::endl; // 输出线程ID
    int tmp = TEST_COUNT/NUM_THREADS; // 计算每个线程应该检索的元素数量
    // 循环检索元素
    for (int i=tid*tmp, count=0; count<tmp; i++) {
        count++;
        skipList.search_element(rand() % TEST_COUNT); // 随机生成一个键,并尝试检索
    }
    pthread_exit(NULL); // 退出线程
}

int main() {
    srand(time(NULL)); // 初始化随机数生成器
    {
        pthread_t threads[NUM_THREADS]; // 定义线程数组
        int rc; // 用于接收pthread_create的返回值
        int i; // 循环计数器

        auto start = std::chrono::high_resolution_clock::now(); // 开始计时

        // 创建插入元素的线程
        for( i = 0; i < NUM_THREADS; i++ ) {
            std::cout << "main() : creating thread, " << i << std::endl;
            rc = pthread_create(&threads[i], NULL, insertElement, (void *)i); // 创建线程

            if (rc) {
                std::cout << "Error:unable to create thread," << rc << std::endl;
                exit(-1); // 如果线程创建失败,退出程序
            }
        }

        void *ret; // 用于接收pthread_join的返回值
        // 等待所有插入线程完成
        for( i = 0; i < NUM_THREADS; i++ ) {
            if (pthread_join(threads[i], &ret) != 0 )  {
                perror("pthread_create() error");
                exit(3); // 如果线程等待失败,退出程序
            }
        }
        auto finish = std::chrono::high_resolution_clock::now(); // 结束计时
        std::chrono::duration<double> elapsed = finish - start; // 计算耗时
        std::cout << "insert elapsed:" << elapsed.count() << std::endl; // 输出插入操作耗时
    }

    // 下面的代码块与上面类似,用于创建并管理检索操作的线程
    {
        pthread_t threads[NUM_THREADS];
        int rc;
        int i;
        auto start = std::chrono::high_resolution_clock::now();

        for( i = 0; i < NUM_THREADS; i++ ) {
            std::cout << "main() : creating thread, " << i << std::endl;
            rc = pthread_create(&threads[i], NULL, getElement, (void *)i);

            if (rc) {
                std::cout << "Error:unable to create thread," << rc << std::endl;
                exit(-1);
            }
        }

        void *ret;
        for( i = 0; i < NUM_THREADS; i++ ) {
            if (pthread_join(threads[i], &ret) != 0 )  {
                perror("pthread_create() error");
                exit(3);
            }
        }

        auto finish = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> elapsed = finish - start;
        std::cout << "get elapsed:" << elapsed.count() << std::endl;
    }

    pthread_exit(NULL); // 主线程退出

    return 0;
}

在测试文件中的 NUM_THREADS(执行线程数量) 和 TEST_COUNT(测试数量) 可自定义。

编译测试文件,执行测试文件,观察输出结果。

编译命令如下:

g++ --std=c++11 main.cpp -o stress -pthread

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
跳表是一种数据结构,可以高效地实现有序链表和有序集合,可以用来实现排行榜。跳表是通过在原始链表中添加多级索引节点来加速查找操作的。 跳表实现思路如下: 1. 创建一个带有头节点的链表,头节点的值为负无穷大,尾节点的值为正无穷大。 2. 在原始链表中,插入新的节点时,根据节点的值,决定是否在当前层级上添加索引节点。添加索引节点的概率可以根据需求进行调整。 3. 使用索引节点可以将跳表分为多个层级(level),每一层级都是一个有序链表。 4. 查询操作时,从最高层级开始,从左向右逐层搜索,直到找到目标值所在的区间(比目标值大的最小节点和比目标值小的最大节点之间)。 5. 对于插入和删除操作,首先在最底层进行,然后根据概率决定是否在上层级插入或删除对应的节点。 使用跳表实现排行榜的步骤如下: 1. 创建一个跳表,每个节点存储着用户的信息,包括用户的排名、分数等。 2. 初始化排行榜时,将所有用户按照分数从大到小顺序插入跳表中。 3. 当有新的用户加入或者用户的分数发生变化时,根据新的分数更新用户节点的位置。 4. 当需要查询某个用户的排名时,可以通过跳表中的索引节点,快速定位到该用户所在的层级,然后在该层级中按照顺序遍历找到目标节点,并返回排名。 通过以上步骤,我们可以使用跳表高效地实现排行榜功能。跳表的插入、删除和查找操作的时间复杂度都可以达到O(log n),在大数据量下具有较高的效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值