суббота, 19 марта 2016 г.

[prog.c++] Голые нити и exception safety

Раз уж на неделе затронул вопрос осторожного обращения с голыми нитями, то можно осветить еще один момент. Касающийся обеспечения exception safety при работе с многопоточностью.

В качестве демонстрации буду использовать вот такую небольшую программку:

[prog.c++] Продолжаем упарываться шаблонами при парсинге бинарных PDU

Продолжение вчерашней заметки.

Во вчерашнем решении очень не понравилось то, что смещение поля в PDU приходилось дублировать при вызовах encode и decode:

class header16_t
   :  protected data_with_encoder_and_decoder_t< 16 >
{
public :
   auto f1() const { return decode< std::uint16_t >( 0 ); }
   auto f2() const { return decode< std::uint16_t >( 2 ); }
   auto f3() const { return decode< std::uint16_t >( 4 ); }
   auto f4() const { return decode< std::uint16_t >( 8 ); }

   void set_f1( std::uint16_t v ) { encode( 0, v ); }
   void set_f2( std::uint16_t v ) { encode( 2, v ); }
   void set_f3( std::uint16_t v ) { encode( 4, v ); }
   void set_f4( std::uint16_t v ) { encode( 8, v ); }
};

Легко ошибиться. Собственно, я и ошибся: при вызове decode задал правильные смещения, а вот при вызове encode записал неправильные значения. Причем код с этой ошибкой ушел в заметку в блоге и только спустя какое-то время эта ошибка была замечена и исправлена.

Поэтому возник вопрос: а можно ли как-то однократно увязать смещение поле с описанием поля? Чтобы затем смещение автоматически извлекалось из описания поля когда это нужно.

Можно.

[comp] Лучи ненависти Asus-у :(

Asus-овский Zenbook, конечно, классный ноутбук. Но с одним фатальным недостатком: самопроизвольные выключения. Как правило, во время набора текста.

Проявляться эта проблема начала где-то месяца через 4 после покупки. Возникала в непредсказуемые моменты времени где-то раз в три недели. Иногда реже. Но иногда и чаще, вплоть до пары раз в день. Как сегодня. С учетом того, что предсказать появление проблемы нельзя, смысла везти в сервис и объяснять, что время от времени ноут отключается, не вижу. Тем более, что везти нужно будет в Москву и оставлять в сервисе на две-три-четыре недели. Тем более, что в Интернете уже много жалоб на такое поведение Zenbook-ов, а вот историй про то, что ноут смоли исправить в ремонте -- намного меньше.

В общем, пичаль-пичаль. Тем более, что не понятно, что можно взять на замену. Такое впечатление, что абсолютно все производители WIntel-овских ноутбуков в последние годы скатились в унылое говно.

пятница, 18 марта 2016 г.

[prog.c++14] Еще один кейс для использования шаблонов и CRTP

Разбираясь с примерами одной C++ной библиотеки обнаружил наличие в разных примерах с парсингом бинарных PDU дублирование в классах для PDU дублирование одних и те же методов encode и decode приблизительно вот такого вида:

void encode( std::size_t index, std::uint16_t v )
{
   auto b = &(rep()[index]);
   b[0] = static_cast< std::uint8_t >(v >> 8);
   b[1] = static_cast< std::uint8_t >(v & 0xffu);
}
void decode( std::size_t index, std::uint16_t & v ) const
{
   auto b = &(rep()[index]);
   v = (static_cast< std::uint16_t >(b[0]) << 8) + b[1];
}

Поскольку не люблю копипасту, то задумался, как можно сделать так, чтобы классы для представления PDU с разного размера payload-ами, имели бы общую реализацию методов encode/decode.

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

Сделано два класса encoder_t и decoder_t. Это для того, чтобы можно было делать PDU, которые можно только распаковывать, но не нужно упаковывать. Или, наоборот: PDU, которые нужно только упаковывать, но не распаковывать. Ну и, кроме того, так задачка была сложнее :)

Вот такую конструкцию пришлось сделать для того, чтобы сохранить методы data(), encode() и decode() скрытыми (protected).

