高精度/微秒级线程的实现

前言

在项目中需要实现一个功能,来对项目做一些特殊的工作.这个工作中需要实现某个线程中按照特定间隔(100微秒~10毫秒)来定时执行.实现过程中发现只要使用执行休眠的函数(sleep,sleep_for,sleep_until)每次线程轮询的时间都必定大于1.5毫秒(不同主频的CPU,可能时间会有一些差别),为了满足需求,使用一些方法来达到这个目的.

普通线程轮询

使用std::thread创建线程,使用std::chrono来处理时间.
创建一个线程,然后在线程当中运行,我们可以发现,线程轮询间隔大概是1500毫秒~2000毫秒.
代码如下(统计1001次,取平均值,并且已消除cout打印的时间影响)

#include "stdafx.h"
#include 
#include 
#include 
using namespace std;
using namespace std::chrono;
#define INTERVAL_ARRAY_LENGTH                1000 + 1 //第一个数据不使用
void thread_function_1() {
    high_resolution_clock::time_point time_point_start, time_point_end;
    uint64_t interval_in_microsecond, interval_array[INTERVAL_ARRAY_LENGTH] = {}, interval_array_index = 0;
    uint64_t total, average;
    cout << __FUNCTION__ << endl;
    while (1) {
        time_point_start = high_resolution_clock::now();
        std::this_thread::sleep_for(std::chrono::milliseconds(1));//这里替换成Sleep(1)效果也是一样的.
        time_point_end = high_resolution_clock::now();
        interval_in_microsecond = duration_cast(time_point_end - time_point_start).count();
        interval_array[interval_array_index++] = interval_in_microsecond;
        //当收集完数据后,对数据做一次统计和打印.
        if (interval_array_index == INTERVAL_ARRAY_LENGTH) {
            interval_array_index = 0;
            total = 0;
            for (int i = 1; i < INTERVAL_ARRAY_LENGTH; i++) {
                total += interval_array[i];
            }
            average = total / (INTERVAL_ARRAY_LENGTH - 1);
            std::cout << "thread[1] average:" << average << ",interval_array[0]=" << interval_array[0] << std::flush << std::endl;
        }
    }
}
int main()
{
    std::thread thread_1(thread_function_1);
    system("pause");
    return 0;
}

结果如下
高精度/微秒级线程的实现_第1张图片
从上面输出结果来看,每个线程的平均轮询时间是1500微秒~2000微秒.并且在上面的代码中,将std::this_thread::sleep_for(std::chrono::milliseconds(1));修改为std::this_thread::sleep_for(std::chrono::microseconds(100));,线程轮询的时间仍然为1500毫秒~2000毫秒之间,在MSDN上查阅一下sleep函数的定义,大概是这样”调用sleep之后,会让线程主动放弃CPU的使用权,此时CPU会去执行其他任务,当设定的时间到达后,CPU在执行这个线程,设定的时间单位是毫秒”,这里其实包含两个地方会导致延迟时间精度不高

  • 接口参数设定的单位是毫秒,不能精确到微秒/纳秒.
  • 放弃CPU使用权后,CPU会去执行其他任务,当其他任务(高优先级或同优先级)没有放弃CPU使用权时,该任务会一直等待,导致延迟时间加长.

针对上面的两个点,这里用timeBeginPeriod/timeEndPeriod来设置定时器的精度(这两个接口是msdn推荐的,但是单位仍然是毫秒…),同时提高该线程的优先级为最高优先级(同一进程中),修改后代码如下

