基于C++标准库实现定时器类

基于C++标准库实现定时器类

定时器类是多线程编程中经常设计到的工具类
简单的定时器原理其实很简单(是不是有点GNU is not unix的味道;):

  • 创建一个新线程
  • 在那个线程里等待
  • 等待指定时长后做任务

python标准库中就有这么一个定时器类:threading.Timer
此类表示一个操作应该在等待一定的时间之后运行 --- 相当于一个定时器。
Timer 类是 Thread 类的子类,因此可以像一个自定义线程一样工作。
与线程一样,通过调用 start() 方法启动定时器。 而 cancel() 方法可以停止计时器(在计时结束前)。

例如:

def hello():print("hello, world")t = Timer(30.0, hello)
t.start()  # after 30 seconds, "hello, world" will be printed

class threading.Timer(interval, function, args=None, kwargs=None) 创建一个定时器,在经过 interval 秒的间隔事件后,将会用参数 args 和关键字参数 kwargs 调用 function。
如果 args 为 None (默认值),则会使用一个空列表。如果 kwargs 为 None (默认值),则会使用一个空字典。

start() 启动定时器。

cancel() 停止定时器并取消执行计时器将要执行的操作。仅当计时器仍处于等待状态时有效。

接下来我将给出两种C++的Timer实现,接口类似于python的threading.Timer,不过精度为秒级的,其原因有二:

  • 实现代码参考了Posix多线程编程里的alarm实例程序,为了方便大家对比C语言版本的实现,这里也以秒为单位
  • 避免一上来过多的涉及C++标准库中<chrono>的用法,使代码逻辑更清晰的集中在定时器相关的部分

当然,作为一个在产品级代码中可用的Timer,精度至少应该在毫秒级才行,所以文章最后也会给出精度在微秒的代码实现的链接。

首先,给出C++版本的Timer的接口定义:
几乎完全仿照python的threading.Timer,

class Timer {
public:typedef std::function<void ()> Callback;Timer(int interval, Callback function); void start();void cancel(); 
};
  • Callback:类型为std::function<void ()>,即返回类型为void的“函数”,当然在C++里可以是普通函数,函数对象,lambda等。
  • Timer(interval, function):构造函数,创建一个Timer,interval秒后到期(相对于调用start函数时的时间点),回调函数为function。
  • start():启动定时器。
  • cancel():停止定时器并取消执行计时器将要执行的操作。

在给出C++的实现前,我们先给出测试驱动程序。测试驱动程序来源于《Posix多线程程序设计》(英文原版书名为Programming with POSIX Threads)里的闹钟实例程序。
而我接下来的介绍顺序源于我的编码实现顺序,如下:

  • 先给出书中基于pthread的多线程版本的实例代码,C代码
  • 将C版本的代码转化成等价的python代码,基于threading.Timer接口实现的版本
  • 将python版本的代码,转化成C++版本,并基于C++的Timer类接口,得到C++的Timer类的测试驱动代码
  • 实现C++版的Timer类,并且编译测试驱动代码,运行验证

那么,我先给出基于pthread的多线程版本的实例代码,C代码:

/** alarm_fork.c** This version of alarm.c uses pthread_create to create a* separate thread to wait for each alarm to expire.*/
#include <pthread.h>
#include "errors.h"typedef struct alarm_tag {int         seconds;char        message[64];
} alarm_t;void *alarm_thread (void *arg)
{alarm_t *alarm = (alarm_t*)arg;int status;status = pthread_detach (pthread_self ());if (status != 0)err_abort (status, "Detach thread");sleep (alarm->seconds);printf ("(%d) %s\n", alarm->seconds, alarm->message);free (alarm);return NULL;
}int main (int argc, char *argv[])
{int status;char line[128];alarm_t *alarm;pthread_t thread;while (1) {printf ("Alarm> ");if (fgets (line, sizeof (line), stdin) == NULL) exit (0);if (strlen (line) <= 1) continue;alarm = (alarm_t*)malloc (sizeof (alarm_t));if (alarm == NULL)errno_abort ("Allocate alarm");/** Parse input line into seconds (%d) and a message* (%64[^\n]), consisting of up to 64 characters* separated from the seconds by whitespace.*/if (sscanf (line, "%d %64[^\n]", &alarm->seconds, alarm->message) < 2) {fprintf (stderr, "Bad command\n");free (alarm);} else {status = pthread_create (&thread, NULL, alarm_thread, alarm);if (status != 0)err_abort (status, "Create alarm thread");}}
}

