Как стать автором
Обновить

Комментарии 25

Спс. Читаю
Или crashrpt.
1) Почему вы решили, что try блоки тормозят программу? Насколько они её тормозят? Например в gcc они вообще бесплатны, про VS не знаю, но tryблок не может быть дорогим, он должен быть очень дешевым. Дорого — это обработка исключения, но они же не возникают при нормальной работе программы.
2) Не надо ставить try блоки в каждой функции. Есть отладчик, посмотреть, откуда вылетело исключение можно в нем.
3) Напишите своё исключение, запоминающее стектрейс, откуда оно было брошено (потребуется использовать libunwind).
4) Если вы ловите все исключения без разбору, то после логирования кидайте их снова: все равно ваша программа сломана и не может продолжать работу.
1) а)Потому что так говорит Бьерн Страутруп
б)Я отловил прикол, что РакНет не завершает строки нулем, в функции, вызываемой каждым потоком по десять раз за один пакет. При большой сетевой нагрузке, сколько бы ни стоило, будет дорого.
2) Пост про те ситуации, когда в отладчике все хорошо, все нормально. А в рабочем режиме приложение валится.
3) Собственно, про свое исключение и идет речь
4)(посыпаю голову пеплом) в программе так и кидается, а в топике забыл.
1) а)Потому что так говорит Бьерн Страутруп
б)Я отловил прикол, что РакНет не завершает строки нулем, в функции, вызываемой каждым потоком по десять раз за один пакет. При большой сетевой нагрузке, сколько бы ни стоило, будет дорого.

Дорого ловить исключения или дорого входить/выходить из try блока в случае, когда исключение не кидается? Обработка исключения — дорогое удовольствие. А вот вход в try секцию можно реализовать вообще без накладных расходов: либо указатель на catch в специальный стек записать (просто, эффективно по памяти, но есть runtime расходы), либо во время компиляции составляется таблица <адрес исполняемой команды> -> <обработчик исключения> (нет runtime расходов, но для обработки исключения надо сопоставлять адреса из стека с жтой таблицей, это дорого).
2) Пост про те ситуации, когда в отладчике все хорошо, все нормально. А в рабочем режиме приложение валится.

Согласен, в этом месте я не прав. Кстати, «под отладчиком» — это debug/release режимы в VS или одна и та же программа, но запущенная под отладчиком и без него, без перекомпиляции.
3) Собственно, про свое исключение и идет речь

Используя нехитрые макросы типа __FILE__ и __LINE__ можно в исключение записывать место, откуда оно было брошено. Используя libunwind можно запомнить весь стектрейс и напечаттаь его в what. Ну или использовать библиотеки, описанные в соседних комментариях, где такие исключения уже есть.
Кстати, исключение неплохо бы наследовать от std::exception.
Пятнично:

#ifdef TRY_DEBUG
#define TRY try 
#define CATCH(extra) catch (...) { Logger::throwing extra; throw; }
#else
#define TRY
#define CATCH (extra)
#endif



TRY {
    int* x = NULL;
    *x =1;
} CATCH(("Unexpected in Foo::Bar"))

Переносимо и пара строк всего. Apache Log4 есть под С++, берем готовый.

Вуаля.
Логировать надо не исключения, а состояние программы, которое возможно поможет для разбора креш-дампов (как дополнительное средство выяснения причины ошибки, но не ее места). Средства для автоматизированного сбора креш-дампов вам уже показали, а если программу тестируем локально, то не стоит брезгать WinDbg/dgb.

А за catch(...) без throw нужно вообще наказывать.
> Итак, вы написали программу, запустили в отладчике — все хорошо, все нормально.
Поставили в рабочий процесс — валится. Причем, в самых неожиданных местах.


И сразу первый вопрос: Ваше приложение многопоточное? И оно прекрасно работало без нагрузки «в дебаге»? А как только Вы его выкатываете «в продакшен», оно сразу начинает «валится»? Причем в «самых неожиданных местах»? Ага, наверное, еще и всегда в разных?

Если это так, то авторитетно Вам заявляю, никакие «try/catch» вам не не помогут! В принципе не помогут! Даже если вы каждую строчку кода обернете в try/catch, Вы не сможете «локализовать баг»! Поскольку есть ряд логических багов, при которых «место проявления» не связано напрямую с «источником». Более того, даже если вы попробуете «отловить плохое условие», скорее всего у Вас ничего не выйдет, если «плохое условие» является следствием нахождения данных в неконсистентном состоянии. И «зафиксировать» с помощью _медленной_и_ресурсоемкой_ конструкции try/catch «состояние плохого условия, которое создал другой поток» Вам тоже не удасться, поскольку даже через десяток-другой машинных тактов состояние будет уже другим.

