Что может быть причиной всплесков истекшего времени в критической секции pthread, когда политика планирования - SCHED_RR? - PullRequest
1 голос
/ 25 марта 2019

Я делаю некоторые тесты для расчета времени в Linux. Мое ядро ​​- Preempt-RT (однако в моих тестах ванильное ядро ​​дает похожие результаты ...)

У меня есть два файла pthreads, работающие одновременно на одном и том же процессоре (дано сродство). Это потоки в реальном времени (prio 99).

У меня есть критическая секция, защищенная спин-блокировкой, где два потока конкурируют за блокировку. Внутри критической секции у меня есть операция с одним приращением, и я пытаюсь вычислить истекшее время для этой операции.

Пример кода с __rdtsc:

pthread_spin_lock(&lock);

start_time = __rdtsc();
++cnt; //shared ram variable, type is unsigned long long
stop_time = __rdtsc();

pthread_spin_unlock(&lock);

Пример кода с таймером хронографа:

pthread_spin_lock(&lock);

auto _start_time = std::chrono::high_resolution_clock::now();
++cnt; //shared ram variable, type is unsigned long long
auto _stop_time = std::chrono::high_resolution_clock::now();

pthread_spin_unlock(&lock);

Потоки запускаются в цикле пару миллионов раз и затем завершаются. После разблокировки спин-блокировки я записываю среднее затраченное время и максимальное истекшее время.

Теперь все идет интересно (по крайней мере, для меня):

Тест 1: Потоки имеют политику планирования как SCHED_RR :

Резьба №: 0, максимальное время: 34124, среднее время: 28.114271, Cnt запуска: 10000000

Резьба №: 1, Макс. Время: 339256976 , Среднее время: 74.781960, Cnt запуска: 10000000


Тест 2. У потоков есть политика планирования: SCHED_FIFO :

Резьба №: 0, Макс. Время: 33114, Среднее время: 48,414173, Cnt запуска: 10000000

Резьба №: 1, максимальное время: 38637, среднее время: 24.327742, Cnt запуска: 10000000


Тест 3: только один поток, политика планирования SCHED_RR:

Резьба №: 0, максимальное время: 34584, среднее время: 54,165470, Cnt запуска: 10000000


Примечание. Основной поток - это не относящийся к rt поток, имеющий сходство с отдельным процессором. Здесь это не важно.

Примечание 1: Все тесты дают ок. подобные результаты каждый раз, когда я запускаю их.

Примечание 2: Данные результаты являются выходными данными rdtsc. Тем не менее, результаты хронографа почти аналогичны этим.

Так что я думаю, что у меня неправильное понимание планировщика, поэтому мне нужно задать следующие вопросы:

  1. Как возникают максимальные максимальные временные пики в тесте 1? Тест 2 и 3 не ведут себя так, как он ...
  2. Почему существует очень большой разрыв между максимальным и средним расчетами? Что вызывает это, прерывание как таймер?

Мой весь тестовый код:

#include <stdio.h>
#include <stdlib.h>
#include "stdint.h"
#include <float.h>
#include <pthread.h>
#include <cxxabi.h>
#include <limits.h>
#include <sched.h>
#include <sys/mman.h>
#include <unistd.h> 
#include <sys/time.h> 
#include <sys/resource.h> 
#include <malloc.h>
#include <chrono>

/********* TEST CONFIG ************/

#define TEST_PTHREAD_RUN_CNT    10000000    //1000000000
#define NUM_OF_TEST_PTHREADS    2
#define MAIN_THREAD_CORE_INDEX  0
#define TEST_PTHREAD_PRIO       99
#define TEST_PTHREAD_POLICY     SCHED_RR

#define TIME_RDTSC              1
#define TIME_CHRONO             0
/**********************************/

/**********************************/
struct param_list_s
 {
    unsigned int thread_no;
 };
/**********************************/