template< std::size_t N >
class data_with_encoder_and_decoder_t
   :  public data_holder_t< N >
   ,  public encoder_t< data_with_encoder_and_decoder_t< N > >
   ,  public decoder_t< data_with_encoder_and_decoder_t< N > >
{};

Без этого пришлось бы кроме наследования от encoder_t/decoder_t еще и прописывать эти классы в качестве френдов. Что-то вроде:

class header16_t
   :  protected data_holder_t< 16 >
   ,  protected encoder< header16_t >
   ,  protected decoder< header16_t >
{
   friend class encoder< header16_t >;
   friend class decoder< header16_t >;

Вместо намного более короткого варианта:

class header16_t
   :  protected data_with_encoder_and_decoder_t< 16 >

Итак, вот полный код эксперимента:

четверг, 17 марта 2016 г.

[prog.flame] Программисты-сишники явно намного более трудолюбивы, чем я...

Ну вот взять ув.тов.asfikon-а (он же Eax Melanhovich) и его свежий пост про работу с libcurl из C (вот целиком исходный файл, о котором пойдет речь). Хватило же у человека терпения и прилежания тщательно выписывать очистку ресурсов при обнаружении ошибок в работе с libcurl:

CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK)
{
    fprintf(stderr"curl_easy_perform() failed: %s\n",
      curl_easy_strerror(res));
    writeFuncCtxFree(&ctx);
    curl_formfree(formFirstItem);
    curl_slist_free_all(curlHeaders);
    curl_easy_cleanup(curl);
    return 1;
}

Но вообще очевидно, что программирование в таком стиле -- это занятие настолько муторное, что нет-нет, да и забудешь где-нибудь код возврата проверить:

curl_easy_setopt(curl, CURLOPT_URL, "http://imageshack.us/upload_api.php");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlHeaders);
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formFirstItem);

Поначалу может казаться, что тут все тривиально и ошибкам возникать неоткуда... Но откуда только что берется со временем ;)

Так вот, возвращаясь к основной мысли: в последние лет 5 регулярно замечаю, что многие люди, которые явно умнее меня, пишут код вот в таком вот простом стиле. Без изысков. Без попыток упростить себе жизнь. Ув.тов.asfikon просто хороший пример этого явления -- у него есть блог, где он публикует фрагменты своего кода, поэтому приводить примеры оттуда проще.

И не очень понятно почему так.

Сам я очень давно убедился в том, что не смогу нормально писать в таком стиле. Годах в 1991-1992-ом, когда более-менее серьезно попрограммировал на Pascal и C. Не хватает у меня терпения и внимательности для того, чтобы следить за тем количеством деталей, за которым приходится следить в C. Потому и ушел на C++ как только разобрался, что к чему.

А пример работы с libcurl хорош тем, что он иллюстрирует количество вещей, за которыми нужно следить при работе с C-шным API libcurl-а. Поэтому лет шесть назад, когда довелось втягивать libcurl в тогдашние проекты, дабы не заниматься всей этой низкоуровневой кухней, была написана простенькая C++ная библиотека, практически header-only. Под катом код примера из состава этой библиотеки.

среда, 16 марта 2016 г.

[prog.c++11] Да, очень аккуратно нужно обращаться с голыми нитями

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

#include <thread>
#include <vector>
#include <chrono>
#include <iostream>

using namespace std;
using namespace std::chrono;

void run_threads( const vector< unsigned int > & times )
{
   vector< thread > threads;
   
   forsize_t i = 0; i <= times.size(); ++i )
      threads.emplace_back(
            [t = times.at(i)]{ this_thread::sleep_for( seconds{t} ); } );

   forauto & t : threads )
      t.join();
}

int main()
{
   try
   {
      run_threads( { 1234321 } );
   }
   catchconst std::exception & x )
   {
      std::cerr << x.what() << std::endl;
   }
}

Я тут намеренно спровоцировал выход за пределы вектора, дабы породить исключение. Только вот вместо нормального завершения после перехвата этого исключения программа под GCC/Clang падает с сообщением "terminate called without an active exception". Далеко не сразу удалось въехать, почему же так происходит...