Поэтому, настоятельно рекомендую Вам прекратить смотреть на try/catch как на «серебрянную пулю», способную одним махом решить «все ваши проблемы с отладкой».
Да, приложение многопоточное. Но валится не приколах с потоками — те максимально разнесены, используя очереди сообщений для межпоточного обмена. Т.е. крутится цикл на прием/отправку.Принял — передал на обработку:
queue[i]->push(packet);
i++;
i=i%NumberOfThread;

Обработчик поднял — обработал, поставил пакет в очередь на отправку. Но уже с синхронизацией.

И так по всем узким местам: SQL-запросы, логирование, работа с интерфейсом.
Если вы можете предложить архитектуру лучше — я вас слушаю.

Кроме того, я собираю ВСЕ состояние о системе. Я не стал описывать этого в примере, но в момент обработки try- catch, на следующую итерацию не идет ни один поток. Т.е. я вижу что было с системой на момент падения.

Как я писал выше, приколы возникают в основном со сторонними библиотеками. РакНет, не завершающий строки в пакете нулем(причем, отладочная библиотека завершает нулем пакет, а рабочая — нет), с PostgreSQL тоже что-то было, уже не помню.
Обработчик поднял — обработал, поставил пакет в очередь на отправку. Но уже с синхронизацией.

Т.е. при записи сообщения к обработчику синхронизации нет? А queue — это std::queue? Это уже двойные грабли. Очередь не thread safe, да еще и не гарантируется, что данные, добавленные в очередь будут записаны в память до того, как это увидит другой тред.
Да, при записи сообщения к обработчику синхронизации нет. Оно и не нужно — сама структура очереди, механизм добавления в очередь, исключают любую бяку с синхронизацией. Самое страшное, что там случалось — обработчик пропускал итерацию. Но это ничем не грозит.

А вот обратный код требует синхронизации, так как в очередь могут добавлять несколько потоков сразу.
То есть это не std::queue, а ваша собственная реализация, использующая atomic операции и барьеры памяти? Какие механизмы используются для исключения проблем синхронизации?
Эм… извините, я не понял… Это очередь. Простая, тупая очередь. Один поток умеет добавлять в конец, другой умеет удалять элементы с головы. Какие еще проблемы синхронизации? Синхронизация нужна тогда и только тогда, когда к одному ресурсу имеет доступ несколько потоков.
Вот псевдо-код

queue* Head;
queue* end;

T pop(){
if(head==end) return NULL;
class T el=head->el;
queue* tmp=head;
head=head->next;
delete tmp;
return el;
}

void push(T el){
end->el=el;             //pop не проходит
end->next=new queue;   //pop не проходит
end=end->next;        //рор проходит
}


Естественно, что в реализации, оно имеет несколько дополнительных проверок, но суть та же.
Эм… извините, я не понял… Это очередь. Простая, тупая очередь. Один поток умеет добавлять в конец, другой умеет удалять элементы с головы. Какие еще проблемы синхронизации?

Много, очень много. Почитайте про lockfree очереди и про барьеры памяти, а также про кеширование и оптимизации, разрешенные компилятору.
Взять, например, этот код:
void push(T el){
    end->el=el;             //pop не проходит
    end->next=new queue;   //pop не проходит
    end=end->next;        //рор проходит
}

Не гарантируется порядок, в котором другой поток увидит эти изменения. Вообще говоря, не гарантируется даже то, что при чтении end там будет значение, которое туда когда-либо записывали. Может прочитать старшие байты от нового значения, а младшие от старого.
Я вам даже больше скажу, ваша функция push может никогда не менять переменную end. Вот полный код программы с вашей очередью:
#include <thread>
#include <iostream>

typedef int Value;

class queue {
public:
    queue() : next(nullptr) {}

    Value el;
    queue* next;
};

queue* head;
queue* end;

Value pop(){
    if (head==end)
        return 0;
    Value el = head->el;
    queue* tmp = head;
    head = head->next;
    delete tmp;
    return el;
}

void push(Value el){
    end->el=el;             //pop не проходит
    end->next = new queue;   //pop не проходит
    end = end->next;        //рор проходит
}