#include "stdafx.h"
#include 
#include 
#include 
#pragma comment(lib,"WinMM.Lib")
using namespace std;
using namespace std::chrono;
#define INTERVAL_ARRAY_LENGTH                1000 + 1 //第一个数据不使用
void thread_function_1() {
    high_resolution_clock::time_point time_point_start, time_point_end;
    uint64_t interval_in_microsecond, interval_array[INTERVAL_ARRAY_LENGTH] = {}, interval_array_index = 0;
    uint64_t total, average;
    cout << __FUNCTION__ << endl;
    while (1) {
        time_point_start = high_resolution_clock::now();
        timeBeginPeriod(1);//set Minimum timer resolution.
        std::this_thread::sleep_for(std::chrono::microseconds(100));
        timeEndPeriod(1);
        time_point_end = high_resolution_clock::now();
        interval_in_microsecond = duration_cast(time_point_end - time_point_start).count();
        interval_array[interval_array_index++] = interval_in_microsecond;
        //当收集完数据后,对数据做一次统计和打印.
        if (interval_array_index == INTERVAL_ARRAY_LENGTH) {
            interval_array_index = 0;
            total = 0;
            for (int i = 1; i < INTERVAL_ARRAY_LENGTH; i++) {
                total += interval_array[i];
            }
            average = total / (INTERVAL_ARRAY_LENGTH - 1);
            std::cout << "thread[1] average:" << average << ",interval_array[0]=" << interval_array[0] << std::flush << std::endl;
        }
    }
}
int main()
{
    std::thread thread_1(thread_function_1);
    //set thread priority
    if (SetThreadPriority(thread_1.native_handle(), THREAD_PRIORITY_TIME_CRITICAL)) {
        cout << "set thread_1 priority to time critical" << endl;
    }
    system("pause");
    return 0;
}

结果如下
高精度/微秒级线程的实现_第2张图片
轮询的时间间隔有所改善,平均延迟时间大概在1500毫秒左右,比之前有所改善,不过离我们的目标还比较远.

微秒级线程实现

在google上找了一圈的资料……(省略2天时间),还是没办法在带休眠的情况下实现微秒级别的轮询,最后不得不使用一些措施来达到这种效果,主要的功能点是

  • 线程永不休眠(最大的坏处).
  • 将线程的优先级提高为最高优先级(如果还需要更高,需要同时把线程所在的进程同时提高到高优先级),防止被其他线程抢占.
  • 将线程绑定在CPU特定核心上运行,减少任务栈切换,提高效果,

实现代码如下