/********* PROCESS RAM ************/
pthread_t threads[NUM_OF_TEST_PTHREADS];
struct param_list_s param_list[NUM_OF_TEST_PTHREADS];
unsigned long long max_time[NUM_OF_TEST_PTHREADS];
unsigned long long _max_time[NUM_OF_TEST_PTHREADS];
unsigned long long tot_time[NUM_OF_TEST_PTHREADS];
unsigned long long _tot_time[NUM_OF_TEST_PTHREADS];
unsigned long long run_cnt[NUM_OF_TEST_PTHREADS];
unsigned long long cnt;
pthread_spinlock_t lock;
/**********************************/

/*Proto*/
static void configureMemoryBehavior(void);
void create_rt_pthread(unsigned int thread_no);

/*
* Date............: 
* Function........: main
* Description.....: 
*/
int main(void)
{
    cpu_set_t  mask;
    int i;

    for (i = 0; i < NUM_OF_TEST_PTHREADS; ++i)
     {
        max_time[i] = 0;
        tot_time[i] = 0;
        run_cnt[i] = 0;

        _max_time[i] = 0;
        _tot_time[i] = 0;
     }
    cnt = 0;

    printf("\nSetting scheduler affinity for the process...");
    CPU_ZERO(&mask);
    CPU_SET(MAIN_THREAD_CORE_INDEX, &mask);
    sched_setaffinity(0, sizeof(mask), &mask);
    printf("done.\n");

    configureMemoryBehavior();

    pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);

    for (i = 0; i < NUM_OF_TEST_PTHREADS; ++i)
     {
        create_rt_pthread(i);
     }

    printf("Waiting threads to join\n...\n");
    for (i = 0; i < NUM_OF_TEST_PTHREADS; i++)
    {
        pthread_join(threads[i], NULL);
        #if(TIME_RDTSC == 1)
        printf("Thread no: %d, Max Time: %llu, Avg Time: %f, Run Cnt: %llu\n", i, max_time[i], (float)((float)tot_time[i] / run_cnt[i]), run_cnt[i]);
        #endif

        #if(TIME_CHRONO == 1)
        printf("Thread no: %d, Max Time: %lu, Avg Time: %f, Run Cnt: %lu\n", i, _max_time[i], (float)((float)_tot_time[i] / run_cnt[i]), run_cnt[i]);
        #endif
    }
    printf("All threads joined\n");
    printf("Shared Cnt: %llu\n", cnt);

    return 0;
}


/*
* Date............:
* Function........: thread_func
* Description.....:
*/
void *thread_func(void *argv)
{

    unsigned long long i, start_time, stop_time, latency = 0;
    unsigned int thread_no;

    thread_no = ((struct param_list_s *)argv)->thread_no;
    i = 0;
    while (1)
     {
        #if(TIME_RDTSC == 1)
        pthread_spin_lock(&lock);
        start_time = __rdtsc();
        ++cnt;
        stop_time = __rdtsc();
        pthread_spin_unlock(&lock);

        if (stop_time > start_time)
        {
            latency = stop_time - start_time;
            ++run_cnt[thread_no];

            tot_time[thread_no] += latency;
            if (latency > max_time[thread_no])
                max_time[thread_no] = latency;
        }
        #endif

        #if(TIME_CHRONO == 1)
        pthread_spin_lock(&lock);

        auto _start_time = std::chrono::high_resolution_clock::now();
        ++cnt;
        auto _stop_time = std::chrono::high_resolution_clock::now();

        pthread_spin_unlock(&lock);

        auto __start_time = std::chrono::duration_cast<std::chrono::nanoseconds>(_start_time.time_since_epoch()).count();
        auto __stop_time = std::chrono::duration_cast<std::chrono::nanoseconds>(_stop_time.time_since_epoch()).count();
        auto __latency = __stop_time - __start_time;

        if (__stop_time > __start_time)
        {
            _tot_time[thread_no] += __latency;
            ++run_cnt[thread_no];
            if (__latency > _max_time[thread_no])
            {
                _max_time[thread_no] = __latency;
            }
        }
        #endif

        if (++i >= TEST_PTHREAD_RUN_CNT)
            break;
     }

    return 0;
}