void pusher() {
    for (int i = 0;i < 10000; ++i) {
        push(1);
    }
    std::cerr << "End";
}

void popper() {
    for (;;) {
        Value v = pop();
        if (v) {
            std::cerr << v;
        }
    }
}

int main() {
    end = new queue;
    head = end;

    std::thread t1(pusher);
    std::thread t2(popper);
    t1.join();
    t2.join();
    return 0;
}

Собираем без оптимизации:
$ g++ -std=c++0x -O0 tst.cpp  -lpthread -g  
$ ./a.out 
1111111111111111111111111111111...

А теперь с оптимизацией:
$ g++ -std=c++0x -O3 tst.cpp  -lpthread -g                                                                                                  
$ ./a.out 
End^C
Странно. На студии, что с оптимизацией, что без нее, он корректно пишет значения. При этом, я проверяю значения.
Но вообще, я предпочитаю хранить ссылки, ибо ссылка гарантированно пишется за одно обращение. Если я правильно помню курс лекций по ассемблеру. int так же пишется за одно обращение, поэтому это просто общее обращение.

Код

#include <thread>
#include <iostream>

typedef int Value;

class queue {
public:
	queue() : next(nullptr) {}

	Value el;
	queue* next;
};

queue* head;
queue* end;

Value pop(){
	if (head==end)
		return 0;
	Value el = head->el;
	queue* tmp = head;
	head = head->next;
	delete tmp;
	return el;
}

void push(Value el){
	end->el=el;             //pop не проходит
	end->next = new queue;   //pop не проходит
	end = end->next;        //рор проходит
}

void pusher() {
	for (int i = 0;i < 10000000; ++i) {
		push(i);
	}
	std::cerr << "End";
}

void popper() {
	Value t=0;
	for (;;) {
		Value v = pop();
		if (v) {
			std::cerr << v<<" ";
			if(t+1!=v) {std::cerr << "ERROR!!!"; break;}
			t=v;
		}
	}
}

int main() {
	end = new queue;
	head = end;

	std::thread t1(pusher);
	std::thread t2(popper);
	t1.join();
	t2.join();
	return 0;
}
Странно. На студии, что с оптимизацией, что без нее, он корректно пишет значения. При этом, я проверяю значения.

Ничего странного. Просто у вас плохой код, а компилятор плохо оптимизирует. Есть функция popper, который много работает с переменной end. Кешируем её на регистр! Это же будет работать быстрее! Все. Он никогда не увилит изменений в ней. То же самое с функцией pusher, кешируем на регистр и никогда не пишем в память. Это корректное поведение компилятора, ему так делать можно и нужно.
Но вообще, я предпочитаю хранить ссылки, ибо ссылка гарантированно пишется за одно обращение. Если я правильно помню курс лекций по ассемблеру. int так же пишется за одно обращение, поэтому это просто общее обращение.

Какго такого ассемблера? При чем тут ассемблер? Вы пишите на C++. Атомарность записи значения гарантируется ТОЛЬКО если вы объявили его atomic. Или вы пишете код, который работает только будучи скомпилированным VS, только на x86, только под windows? Это плохой код. Руководствуйтесь стандартом C++.
Кстати, на x86 запись ссылки или int атомарна, толкьо если она выровняна. Кроме того, я уже говорил, что не гарантируется, что ссылка будет записана ДО того, как будет записан сам объект.
Ничего странного. Просто у вас плохой код, а компилятор плохо оптимизирует. Есть функция popper, который много работает с переменной end. Кешируем её на регистр! Это же будет работать быстрее! Все. Он никогда не увилит изменений в ней. То же самое с функцией pusher, кешируем на регистр и никогда не пишем в память. Это корректное поведение компилятора, ему так делать можно и нужно.


Знаете, то, что вы сейчас говорите — это дикий ахтунг, который говорит: не пользуйтесь оптимизатором от g++. И вообще не пользуйтесь этим компилятором. Ибо компилятор, из-за которого код в итоге работает не так, как было задумано программистом — это плохой компилятор.

Потому что как было задумано программистом? переменная сохраняется каждый раз в новую область памяти. Где она? Она как была в регистре, так и осталась. Ахтунг. Потому что я даже не знаю как этот компилятор может использовать кэш, чтобы объект так и не появился в ОЗУ.

Какго такого ассемблера? При чем тут ассемблер? Вы пишите на C++. Атомарность записи значения гарантируется ТОЛЬКО если вы объявили его atomic. Или вы пишете код, который работает только будучи скомпилированным VS, только на x86, только под windows? Это плохой код. Руководствуйтесь стандартом C++.