代码的完整说明参见《Posix多线程程序设计》 1.5.3章节,这里就不再搬运原文了。

接下来是移植成python版本的代码:

#!/usr/bin/env python3from threading import Timerclass Alarm:def __init__(self, seconds:int, message:str):self.seconds = secondsself.message = messagedef callback(alarm:Alarm):print("({}) {}\n".format(alarm.seconds, alarm.message))if __name__ == "__main__":while True:line = input("Alarm> ")if len(line) <= 1:continuetry:seconds, *message = line.split(' ')alarm = Alarm(int(seconds), ' '.join(message))t = Timer(interval=int(seconds), function=callback, args=(alarm, ))t.start()except:print("Bad command")

python版本的代码大家有兴趣的可以在本地运行一下,看看效果;)

再然后,我把这段代码翻译成C++版本的,基于C++的Timer类接口:

cpp

#include "timer.hpp"
#include <cstdlib>
#include <string>
#include <memory>
#include <iostream>struct Alarm {Alarm(int seconds_, const std::string& message_): seconds(seconds_), message(message_) {}int seconds;std::string message;
};void callback(std::shared_ptr<Alarm> alarm) {std::cout << "(" << alarm->seconds << ") " << alarm->message << std::endl;
}std::tuple<int, std::string> parse_command(const std::string& line) {auto pos = line.find(' ');if (pos == std::string::npos)throw std::runtime_error("invalid line: separator not found");int seconds = std::stoi(line.substr(0, pos));std::string message = line.substr(pos+1);return std::make_tuple(seconds, message);
}int main()
{std::string line;int seconds;std::string message;while (true) {std::cout << "Alarm> ";if (!std::getline(std::cin, line)) exit(0);if (line.length() <= 1) continue;try {std::tie(seconds, message) = parse_command(line);auto alarm = std::make_shared<Alarm>(seconds, message);Timer t(seconds, std::bind(callback, alarm));t.start();} catch (const std::exception& e) {std::cout << "Bad command" << std::endl;}}
}

这样我们就有了C++版本Timer类的测试驱动程序,并且可以跟C和python版本的代码对比运行。

接下来给出C++版本的基于创建线程的Timer实现:

cpp

#pragma once#include <thread>
#include <chrono>
#include <atomic>
#include <functional>class Timer {
public:typedef std::function<void ()> Callback;Timer(int interval, Callback function) {pimpl = std::make_shared<Impl>(interval, function);}void start() {std::thread t([pimpl=pimpl]() {if(!pimpl->active.load()) return;std::this_thread::sleep_for(std::chrono::seconds(pimpl->interval));if(!pimpl->active.load()) return;pimpl->function();});t.detach();}void cancel() {pimpl->active.store(false);}private:struct Impl {Impl(int interval_, Callback function_): interval(interval_), function(function_) {}int interval;Callback function;std::atomic<bool> active{true};};private:std::shared_ptr<Impl> pimpl;
};

C++实现部分,基本上是C版本的代码抽离和封装,并把相关函数替换成C++标准库的实现而已。不过Timer类麻雀虽小,但五脏俱全,其中用到的C++标准库组件有:

  • std::function:用于抽象到期回调函数
  • std::shared_ptr:用于管理Timer::Impl的生命周期
  • std::atomic:用于cancel Timer的flag,保证线程安全
  • std::thread:用于Timer线程,sleep指定时间,然后调用回调函数
  • std::chrono:C++标准库中时间相关的实现都在其中
  • C++ lambda:Timer线程的target函数,捕获了this->pimpl,保证了Timerl::Impl对象不会因为Timer对象的析构而析构

这里还用的了Pimpl惯用法,虽然目前是把接口和实现都放在了头文件里,但标准的做法是Timer的成员函数实现和Timer::Impl实现都放到源文件中,
这样头文件里可以去掉除了std::shared_ptr和std::function以外的依赖。

