快速路径数据包处理的配置更新
configuration update on a fast path packet processing
我们有一个使用线程池处理传入数据包的应用程序。每个线程都有一个在数据包处理时使用的配置。
我们目前正在使用互斥锁在检查配置是否更改之前进行锁定。
这使得线程花费太多时间来锁定互斥锁以检查是否有配置更新。我们想知道你们是否可以建议更快的替代方案。
实现是用 C++
此致。
解决此问题的一种可能方法是通过 atomics via std::atomic
。以下是您问题的简化版本的简化解决方案。在下文中,您的问题已简化为单处理器线程(原则上多个情况相同)。第一个版本的解决方案 "leaks" 配置更改。对于足够少的配置更改(至少根据我的经验,这是一种非常常见的情况),这可能是可以接受的。否则,我将在最后描述两种解决方法。
假设您从以下配置开始 class:
#include <thread>
#include <vector>
#include <list>
#include <iostream>
#include <atomic>
#include <chrono>
constexpr int init_config_val = 3;
struct config{
int m_val = init_config_val;
};
配置只有一个值字段,m_val
。
现在让我们为指向配置的原子指针和配置列表设置类型:
using config_atomic_ptr_t = std::atomic<config *>;
using config_list_t = std::list<config>;
线程进程采用指向原子配置指针的指针。当它需要访问配置时,它调用 std::atomic::load
。
void process(config_atomic_ptr_t *conf) {
while(true) {
const config *current_config = conf->load();
...
}
}
(请注意,上面显示了线程在每次迭代时检查配置;在某些类型的应用程序中,检查它可能就足够了 "often enough"。)
当不同的线程想要设置配置时,它调用以下函数:
void modify_config(config_list_t &configs, config_atomic_ptr_t ¤t_config, config conf) {
configs.push_back(conf);
current_config.store(&*configs.rbegin());
}
该函数引用配置列表、引用原子配置指针和新配置对象。它将配置对象推送到列表的末尾,然后使用 std::atomic::store
将指针设置为列表中的末尾元素。
main
可以这样设置:
int main() {
config_list_t configs;
configs.push_back(config{});
config_atomic_ptr_t current_config{&*configs.rbegin()};
std::thread processor(process, ¤t_config);
config new_conf{init_config_val + 1};
modify_config(configs, current_config, new_conf);
processor.join();
}
如前所述,每次配置更改都会将一个新的配置对象推送到列表中,因此该程序实际上具有无限的内存需求。
至少根据我的经验,原则上许多应用程序需要支持配置更改,但预计这种情况很少见。如果是这样,上述解决方案可能是可以接受的。 (实际上,您可以通过删除列表来简化事情,只需在堆上分配新配置即可。)
如果没有,至少有两种选择。
第一个替代方案涉及修复上述问题,如下所示:
- 在
config
中,添加另一个描述配置版本的字段 - 例如,一个整数。
- 向
process
线程发送一个指向 std::atomic<int>
的指针。
- 周期性地(比如每 1000 次迭代一次),线程将检查它正在使用的配置的版本,并设置
std::atomic<int>
来反映它。
- 清理线程(可能是主线程)也会定期检查
std::atomic<int>
的值,并相应地清理列表。
第二种方法是将线程函数的指针传递给 boost::lockfree::queue
。在每次迭代(或每迭代次数一次)中,线程可以检查队列中的新配置,然后使用它。
完整示例
#include <thread>
#include <vector>
#include <list>
#include <iostream>
#include <atomic>
#include <chrono>
constexpr int init_config_val = 3;
struct config{
int m_val = init_config_val;
};
using config_atomic_ptr_t = std::atomic<config *>;
using config_list_t = std::list<config>;
void process(config_atomic_ptr_t *conf) {
while(true) {
const config *current_config = conf->load();
if(current_config->m_val != init_config_val)
break;
}
}
void modify_config(config_list_t &configs, config_atomic_ptr_t ¤t_config, config conf) {
configs.push_back(conf);
current_config.store(&*configs.rbegin());
}
int main() {
using namespace std::chrono_literals;
config_list_t configs;
configs.push_back(config{});
config_atomic_ptr_t current_config{&*configs.rbegin()};
std::thread processor(process, ¤t_config);
std::this_thread::sleep_for(1s);
config new_conf{init_config_val + 1};
modify_config(configs, current_config, new_conf);
processor.join();
}
我们有一个使用线程池处理传入数据包的应用程序。每个线程都有一个在数据包处理时使用的配置。
我们目前正在使用互斥锁在检查配置是否更改之前进行锁定。
这使得线程花费太多时间来锁定互斥锁以检查是否有配置更新。我们想知道你们是否可以建议更快的替代方案。 实现是用 C++
此致。
解决此问题的一种可能方法是通过 atomics via std::atomic
。以下是您问题的简化版本的简化解决方案。在下文中,您的问题已简化为单处理器线程(原则上多个情况相同)。第一个版本的解决方案 "leaks" 配置更改。对于足够少的配置更改(至少根据我的经验,这是一种非常常见的情况),这可能是可以接受的。否则,我将在最后描述两种解决方法。
假设您从以下配置开始 class:
#include <thread>
#include <vector>
#include <list>
#include <iostream>
#include <atomic>
#include <chrono>
constexpr int init_config_val = 3;
struct config{
int m_val = init_config_val;
};
配置只有一个值字段,m_val
。
现在让我们为指向配置的原子指针和配置列表设置类型:
using config_atomic_ptr_t = std::atomic<config *>;
using config_list_t = std::list<config>;
线程进程采用指向原子配置指针的指针。当它需要访问配置时,它调用 std::atomic::load
。
void process(config_atomic_ptr_t *conf) {
while(true) {
const config *current_config = conf->load();
...
}
}
(请注意,上面显示了线程在每次迭代时检查配置;在某些类型的应用程序中,检查它可能就足够了 "often enough"。)
当不同的线程想要设置配置时,它调用以下函数:
void modify_config(config_list_t &configs, config_atomic_ptr_t ¤t_config, config conf) {
configs.push_back(conf);
current_config.store(&*configs.rbegin());
}
该函数引用配置列表、引用原子配置指针和新配置对象。它将配置对象推送到列表的末尾,然后使用 std::atomic::store
将指针设置为列表中的末尾元素。
main
可以这样设置:
int main() {
config_list_t configs;
configs.push_back(config{});
config_atomic_ptr_t current_config{&*configs.rbegin()};
std::thread processor(process, ¤t_config);
config new_conf{init_config_val + 1};
modify_config(configs, current_config, new_conf);
processor.join();
}
如前所述,每次配置更改都会将一个新的配置对象推送到列表中,因此该程序实际上具有无限的内存需求。
至少根据我的经验,原则上许多应用程序需要支持配置更改,但预计这种情况很少见。如果是这样,上述解决方案可能是可以接受的。 (实际上,您可以通过删除列表来简化事情,只需在堆上分配新配置即可。)
如果没有,至少有两种选择。
第一个替代方案涉及修复上述问题,如下所示:
- 在
config
中,添加另一个描述配置版本的字段 - 例如,一个整数。 - 向
process
线程发送一个指向std::atomic<int>
的指针。 - 周期性地(比如每 1000 次迭代一次),线程将检查它正在使用的配置的版本,并设置
std::atomic<int>
来反映它。 - 清理线程(可能是主线程)也会定期检查
std::atomic<int>
的值,并相应地清理列表。
第二种方法是将线程函数的指针传递给 boost::lockfree::queue
。在每次迭代(或每迭代次数一次)中,线程可以检查队列中的新配置,然后使用它。
完整示例
#include <thread>
#include <vector>
#include <list>
#include <iostream>
#include <atomic>
#include <chrono>
constexpr int init_config_val = 3;
struct config{
int m_val = init_config_val;
};
using config_atomic_ptr_t = std::atomic<config *>;
using config_list_t = std::list<config>;
void process(config_atomic_ptr_t *conf) {
while(true) {
const config *current_config = conf->load();
if(current_config->m_val != init_config_val)
break;
}
}
void modify_config(config_list_t &configs, config_atomic_ptr_t ¤t_config, config conf) {
configs.push_back(conf);
current_config.store(&*configs.rbegin());
}
int main() {
using namespace std::chrono_literals;
config_list_t configs;
configs.push_back(config{});
config_atomic_ptr_t current_config{&*configs.rbegin()};
std::thread processor(process, ¤t_config);
std::this_thread::sleep_for(1s);
config new_conf{init_config_val + 1};
modify_config(configs, current_config, new_conf);
processor.join();
}