Извините, но ваша программа выполняется не на сферическом в вакууме компьютере, а на вполне конкретной архитектуре. Да, можно писать переносимый код. Но. Он не будет оптимальным. У каждой архитектуры будут свои особенности. И моя задача вот конкретно прямо сейчас — выжать из этой архитектуры вообще все что, можно.

Кроме того, я уже говорил, что не гарантируется, что ссылка будет записана ДО того, как будет записан сам объект.

не пользуйтесь плохим компилятором :-)
Кроме того, я уже говорил, что не гарантируется, что ссылка будет записана ДО того, как будет записан сам объект.

не пользуйтесь плохим компилятором :-


Поясняю:
а)нет нужды кэшировать объект, который используется один раз в обозримом коде.

б)Та же переменная end может быть сколько угодно кэширована, но! та же студия, тот интеловый компилятор смотрят кто ее еще используют. И если ее использует другой поток — она компилится с сохранением в ОЗУ после каждого изменения значения.
Знаете, то, что вы сейчас говорите — это дикий ахтунг, который говорит: не пользуйтесь оптимизатором от g++. И вообще не пользуйтесь этим компилятором. Ибо компилятор, из-за которого код в итоге работает не так, как было задумано программистом — это плохой компилятор.

Читайте стандарт. Это полезно. Компилятор сделал то, что должен и то, что я его попросил.
Потому что как было задумано программистом? переменная сохраняется каждый раз в новую область памяти. Где она? Она как была в регистре, так и осталась. Ахтунг.

а)нет нужды кэшировать объект, который используется один раз в обозримом коде.

Вы не обратили внимания на одну тонкость, которую я использовал, чтобы сломать ваш код: написал реализации push и pusher, а также pop и popper в одном файле. Компилятор может их инлайнить, я этого попросил опцией -O3. Это позволило оптимизировать код методов pop и push в контексте, откуда он вызывается. Поэтому в обозримом коде эти переменные используются много раз и очевидно, что их надо кешировать.
Потому что я даже не знаю как этот компилятор может использовать кэш, чтобы объект так и не появился в ОЗУ.

Ну вы же не просили его записывать переменную в ОЗУ! Я вынужден в третий раз сказать слово atomic. В крайнем случае, volatile. А также напомнить про memory barriers, которые вы упорно игнорируете.
Извините, но ваша программа выполняется не на сферическом в вакууме компьютере, а на вполне конкретной архитектуре.

Не известно, на какой архитектуре он будет выполняться.
Да, можно писать переносимый код. Но. Он не будет оптимальным.

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

Подумайте о том, сколько стоит найти и справить те ошибки, которыйе вы посадили в этом коде. Гонки трудно искать и отлаживать. Если вы хотите продолжать писать пхохой код — это ваши проблемы и проблемы вашего работодателя. Я надеялся, что вы все-таки прочтете про те вещи, на которые я сослался и узнаете для себя много нового.
та же студия, тот интеловый компилятор смотрят кто ее еще используют. И если ее использует другой поток — она компилится с сохранением в ОЗУ после каждого изменения значения.

Кто-то погорячился. Давайте вы соберете один объектный модуль содержащий только push и объявление end, а затем другой модуль, где сделаете extern end и опишите pop. И помотрим, как ваши компиляторы, не имея кода, узнают, использует ли кто-то еще эту переменную.
> Если я правильно помню курс лекций по ассемблеру. int так же пишется за одно обращение

> Кстати, на x86 запись ссылки или int атомарна, толкьо если она выровняна.

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

Иными словами возможна ситуация:

tread 1 (Core1):

// Core1 пишет в свой кеш
time == 0 ns: mov dword ptr[address], eax

thread 2 (Core2):
// Core2 читает в eax старое значение, т.к. кеш первого
// ядра еще не сброшен в память (и не синхронизирован с кешем второго ядра)
time == 1 ns: mov eax, dword ptr[address]

Для предотвращения этих ситуаций есть специальный префикс lock.
В С++ для него есть специальные обвязки: ищите interlocked compare exchange

Есть мысль, что он с оптимизацией не ставит выравнивание адреса значения, в итоге значение пишется за два такта, а не за один. Сейчас попробую симитировать эту ситуацию в студии.
Слово 'namber' правильно пишется через u — 'number'
Постоянно путаю :-) Спасибо :-)
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории