Каков наиболее эффективный способ сделать этот поток кода безопасным? - PullRequest
1 голос
/ 09 июля 2010

Некоторая библиотека C ++, над которой я работаю, имеет простой механизм трассировки, который можно активировать для генерации файлов журнала, показывающих, какие функции были вызваны и какие аргументы были переданы. По сути, это сводится к тому, что макрос TRACE распространяется по всему источнику библиотеки, а макрос расширяется до чего-то вроде этого:

typedef void(*TraceProc)( const char *msg );

/* Sets 'callback' to point to the trace procedure which actually prints the given
 * message to some output channel, or to a null trace procedure which is a no-op when
 * case the given source file/line position was disabled by the client.
 *
 * This function also registers the callback pointer in an internal data structure
 * and resets it to zero in case the filtering configuration changed since the last
 * invocation of updateTraceCallback.
 */
void updateTraceCallback( TraceProc *callback, const char *file, unsinged int lineno );

#define TRACE(msg)                                                  \
{                                                                   \
    static TraceProc traceCallback = 0;                             \
    if ( !traceCallback )                                           \
        updateTraceCallback( &traceCallback, __FILE__, __LINE__ );  \
    traceCallback( msg );                                           \
}

Идея состоит в том, что люди могут просто сказать TRACE("foo hit") в своем коде, и это либо вызовите функцию отладочной печати, иначе она будет недоступна. Они могут использовать какой-то другой API (который здесь не показан) для настройки печати только для TRACE мест (исходный файл / номер строки). Эта конфигурация может измениться во время выполнения.

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

Какой самый эффективный способ сделать этот подход безопасным? Мне нужно решение для Windows (XP и новее) и Linux. Я боюсь делать чрезмерную блокировку только для того, чтобы проверить, изменилась ли конфигурация фильтра (99% времени, когда достигается точка трассировки, конфигурация не изменилась). Я тоже открыт для больших изменений в макросе. Поэтому вместо обсуждения производительности мьютекса и критической секции было бы также приемлемо, если бы макрос просто отправлял событие в цикл событий в другом потоке (при условии, что доступ к циклу событий безопасен для потока), и вся обработка происходит в одном и том же поток, поэтому он синхронизируется с помощью цикла событий.

ОБНОВЛЕНИЕ : Вероятно, я могу упростить этот вопрос до:

Если у меня есть один поток, читающий указатель, и другой поток, который может записать в переменную (но в 99% случаев это не так), как я могу избежать того, что поток чтения должен заблокировать все время?

Ответы [ 6 ]

2 голосов
/ 09 июля 2010

Вы можете реализовать переменную версии файла конфигурации. Когда ваша программа запускается, она устанавливается в 0. Макрос может содержать статическое int, то есть последнюю версию конфигурации, которую он видел. Тогда простое атомарное сравнение между последней увиденной и текущей версией конфигурации скажет вам, если вам нужно сделать полную блокировку и повторно позвонить updateTraceCallback();.

Таким образом, в 99% случаев вы будете добавлять только дополнительные атомные операции, или барьер памяти, или что-то похожее, что очень дешево. 1% времени, просто делайте полную вещь mutex, это не должно влиять на вашу производительность каким-либо заметным образом, если это только 1% времени.

Edit:

Какой-то .h файл:

extern long trace_version;

Некоторый файл .cpp:

long trace_version = 0;

Макрос:

#define TRACE(msg)                                                  
{
    static long __lastSeenVersion = -1;
    static TraceProc traceCallback = 0;
    if ( !traceCallback || __lastSeenVersion != trace_version )
        updateTraceCallback( &traceCallback, &__lastSeenVersion, __FILE__, __LINE__ );
    traceCallback( msg );
}

Функции для увеличения версии и обновлений:

static long oldVersionRefcount = 0;
static long curVersionRefCount = 0;