/*
* Date............:
* Function........: create_rt_pthread
* Description.....:
*/
void create_rt_pthread(unsigned int thread_no)
{

    struct sched_param  param;
    pthread_attr_t      attr;

    printf("Creating a new real-time thread\n");
    /* Initialize pthread attributes (default values) */
    pthread_attr_init(&attr);

    /* Set a specific stack size  */
    pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);

    /* Set scheduler policy and priority of pthread */
    pthread_attr_setschedpolicy(&attr, TEST_PTHREAD_POLICY);
    param.sched_priority = TEST_PTHREAD_PRIO;
    pthread_attr_setschedparam(&attr, &param);

    /* Set the processor affinity*/
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(1, &cpuset);

    pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpuset);

    /* Use scheduling parameters of attr */
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    param_list[thread_no].thread_no = thread_no;

    if(pthread_create(&threads[thread_no], &attr, thread_func, (void *)&param_list[thread_no]) != 0)
     {
        printf("Thread could not be created.\n");
        exit(-1);
     }
}


/*
* Date............:
* Function........: configureMemoryBehavior
* Description.....:
*/
static void configureMemoryBehavior(void)
{
    printf("\nLocking memory...");
    /* Now lock all current and future pages
       from preventing of being paged */
    if (mlockall(MCL_CURRENT | MCL_FUTURE))
        perror("mlockall failed:");

    /* Turn off malloc trimming.*/
    mallopt(M_TRIM_THRESHOLD, -1);

    /* Turn off mmap usage. */
    mallopt(M_MMAP_MAX, 0);
    printf("done.\n");
}

1 Ответ

1 голос
/ 26 марта 2019

Когда вы запускаете с SCHED_FIFO, запускается один из ваших потоков. Затем он работает, пока не закончится - потому что так работает SCHED_FIFO - ничто его не вытеснит. Следовательно, время, которое он проводит внутри спин-блокировки, является относительно постоянным. Затем, после завершения первого потока, второй поток запускается до завершения без конфликта для блокировки. Так что это тоже имеет более последовательное время. Из-за прерываний и т. Д. Все еще есть некоторый джиттер, но это вполне соответствует между ними.

Когда вы запускаете с SCHED_RR, один из ваших потоков работает некоторое время. В конце временного отрезка он становится пустым, а другой запускается - потому что так работает SCHED_RR. Теперь, есть большая вероятность, что он получает приоритет , удерживая спинлок . Итак, теперь другой поток запущен, он сразу же пытается получить спин-блокировку, которая не срабатывает - потому что другой поток удерживает блокировку. Но он просто пытается до конца временного интервала (потому что так работают спин-блокировки - он никогда не будет блокировать ожидание получения блокировки). Конечно, это ничего не дает в течение этого времени. В конце концов временной интервал заканчивается, и поток, удерживающий блокировку, снова запускается. Но время, связанное с этой единственной операцией приращения, теперь включает в себя все это время, ожидающее, пока другой поток начнет вращаться в течение своего временного интервала.

Я думаю, что если вы увеличите максимальное количество (TEST_PTHREAD_RUN_CNT), вы увидите, что поведение SCHED_RR выравнивается, когда оба ваших потоков в конечном итоге подвергаются этому эффекту. Прямо сейчас, я предполагаю, что есть хороший шанс, что один поток может в значительной степени завершиться за один или два временных отрезка.

Если вы хотите заблокировать другой поток с одинаковым приоритетом на том же процессоре, вам, вероятно, следует использовать pthread_mutex_t. Это будет действовать почти так же, как спин-блокировка в случае успешного захвата, но будет блокироваться, когда он не сможет получить блокировку.

Но затем обратите внимание: результат этого вполне может превратить поведение SCHED_RR в поведение SCHED_FIFO: большую часть времени упреждение будет происходить, пока один поток удерживает блокировку, поэтому другой будет выполнить несколько инструкций до тех пор, пока он не попытается получить блокировку, затем он заблокируется, а первый снова запустится для полного отрезка времени.

В целом, очень рискованно пытаться запускать два потока с приоритетом RT на одном процессоре, где оба они будут работать в течение длительного времени. Приоритет RT будет работать лучше всего, когда вы привязываете каждый поток к его собственному ядру, или когда потоки RT должны быть запланированы немедленно, но будут выполняться только в течение короткого времени перед повторной блокировкой.

...