// high_precision_thread.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include 
#include 
#include 
#include 
#pragma comment(lib,"WinMM.Lib")
using namespace std;
using namespace std::chrono;
#define THE_NUMBER_OF_PROCESSOR_CORES        4         //CPU核心数,本机是4个
#define INTERVAL_ARRAY_LENGTH                1000 + 1 //第一个数据包含了打印时间,剔除.
#define PROCESSOR_3_MASK                    0x01 << 3
void thread_function_1() {
    high_resolution_clock::time_point time_point_start, time_point_end;
    uint64_t interval_in_microsecond,interval_array[INTERVAL_ARRAY_LENGTH] = {}, interval_array_index = 0;
    uint64_t total, average;
    uint64_t processor_running_times[THE_NUMBER_OF_PROCESSOR_CORES] = {};
    while (1) {
        time_point_start = high_resolution_clock::now();
        timeBeginPeriod(1);
        std::this_thread::sleep_for(std::chrono::microseconds(100));
        timeEndPeriod(1);
        time_point_end = high_resolution_clock::now();
        //计算每次循环的时间.
        interval_in_microsecond = duration_cast(time_point_end - time_point_start).count();
        interval_array[interval_array_index++] = interval_in_microsecond;
        //记录当前线程运行在哪个CPU上.
        processor_running_times[GetCurrentProcessorNumber()]++;
        //每次打印会占用很多时间,先存储在数组中,最后一次性打印出来.
        if (interval_array_index == INTERVAL_ARRAY_LENGTH) {
            interval_array_index = 0;
            total = 0;
            for (int i = 1; i < INTERVAL_ARRAY_LENGTH; i++) {
                total += interval_array[i];
            }
            average = total / (INTERVAL_ARRAY_LENGTH - 1);
            //打印平均耗时,以及在每个core上运行的次数.
            std::cout << "thread[1] average:" << average << " microseconds,interval_array[0]=" << interval_array[0] << ",processor_run_times(" << processor_running_times[0] << "," << processor_running_times[1] << "," << processor_running_times[2] << "," << processor_running_times[3] << ")" << std::endl;
        }
    }
}
void thread_function_3() {
    high_resolution_clock::time_point time_point_start, time_point_end;
    uint64_t interval_in_microsecond, interval_array[INTERVAL_ARRAY_LENGTH] = {}, interval_array_index = 0;
    uint64_t total, average;
    uint64_t processor_running_times[THE_NUMBER_OF_PROCESSOR_CORES] = {};
    Sleep(100);
    high_resolution_clock::time_point tp_start = high_resolution_clock::now();
    while (1) {
        if (duration_cast(high_resolution_clock::now() - tp_start).count() >= 100) {
            high_resolution_clock::time_point tp_end = high_resolution_clock::now();
            //计算每次循环的时间.
            interval_in_microsecond = duration_cast(tp_end - tp_start).count();
            //每次打印会占用很多时间,先存储在数组中,最后一次性打印出来.
            interval_array[interval_array_index++] = interval_in_microsecond;
            tp_start = tp_end;
            processor_running_times[GetCurrentProcessorNumber()]++;
            //数组填满后,打印平均值.或则在debug模式下,看数组中的数据
            if (interval_array_index == INTERVAL_ARRAY_LENGTH) {
                interval_array_index = 0;
                total = 0;
                for (int i = 1; i < INTERVAL_ARRAY_LENGTH; i++) {
                    total += interval_array[i];
                }
                average = total / (INTERVAL_ARRAY_LENGTH - 1);
                //这里添加打印会调用std::cout会主动放弃core3,会有几率让core3被其他线程占用.
                std::cout << "thread[3] average:" << average << " microseconds,interval_array[0]=" << interval_array[0] << ",processor_run_times(" << processor_running_times[0] << "," << processor_running_times[1] << "," << processor_running_times[2] << "," << processor_running_times[3] << ")" << std::endl;
            }
        }
    }
}
int main()
{
    std::thread thread_1(thread_function_1),thread_3(thread_function_3);
    //设定线程3为最高优先级.
    SetThreadPriority(thread_3.native_handle(), THREAD_PRIORITY_TIME_CRITICAL);
    //将线程3绑定在core3上运行.
    SetThreadAffinityMask(thread_3.native_handle(), PROCESSOR_3_MASK);
    system("pause");
    return 0;
}

效果如下
高精度/微秒级线程的实现_第3张图片
从上图数据可以看出,线程3的轮询间隔是非常精准的100微秒(如果希望做到1微秒只需要将上面代码100修改为1即可).另外线程3只运行在core3上,而线程1多数运行在core0,core1,core2上,有少量次数运行在core3,运行在core3的原因是因为线程3中有std::cout打印导致的,如果将线程3的打印代码去除,可以看到线程1永远没机会运行在core3上.

最后

按照上面的方法虽然获得了高精度的线程轮询间隔,但是需要占用一个核心来做这个事情,是比较浪费资源的(最坏的情况会让4核CPU性能直接降低了25%),但目前来看没有其他方式能够实现精准的微秒的轮询功能(如果有,麻烦告诉我一声).在实际项目中,如果不得不使用这种方式来实现,这里个人觉得有不少可以优化的空间,就是想办法把这25%的功能用上(目前大部分时间是空转),理论上最好的情况可以完全消除这种资源的浪费:假设你的项目中有多个需要精确定时的模块A,B,C,D,E.这5个模块需要轮询的间隔都是100微秒,每个模块执行的时间都是20微秒,这样就可以实现完美的轮询间隔,而不造成任何资源的浪费(当然,这是理想的状态).
最后,这是一个抛砖引玉的例子,应该有更好的方法,欢迎指教,谢谢!

资料链接

scheduling priorities
MSDN:sleep
timeBeginPeriod

你可能感兴趣的:(C++)