void updateTraceCallback( TraceProc *callback, long &version, const char *file, unsinged int lineno ) {
   if ( version != trace_version ) {
      if ( InterlockedDecrement( oldVersionRefcount ) == 0 ) {
        //....free resources.....
        //...no mutex needed, since no one is using this,,,
      }
      //....aquire mutex and do stuff....
      InterlockedIncrement( curVersionRefCount );
      *version = trace_version;
      //...release mutex...
   }
 }

 void setNewTraceCallback( TraceProc *callback ) {
    //...aquire mutex...
    trace_version++;  // No locks, mutexes or anything, this is atomic by itself.  
    while ( oldVersionRefcount != 0 ) { //..sleep? }
    InterlockedExchange( &oldVersionRefcount, curVersionRefCount );
    curVersionRefCount = 0;
    //.... and so on...
    //...release mutex...

Конечно, это очень упрощено, так как если вам нужно обновить версию и oldVersionRefCount > 0, то у вас проблемы; Как решить эту проблему, зависит только от вас. Я предполагаю, что в этих ситуациях вы могли бы просто подождать, пока счетчик ссылок не станет равным нулю, поскольку период времени, в течение которого счетчик ссылок увеличивается, должен быть временем, необходимым для запуска макроса.

2 голосов
/ 09 июля 2010

Я до сих пор не до конца понял вопрос, поэтому, пожалуйста, исправьте меня в том, что я не получил.

(я опускаю обратные слеши.)

#define TRACE(msg)
{
    static TraceProc traceCallback = NULL;
    TraceProc localTraceCallback;

    localTraceCallback = traceCallback;

    if (!localTraceCallback)
    {
        updateTraceBallback(&localTraceCallback, __FILE__, __LINE__);
        // If two threads are running this at the same time 
        // one of them will update traceCallback and get it overwritten 
        // by the other. This isn't a big deal.
        traceCallback = localTraceCallback;
    }

    // Now there's no way localTraceCallback can be null.
    // An issue here is if in the middle of this executing 
    // traceCallback gets null'ed. But you haven't specified any 
    // restrictions about this either, so I'm assuming it isn't a problem.
    localTraceCallback(msg);
}
1 голос
/ 09 июля 2010

Если у меня есть один поток, читающий указатель, и другой поток, который может записывать в переменную (но в 99% случаев это не так), как я могу избежать того, что поток чтения должен заблокировать всевремя?

Из вашего кода можно использовать мьютекс внутри updateTraceCallback(), так как он будет вызываться очень редко (один раз для каждого местоположения).После взятия мьютекса проверьте, инициализирована ли traceCallback: если да, то другой поток только что сделал это для вас, и ничего не поделаешь.

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

1 голос
/ 09 июля 2010

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

Как вы думаете, потокобезопасный обмен сообщениями между потоками реализован без блокировок?

В любом случае, вот конструкция, которая может работать:

Структура данных, которая содержит фильтр, должна быть изменена, чтобы онвыделяется динамически из кучи, потому что мы собираемся создавать несколько экземпляров фильтров.Кроме того, к нему нужно добавить счетчик ссылок.Вам нужен typedef что-то вроде:

typedef struct Filter
{
    unsigned int refCount;
    // all the other filter data
} Filter;

Там где-то объявлен одиночный 'текущий фильтр'.

static Filter* currentFilter;

и инициализирован с некоторыми настройками по умолчанию.

В вашемМакрос TRACE:

#define TRACE(char* msg)
{
    static Filter* filter = NULL;
    static TraceProc traceCallback = NULL;

    if (filterOutOfDate(filter))
    {
        getNewCallback(__FILE__, __LINE__, &traceCallback, &filter);
    }
    traceCallback(msg);
}

filterOutOfDate() просто сравнивает фильтр с currentFilter, чтобы убедиться, что он такой же.Этого должно быть достаточно, чтобы просто сравнить адреса.Он не блокируется.

getNewCallback() применяет текущий фильтр для получения новой функции трассировки и обновляет переданный фильтр с адресом текущего фильтра.Его реализация должна быть защищена блокировкой мьютекса.Кроме того, он вычисляет refCount исходного фильтра и увеличивает refCount нового фильтра.Это так, мы знаем, когда мы можем освободить старый фильтр.

void getNewCallback(const char* file, int line, TraceProc* newCallback, Filter** filter)
{
    // MUTEX lock
    newCallback = // whatever you need to do
    currentFilter->refCount++;
    if (*filter != NULL)
    {
        *filter->refCount--;
        if (*filter->refCount == 0)
        {
            // free filter and associated resources
        }
    }
    *filter = currentFilter;
    // MUTEX unlock
}

Когда вы хотите изменить фильтр, вы делаете что-то вроде

changeFilter()
{ 
    Filter* newFilter = // build a new filter
    newFilter->refCount = 0;
    // MUTEX lock (same mutex as above)
    currentFilter = newFilter;
    // MUTEX unlock
}
1 голос
/ 09 июля 2010

Ваш комментарий говорит "сбрасывает его в ноль в случае изменения конфигурации фильтрации во время выполнения", но правильно ли я прочитал, что "сбрасывает его в ноль при изменении конфигурации фильтрации"?

Не зная точно, как updateTraceCallback реализует свою структуру данных или какие другие данные он ссылается, чтобы решить, когда следует сбросить обратные вызовы (или, действительно, установить их в первую очередь), невозможно судить о том, что будет безопасно.Аналогичная проблема относится к знанию того, что делает traceCallback - например, если он обращается к общему назначению вывода.

С учетом этих ограничений единственная безопасная рекомендация, которая не требует переделки другого кода, - это прикрепить мьютекс вокруг всего лота (или, предпочтительно, критического раздела в Windows).

0 голосов
/ 09 июля 2010
#define TRACE(msg)                                                  \
{                                                                   \
    static TraceProc traceCallback =                                \
        updateTraceBallback( &traceCallback, __FILE__, __LINE__ );  \
    traceCallback( msg );                                           \
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...