这个Timer类的实现优缺点是十分明显的:优点是代码简洁,一目了然,缺点是太过简洁,每个Timer需要一个线程去运行,系统资源消耗大。
于是就引出了基于条件变量版本的Timer,实现“参考”了《Posix多线程程序设计》3.3.4章节提到闹钟实例的最终版本(与其说“参考”,改成“直译”也不为过;)。

照例,我先给出基于pthread的条件变量版本的实例代码,C代码:

c

/** alarm_cond.c** This is an enhancement to the alarm_mutex.c program, which* used only a mutex to synchronize access to the shared alarm* list. This version adds a condition variable. The alarm* thread waits on this condition variable, with a timeout that* corresponds to the earliest timer request. If the main thread* enters an earlier timeout, it signals the condition variable* so that the alarm thread will wake up and process the earlier* timeout first, requeueing the later request.*/
#include <pthread.h>
#include <time.h>
#include "errors.h"/** The "alarm" structure now contains the time_t (time since the* Epoch, in seconds) for each alarm, so that they can be* sorted. Storing the requested number of seconds would not be* enough, since the "alarm thread" cannot tell how long it has* been on the list.*/
typedef struct alarm_tag {struct alarm_tag    *link;int                 seconds;time_t              time;   /* seconds from EPOCH */char                message[64];
} alarm_t;pthread_mutex_t alarm_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t alarm_cond = PTHREAD_COND_INITIALIZER;
alarm_t *alarm_list = NULL;
time_t current_alarm = 0;/** Insert alarm entry on list, in order.*/
void alarm_insert (alarm_t *alarm)
{int status;alarm_t **last, *next;/** LOCKING PROTOCOL:* * This routine requires that the caller have locked the* alarm_mutex!*/last = &alarm_list;next = *last;while (next != NULL) {if (next->time >= alarm->time) {alarm->link = next;*last = alarm;break;}last = &next->link;next = next->link;}/** If we reached the end of the list, insert the new alarm* there.  ("next" is NULL, and "last" points to the link* field of the last item, or to the list header.)*/if (next == NULL) {*last = alarm;alarm->link = NULL;}
#ifdef DEBUGprintf ("[list: ");for (next = alarm_list; next != NULL; next = next->link)printf ("%d(%d)[\"%s\"] ", next->time,next->time - time (NULL), next->message);printf ("]\n");
#endif/** Wake the alarm thread if it is not busy (that is, if* current_alarm is 0, signifying that it's waiting for* work), or if the new alarm comes before the one on* which the alarm thread is waiting.*/if (current_alarm == 0 || alarm->time < current_alarm) {current_alarm = alarm->time;status = pthread_cond_signal (&alarm_cond);if (status != 0)err_abort (status, "Signal cond");}
}/** The alarm thread's start routine.*/
void *alarm_thread (void *arg)
{alarm_t *alarm;struct timespec cond_time;time_t now;int status, expired;/** Loop forever, processing commands. The alarm thread will* be disintegrated when the process exits. Lock the mutex* at the start -- it will be unlocked during condition* waits, so the main thread can insert alarms.*/status = pthread_mutex_lock (&alarm_mutex);if (status != 0)err_abort (status, "Lock mutex");while (1) {/** If the alarm list is empty, wait until an alarm is* added. Setting current_alarm to 0 informs the insert* routine that the thread is not busy.*/current_alarm = 0;while (alarm_list == NULL) {status = pthread_cond_wait (&alarm_cond, &alarm_mutex);if (status != 0)err_abort (status, "Wait on cond");}alarm = alarm_list;alarm_list = alarm->link;now = time (NULL);expired = 0;if (alarm->time > now) {
#ifdef DEBUGprintf ("[waiting: %d(%d)\"%s\"]\n", alarm->time,alarm->time - time (NULL), alarm->message);
#endifcond_time.tv_sec = alarm->time;cond_time.tv_nsec = 0;current_alarm = alarm->time;while (current_alarm == alarm->time) {status = pthread_cond_timedwait (&alarm_cond, &alarm_mutex, &cond_time);if (status == ETIMEDOUT) {expired = 1;break;}if (status != 0)err_abort (status, "Cond timedwait");}if (!expired)alarm_insert (alarm);} elseexpired = 1;if (expired) {printf ("(%d) %s\n", alarm->seconds, alarm->message);free (alarm);}}
}int main (int argc, char *argv[])
{int status;char line[128];alarm_t *alarm;pthread_t thread;status = pthread_create (&thread, NULL, alarm_thread, NULL);if (status != 0)err_abort (status, "Create alarm thread");while (1) {printf ("Alarm> ");if (fgets (line, sizeof (line), stdin) == NULL) exit (0);if (strlen (line) <= 1) continue;alarm = (alarm_t*)malloc (sizeof (alarm_t));if (alarm == NULL)errno_abort ("Allocate alarm");/** Parse input line into seconds (%d) and a message* (%64[^\n]), consisting of up to 64 characters* separated from the seconds by whitespace.*/if (sscanf (line, "%d %64[^\n]", &alarm->seconds, alarm->message) < 2) {fprintf (stderr, "Bad command\n");free (alarm);} else {status = pthread_mutex_lock (&alarm_mutex);if (status != 0)err_abort (status, "Lock mutex");alarm->time = time (NULL) + alarm->seconds;/** Insert the new alarm into the list of alarms,* sorted by expiration time.*/alarm_insert (alarm);status = pthread_mutex_unlock (&alarm_mutex);if (status != 0)err_abort (status, "Unlock mutex");}}
}

代码有些长,代码说明见《Posix多线程程序设计》3.3.4章节,我这里啰嗦几句,简单总结一下,基于条件变量实现定时器的原理:

  • 程序维护了一个有序的定时器列表,顺序按照到期时间从小到大排列
  • 独立的alarm_thread线程函数,实现定时器到期回调的逻辑,通过条件变量的pthread_cond_wait函数,处理定时器列表为空的情况,以及等待队列中最早的定时器到期,以及有更早的定时器插入队列的情况
  • 函数alarm_insert函数,实现将定时器插入到定时器列表的逻辑,并在定时器列表原先为空的情况下,或者在插入的定时器到期时间早于定时间列表中最早到期时,通知alarm_thread线程,唤醒pthread_cond_wait函数。

由于基于pthread的条件变量版本的实例代码中,增加了DEBUG宏和调试代码,所以我的C++版本的测试驱动程序也需要做相应的调整:

cpp

#include "timer.hpp"
#include <cstdlib>
#include <string>
#include <memory>
#include <iostream>struct Alarm {Alarm(int seconds_, const std::string& message_): seconds(seconds_), message(message_) {}int seconds;std::string message;
};void callback(std::shared_ptr<Alarm> alarm) {std::cout << "(" << alarm->seconds << ") " << alarm->message << std::endl;
}std::tuple<int, std::string> parse_command(const std::string& line) {auto pos = line.find(' ');if (pos == std::string::npos)throw std::runtime_error("invalid line: separator not found");int seconds = std::stoi(line.substr(0, pos));std::string message = line.substr(pos+1);return std::make_tuple(seconds, message);
}int main()
{std::string line;int seconds;std::string message;while (true) {std::cout << "Alarm> ";if (!std::getline(std::cin, line)) quick_exit(0);if (line.length() <= 1) continue;try {std::tie(seconds, message) = parse_command(line);auto alarm = std::make_shared<Alarm>(seconds, message);Timer t(seconds, std::bind(callback, alarm));
#ifdef DEBUGt.set_message(message);
#endift.start();} catch (const std::exception& e) {std::cout << "Bad command" << std::endl;}}
}

其实修改就只有两处: - 将exit改成了quick_exit(因为下面的C++实现里,对应于C版本的alarm_thread线程是detach的,exit时会挂起在cond的wait上) - 增加t.set_message调用,用来打印debug信息

alarm_cond.png

接下来给出条件变量版本的C++的Timer类实现,如果C版本的代码看得懂,那C++版本的代码就可以说是一目了然,毕竟C++的版本是“直译”过来的。

cpp

#pragma once#include <functional>
#include <memory>class Timer {
public:typedef std::function<void ()> Callback;Timer(int interval, Callback function); void start();void cancel(); #ifdef DEBUGvoid set_message(const std::string& message);
#endifpublic:struct Impl; private:std::shared_ptr<Impl> pimpl;
};

首先来看Timer的头文件,这里就看出Pimpl惯用法的优势了,头文件里完全剥离了对<chrono>、<thread>、<atomic>的依赖。 另外,增加了set_message接口,用于实现C版本中DEBUG宏中的调试信息打印

下面是Timer的源文件,代码和C版本的一样,有点儿长:

cpp

#include "timer.hpp"
#include <list>
#include <memory>
#include <chrono>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <thread>#ifdef DEBUG
#include <iostream>
#endifusing Clock = std::chrono::system_clock;
using TimePoint = Clock::time_point; 
using Seconds = std::chrono::seconds;
using AlarmPtr = std::shared_ptr<Timer::Impl>;#ifdef DEBUG
namespace {std::ostream& operator<<(std::ostream& out, const TimePoint& tp) {using namespace std::chrono;auto d = duration_cast<seconds>(tp.time_since_epoch());out << d.count();return out;
}template <typename Rep, typename Preiod>
std::ostream& operator<<(std::ostream& out, const std::chrono::duration<Rep, Preiod>& d) {using namespace std::chrono;auto s = duration_cast<seconds>(d);out << s.count();return out;
}}   // namespace
#endifclass Timer::Impl {
public:Impl(int interval_, Callback function_): interval(interval_), function(function_) {}void setup_alarm() {time = Clock::now() + Seconds(interval);}int interval;Timer::Callback function;TimePoint time;std::atomic<bool> active{true};#ifdef DEBUGstd::string message;
#endif
};class AlarmLooper {
public:AlarmLooper() = default; AlarmLooper(const AlarmLooper&) = delete;void operator=(const AlarmLooper&) = delete;void thread_safety_insert(AlarmPtr alarm) {std::unique_lock<std::mutex> lock(alarm_mutex);insert(alarm);}void insert(AlarmPtr alarm);void run();private:std::list<AlarmPtr> alarm_list;TimePoint current_alarm;std::mutex alarm_mutex;std::condition_variable alarm_cond;
};/** Insert alarm entry on list, in order.*/
void AlarmLooper::insert(AlarmPtr alarm) {auto first = alarm_list.begin();auto last = alarm_list.end();/** LOCKING PROTOCOL:* * This routine requires that the caller have locked the* alarm_mutex!*/for ( ; first != last; ++first) {if ((*first)->time >= alarm->time) {alarm_list.insert(first, alarm);break;}}/** If we reached the end of the list, insert the new alarm there. */if (first == last) {alarm_list.push_back(alarm);}#ifdef DEBUGstd::cout << "[list:";for (auto item : alarm_list) {std::cout << item->time << "(" << (item->time - Clock::now()) << ")[\""<< item->message << "\"] ";}std::cout << "]\n" << std::flush;
#endif/** Wake the alarm thread if it is not busy (that is, if* current_alarm is 0, signifying that it's waiting for* work), or if the new alarm comes before the one on* which the alarm thread is waiting.*/if (current_alarm == TimePoint{} || alarm->time < current_alarm) {current_alarm = alarm->time;alarm_cond.notify_one();}
}/** The alarm thread's start routine.*/
void AlarmLooper::run() {AlarmPtr alarm;TimePoint now;bool expired;std::cv_status status;/** Loop forever, processing commands. The alarm thread will* be disintegrated when the process exits. Lock the mutex* at the start -- it will be unlocked during condition* waits, so the main thread can insert alarms.*/std::unique_lock<std::mutex> lock(alarm_mutex);while (true) {/** If the alarm list is empty, wait until an alarm is* added. Setting current_alarm to 0 informs the insert* routine that the thread is not busy.*/current_alarm = TimePoint{};while (alarm_list.empty()) {alarm_cond.wait(lock);}alarm = alarm_list.front();alarm_list.pop_front();now = Clock::now();expired = false;if (alarm->time > now) {
#ifdef DEBUGstd::cout << "[waiting: " << alarm->time << "(" << (alarm->time - Clock::now()) << ")\""<< alarm->message << "\"\n" << std::flush; 
#endifcurrent_alarm = alarm->time;while (current_alarm == alarm->time) {status = alarm_cond.wait_until(lock, alarm->time);if (status == std::cv_status::timeout) {expired = true;break;} }if (!expired) {insert(alarm);}} else {expired = true;}if (expired) {if (alarm->active) {alarm->function();}}}
}class TimerThread {
public:TimerThread(); void insert_alarm(std::shared_ptr<Timer::Impl> timer);bool is_in_looper_thread() {return std::this_thread::get_id() == looper_thread.get_id();}static TimerThread& get_instance() {static TimerThread timer_thread;return timer_thread;}private:AlarmLooper alarm_looper;std::thread looper_thread;
};TimerThread::TimerThread() {looper_thread = std::thread(&AlarmLooper::run, &alarm_looper);looper_thread.detach();
}void TimerThread::insert_alarm(std::shared_ptr<Timer::Impl> pimpl) {if (is_in_looper_thread()) {alarm_looper.insert(pimpl);} else {alarm_looper.thread_safety_insert(pimpl);}
}Timer::Timer(int interval, Callback function) {pimpl = std::make_shared<Impl>(interval, function);
}void Timer::start() {pimpl->setup_alarm();TimerThread::get_instance().insert_alarm(pimpl);
}void Timer::cancel() {pimpl->active = false;
}#ifdef DEBUG
void Timer::set_message(const std::string& message) {pimpl->message = message;
}
#endif

实现原理和C版本的一致(要不说是“直译”呢;),具体的映射关系如下:

  • AlarmLooper类:封装了C版本的函数alarm_thread、alarm_insert逻辑,AlarmLooper::run对应alarm_thread,AlarmLooper::insert对应alarm_insert
  • TimerThread类:作为单例类,管理运行AlarmLooper::run的线程
  • 然后就是std::list替代了C手写的链表
  • std::mutex和std::conditon_variable替代了pthread_mutex_t和pthread_cond_t结构体和函数。

最后,我来简单说明一下我为什么实现C++版本Timer类,以及可以改进优化的方向。

理由方面:

  • C版本的代码很优秀,通过将C版本的代码移植并封装成C++版本,可以提高我对timer实现的算法理解,封装过程也是对C++的OOP的一次实践
  • 实现为Python的threading.Timer接口,则是认识到python接口的简洁性
  • Timer类具备一定的通用性,可以在其他项目中复用,考虑到目前C++标准库(截至C++20)中还没有类似的timer类,以及其他第三方库(例如boost,libuv等)都相对比较重
  • C++版本依赖于C++标准库,而C++标准库是跨平台的,所以Timer类也是跨平台的(当然,如果把C版本的pthread函数替换成C11标准库的线程函数也能达到同样目的)

改进方面:

  • 条件变量版本的C++实现中,alarm_looper的线程是detach的,造成主线程exit时,alarm_looper的线程会因为cond的wait,把整个进程挂起, 所以需要在AlarmLooper增加stop()接口及实现,使得可以通知AlarmLooper::run结束循环并返回(从而使alarm_looper的线程退出),然后, TimerThread类里,alarm_looper的线程不是detach的,而是在TimerThread类的析构函数里通知alarm_looper的线程stop,并join它。
  • 到期时间的精度是秒级的,实际应用中往往需要更高精度的timer
  • std::list可以替换某些已序的数据结构(比如std::multi_set)以提高性能,list的平均插入时间(保证有序的前提下)时O(n)的

最后的最后,给出文章中提到的所有代码的完整实现链接:

  • 基于thread的C++的Timer类实现,精度秒级,工程代码
  • 基于thread的C++的Timer类实现,精度微秒级,工程代码
  • 基于条件变量的C++的Timer类实现,精度秒级,工程代码
  • 基于条件变量的C++的Timer类实现,精度秒级,并增加了AlarmLooper的stop接口,工程代码
  • 基于条件变量的C++的Timer类实现,精度微秒级,并增加了AlarmLooper的stop接口,工程代码

2024/02/08日追加内容:
一般的Timer的接口都是支持一次性(alarm once)和周期性(alarm period)两种模式的,python的threading.Timer接口显然只支持alarm once模式, 虽然也可以通过在alarm once回调函数里再次创建timer的方式等价的实现alarm period模式, 不过我最近参考A Simple Timer in C++的接口,实现了支持alarm period模式的Timer, 这里也一并分享出来

  • A Simple Timer in C++原始版本:基于thread实现,接口为毫秒级精度,工程代码
  • 我的版本-1:基于thread实现,接口参数改成C++11的chrono::duration类型,工程代码
  • 我的版本-2:基于条件变量实现,接口同A Simple Timer in C++,也为毫秒级精度,工程代码
  • 我的版本-3:基于条件变量实现,接口同版本-1,接口参数改成C++11的chrono::duration类型,工程代码

参考文档:

  • 《Posix多线程程序设计(Programming with POSIX Threads)》
  • 3.11.0 Documentation » The Python Standard Library » Concurrent Execution » threading
  • A Simple Timer in C++

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/36856.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

升级!升级!升级!MobPush基础标签推送全新升级,助力开发者精细化运营

“广播推送点击率不高&#xff0c;会员转化差” “新用户拉新后留存不高&#xff0c;次留、3日留存不达标” “用户的复购较低&#xff0c;黏性不高&#xff0c;导致GMV未达预期” 我们总是会听到运营人员关于目标达成过程中遇到这样或者那样的问题。这些问题汇总起来就回到…

STM32 HAL库 外部中断 实现按键控制LED亮灭

目录 1、为什么使用GPIO外部中断控制LED亮灭&#xff1f; 2、NVIC嵌套向量中断控制器 3、EXTI外部中断 4、项目的硬件排线 5、STM32CUBE_MX配置 6、HAL库代码 7、实际效果 1、为什么使用GPIO外部中断控制LED亮灭&#xff1f; 实现LED亮灭控制有很多方式&#xff0c;其中…

公文出错事非小,这些公文写作的常见错误,你中过招吗?

公文是企事业单位、相关部门内外沟通交流的重要工具&#xff0c;不少“笔杆子”经常需要与公文打交道&#xff0c;每天会接触大量的公文。然而在公文撰写的细微之处&#xff0c;稍有不慎&#xff0c;便可能犯下一些常见的错误。这些错误如同蚁穴&#xff0c;虽小却足以破坏公文…

stm32cubemx,adc采样的几种方总结,触发获取adc值的方法dma timer trigger中断

stm32cubemx adc采样的几种方总结&#xff0c;触发获取adc值的方法 timer trigger中断 方法1&#xff0c;软件触发方法2&#xff1a;,Timer触发ADC采集通过DMA搬运 触发获取adc值的方法 Regular Conversion launched by software 软件触发 调用函数即可触发ADC转换 Timer X Cap…

Python基于逻辑回归分类模型、决策树分类模型、LightGBM分类模型和XGBoost分类模型实现车辆贷款违约预测项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 随着经济的发展和人民生活水平的提高&#xff0c;汽车消费在居民消费中所占比例逐渐增加&#xff0c;汽…

克服指标管理痛点,实现数据价值最大化

在当下的企业管理中&#xff0c;由于数据量的激增&#xff0c;管理方式逐渐从基于经验转向基于数据。在此过程中&#xff0c;我们能够通过数据探查业务情况、分析数据&#xff0c;从而获取更优的决策支持数据。这通常通过数据报表或分析平台来实现&#xff0c;对于临时性场景&a…

护眼大路灯哪个牌子好用?五款好用的护眼灯分享

护眼大路灯哪个牌子好用&#xff1f;现在的孩子从幼儿园开始就开始学习&#xff0c;面临的用眼压力就很大&#xff0c;但想要孩子视力不受影响&#xff0c;大路灯是得好好安排起来&#xff0c;但动辄大几千的护眼灯真是让我们这些普通家庭的家长望而却步&#xff0c;有没有好用…

控制系统稳定性常见策略

提升控制系统稳定性是确保系统可靠运行的关键&#xff0c;以下是一些常见策略&#xff1a; 1. 控制算法优化 优化控制算法是提升系统稳定性的核心方法之一。常见的控制算法包括PID控制、模糊控制、自适应控制等。通过调整和优化这些算法的参数&#xff0c;可以显著提高系统的…

填报高考志愿时,学校、专业和城市怎么选择呢?

我的观点是&#xff1a; 专业>城市>学校 专业是兴趣导向&#xff0c;符合自己的价值观&#xff0c;失去了这种驱动力的专业学习&#xff0c;会变得非常艰难的&#xff0c;而且没有竞争力&#xff0c;所以我的排序第一位是专业。 其次是城市&#xff0c;最好是一线城市&…

划重点!炒伦敦金看k线图的要点

对于刚刚开始参与伦敦金交易&#xff0c;还在学习如何看K线的投资者来说&#xff0c;掌握一些看k线图的要点&#xff0c;对于更好地分析市场的行情走势&#xff0c;找到有利的入场机很有帮助&#xff0c;以下是一些关键的建议&#xff0c;希望能够供大家参考。 K线有阳线和阴线…

Coze搭建《测测你的本命宠物》

前言 本文讲解如何从零开始&#xff0c;使用扣子平台去搭建《测测你的本命宠物》 《测测你的本命宠物》&#xff1a;测测你的本命宠物 - 扣子 AI Bot (coze.cn) 欢迎大家去体验一下&#xff01;&#xff01;&#xff01; 正文 接下来我们开始讲解制作这个bot的流程吧&#…

rclone 上传资料到 onedrive 遇到限速问题解决

原因分析 可能和脚本参数设置有关系,我的参数是: rclone copy "F:\阿里云盘\6666\局域网" "od:影视" --ignore-existing -u -v -P --transfers20 --ignore-errors --buffer-size128M --check-first --checkers10 --drive-acknowledge-abuse差不多8G大小的…

如何使得Macos的剪切板感知fileURL并当fileURL被执行paste 动作时 回调到某个监听的函数 从而来填充file content

问题及尝试&#xff1a; 我在做一个跨平台文件拷贝的功能&#xff0c;文件可能是从其他操作系统比如Linux 或者Windows 拷贝到Macos上&#xff0c; 但是我试过所有可以hook NSPasteboard的方法&#xff0c;确实没有找到可以监听macos 剪切板的方法&#xff0c;因为fileURL 确实…

邮件接口代码案例中的SMTP服务器配置详解!

邮件接口代码案例的编写技巧&#xff1f;如何集成邮件服务API&#xff1f; 对于开发者来说&#xff0c;邮件接口代码案例提供了丰富的参考和实践机会&#xff0c;其中SMTP服务器配置是不可或缺的一环。AoKSend将详细讲解如何配置SMTP服务器&#xff0c;并解析其中的重要参数和…

【自动化测试】Selenium自动化测试框架 | 相关介绍 | Selenium + Java环境搭建 | 常用API的使用

文章目录 自动化测试一、selenium1.相关介绍1.Selenium IDE2.Webdriverwebdriver的工作原理&#xff1a; 3.selenium Grid 2.Selenium Java环境搭建3.常用API的使用1.定位元素2.操作测试对象3.添加等待4.打印信息5.浏览器的操作6.键盘事件7.鼠标事件8.定位一组元素9.多层框架定…

2024年6月京东睡眠呼吸暂停和低通气事件检测赛题-baseline

赛题地址&#xff1a;DC竞赛-大数据竞赛平台 (datacastle.cn) 一、数据集介绍 train_x训练数据集特征描述&#xff0c;其样本分布不均匀&#xff0c;0样本29808&#xff0c;1样本3221&#xff0c;2样本4520&#xff0c;共计37549条样本 第一维度&#xff1a;60 位受试样本数总…

一个多智能体AI搜索工具

brainstorm.cool 是最近刚留意到的一个小工具&#xff0c;因为好奇去体验了一把&#xff0c;简单写一下使用体验。 初体验 界面比较清爽&#xff0c;检索结果包括Status、多Agent回答、Relates和右侧的Sources。从Status可以大致了解检索过程&#xff1a;Web Search - Decisio…

pythons工具——裁剪labelme的json不规则多边形标签保存成矩形图像

原图labelme标注 使用以下程序&#xff0c;裁剪labelme的json不规则多边形标签保存成矩形图像 import os import cv2 import math import json import numpy as np from PIL import Image, ImageDrawdef calculate_bounding_box(points):"""计算多边形的最小外…

二次封装 el-dialog 实现 全屏和最小化 功能

效果 封装后的组件 <template><el-dialog v-model"dialogVisible" :show-close"false" :fullscreen"fullscreen" draggable overflow><template #header"{ close }"><div><span style"font-weight: b…

图像增强及运算篇之图像掩膜直方图和HS直方图

一.图像掩膜直方图 如果要统计图像的某一部分直方图&#xff0c;就需要使用掩码&#xff08;蒙板&#xff09;来进行计算。假设将要统计的部分设置为白色&#xff0c;其余部分设置为黑色&#xff0c;然后使用该掩膜进行直方图绘制&#xff0c;其完整代码如下所示。 # -*- codi…