Дело в том, что для уже стартовавших нитей не вызывается join. Что, видимо, и крешит приложение где-то в деструкторе std::thread.

Вот так. Казалось бы, везде объекты с деструкторами, посему и RAII должно быть во все поля... Ан нет: не вызвал join() явно -- получил гранату :) А все почему? А все потому, что не нужно с голыми потоками работать, лучше правильные инструменты использовать, которые берут на себя заботу о целой куче деталей... ;)

вторник, 15 марта 2016 г.

[prog.c++11] Этюд для программистов: методы базового класса должны возвращать ссылки на производный класс

Disclaimer: все написанное ниже будет очевидно для C++ных гуру, которые хорошо знакомы с CRTP. Однако пост писался не для гуру, а для самого себя, можно сказать, "в склерозник". Но если читатель, как и я сам, сталкивается с CRTP не чаще пары раз в году, то такому читателю может быть интересно вспомнить про CRTP еще раз и увидеть одну из ситуаций, в которых CRTP сильно выручает.

Встретился с интересной задачкой. Нужно создать несколько очень похожих друг на друга классов приблизительно вот такого вида:

[prog.c++] CSP-шные каналы при работе с голыми нитями -- это удобная штука, как оказалось...

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

  • запустить три рабочих нити;
  • дождаться от них уведомлений о начале работы;
  • дать какое-то время рабочим нитям для выполнения ими каких-то действий;
  • каждой рабочей нити дается сигнал на завершение работы;
  • от каждой рабочей нити принимается сообщение с результатами работы.

На CSP-шных каналах сделать это можно посредством трех типов сообщений и двух каналов:

struct ready {};
struct done {};
struct worker_result {...};

// Канал для передачи информации рабочим потокам.
auto cmd_ch = env.create_mchain( make_unlimited_mchain_params() );
// Канал для получения информации от рабочих потоков.
auto info_ch = env.create_mchain( make_unlimited_mchain_params() );

... // Запуск рабочих потоков с привязкой их к cmd_ch и info_ch.

// Ожидаем трех подтверждений о том, что рабочие потоки стартовали.
receive( from(info_ch).handle_n(3), [](ready){} );

... // Даем какое-то время на работу потокам.

// Указываем рабочим потокам, что пора завершать свою работу.
// Каждый поток вычитывает всего одно сообщение done из канала,
// поэтом просто отсылаем три сообщения done, по одному для каждого потока.
send< done >( cmd_ch );
send< done >( cmd_ch );
send< done >( cmd_ch );

// Ожидаем три результата от рабочих потоков.
receive( from(info_ch).handle_n(3), [](const worker_result & r) {...} );

PS. Вот еще один похожий пример.

понедельник, 14 марта 2016 г.

[prog.c++] Механизм multi chain select (он же multi channel receive) начал дышать...

Многострадальная фича, которая раньше называлась multi channel receive и которую было не очень понятно как воплощать в жизнь, начала дышать. Но уже под названием multi chain select (по аналогии с конструкцией select из языка Go).

Вызов такого select-а выглядит приблизительно таким образом:

so_5::select( wait_time,
   case_( ch_one, handlers... ),
   case_( ch_two, handlers... ),
   ...
   case_( ch_last, handlers... ) );

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

Пока готов только самый простой вариант select-а, который выбирает всего одно сообщение из любого из mchain-ов. С ожиданием появления этого сообщения или без ожидания. После некоторого тестирования приступим к реализации более продвинутого select-а, аналогичного продвинутому receive (где можно будет использовать методы вроде handle_n, extract_n и др.).

Под катом небольшой пример, в котором создается три канала (mchain-а) и три рабочих нити. Каждая рабочая нить получает один из каналов в качестве параметра и пишет в свой канал несколько целых чисел, делая паузу случайной длительности после каждой записи. Затем каждая рабочая нить закрывает свой канал и завершает работу. Главная нить посредством select-а вычитывает числа из этих трех каналов. Работа главной нити завершается, когда обнаруживается, что все каналы закрыты.