Содержание


Практическое использование MySQL++

Часть 7. Применение в многопоточных приложениях

Comments

Серия контента:

Этот контент является частью # из серии # статей: Практическое использование MySQL++

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Практическое использование MySQL++

Следите за выходом новых статей этой серии.

Многопоточность (multithreading) из модной технической новинки уже давно превратилась в средство реального повышения эффективности работы приложений. В данной статье я попытаюсь продемонстрировать, как можно использовать библиотеку MySQL++ в многопоточных приложениях.

Сразу необходимо отметить, что сама по себе библиотека MySQL++ не является "многопоточно ориентированной" и не содержит каких-либо средств поддержки корректности выполнения потоков. Основная причина этого заключается в том, что эффективность операций MySQL++ в большей степени зависит от производительности подсистем ввода-вывода, чем от производительности процессора (процессоров). То есть, если вы определили, что "узким местом" в вашем приложении являются именно объекты библиотеки MySQL++, например, те, что выполняют операции обмена данными с удалённым сервером СУБД, то увеличение количества потоков в таком приложении вряд ли решит возникшую проблему.

Далее я опишу некоторые нюансы применения MySQL++ в многопоточных приложениях, но окончательное решение - делать ли приложение многопоточным - как всегда, в каждом конкретном случае остаётся за разработчиком.

1. Подготовка MySQL++ к созданию многопоточных приложений

1.1. Поддержка многопоточности в самой библиотеке

Если вы собираете библиотеку MySQL++ из исходных текстов, то при запуске скрипта configure необходимо указать флаг --enable-thread-check, иначе библиотека будет скомпонована без поддержки многопоточности.

1.2. Компоновка программ с многопоточной версией MySQL C API

При установке дистрибутивных пакетов MySQL в Linux и в других Unix-системах вы обычно получаете две версии библиотеки поддержки MySQL C API-интерфейса: с поддержкой многопоточности и без неё. В каталоге /usr/lib/mysql расположены файлы обеих версий: libmysqlclient и libmysqlclient_r. Во втором случае суффикс "_r" означает "reentrant" - "с возможностью повторного входа" - именно эта версия нужна для многопоточных программ.

1.3. Сборка приложения с поддержкой многопоточности

Обычно при сборке программ поддержка многопоточности по умолчанию не включена, и её активизацию нужно указывать в явной форме. Здесь всё зависит от конкретного компилятора и компоновщика, а также от установленных в системе библиотек, реализующих многопоточность. При использовании gcc обычно подключают POSIX-потоки (флаг -pthreads). Возможны и другие варианты (многие разработчики отдают предпочтение более современной библиотеке NPTL).

2. Управление соединением при многопоточности

2.1. Проблема "параллельных запросов"

MySQL C API-интерфейс, лежащий в основе библиотеки MySQL++, не допускает возможности выполнения нескольких параллельных запросов в рамках одного соединения. С этим ограничением вполне можно столкнуться и при разработке программы без многопоточности, а уж в приложении, использующем несколько потоков, данная проблема станет ещё более острой.

Самое простое решение состоит в создании отдельных объектов Connection для каждого потока, в котором предполагается выполнение запроса к базе данных. Это решение является более или менее удовлетворительным в тех случаях, когда количество потоков, выполняющих запросы, невелико, и каждый поток использует своё соединение достаточно часто, чтобы не допустить отключения от сервера БД "по таймауту".

Но если количество потоков в приложении достаточно велико, а запросы выполняются не слишком часто, то предложенное выше решение становится крайне неэффективным. В подобной ситуации лучше создать пул соединений на основе класса ConnectionPool, предлагаемого библиотекой MySQL++. Идея проста: этот класс управляет набором Connection-объектов, то есть, по требованию потока ему выделяется свободный Connection-объект, а после завершения операции поток возвращает этот объект в пул. Это позволяет свести к минимуму количество активных соединений и не держать их постоянно в режиме пассивного ожидания.

В классе ConnectionPool имеются три метода, которые разработчик должен реализовать (точнее, выполнить их замещение, overriding) в подклассе: create(), destroy() и max_idle_time(). Кроме того, ConnectionPool позволяет выполнить замещение метода release(), если это необходимо.

При проектировании собственного подкласса, производного от ConnectionPool, разработчик может организовать его в виде синглтона (Singleton), предлагаемого известной "группой-четвёркой" (Гамма [Gamma], Хелм [Helm], Джонсон [Johnson], Влиссидес [Vlissides]), поскольку в программе должен присутствовать только один пул объектов.

2.2. Пример организации пула соединений в многопоточном приложении

Исходный код примера размещается в двух файлах. Первый - заголовочный файл my_pool.h:

#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <mysql++.h>
#include <iostream>

#define WITH_THREADS

using namespace std;

// Определение собственного класса, производного от ConnectionPool
class MyConnectionPool : public mysqlpp::ConnectionPool
{
public:
  MyConnectionPool( const char *db, const char *server,
                    const char *user, const char *password )
  {
    p_db = (db ? db : "" );
    p_server = (server ? server : "" );
    p_user = (user ? user : "" );
    p_password = (password ? password : "" );
  }
  // В деструкторе обязательно нужно вызвать метод ConnectionPool::clear()
  ~MyConnectionPool()
  {
    clear();
  }
protected:
  // замещение методов суперкласса
  mysqlpp::Connection* create()
  {
    cout << "Создание соединения..." << endl; cout.flush();
    return new mysqlpp::Connection( 
     p_db.empty() ? 0 : p_db.c_str(), p_server.empty() ? 0 : p_server.c_str(),
     p_user.empty() ? 0 : p_user.c_str(),
     p_password.empty() ? 0 : p_password.c_str() );
  }

  void destroy( mysqlpp::Connection *con )
  {
    cout << "Удаление соединения..." << endl; cout.flush;
    delete con;
  }

  unsigned int max_idle_time()
  {
    // В данном примере достаточно 3 секунд
    // В реальном приложении время ожидания следует вычислять в соответствии
    // с условиями выполнения (таймаут сервера, кол-во соединений и т.д.)
    return 3;
  }
private:
  std::string p_db, p_server, p_user, p_password;   // параметры соединения
};

// Объявление глобального указателя на объект только что определённого класса,
// поскольку в любом случае в программе может существовать только один пул.
MyConnectionPool *pool = 0;

#if defined(WITH_THREADS)
static void* work_thread( void *running_flag )
{
  mysqlpp::Connection::thread_start();
  for( int i=0; i < 6; i++ )
  {
    mysqlpp::Connection *con = pool->grab();
    if( !con )
    {
      cerr << "Ошибка при получении соединения из пула" << endl;
      break;
    }
    // Запрос на получение содержимого таблицы wares.
    // Для краткости каждая полученная строка обозначена звёздочкой
    mysqlpp::Query query( con->query( "SELECT * FROM wares" ) );
    mysqlpp::StoreQueryResult res = query.store();
    for( int j=0; j < res.num_rows(); j++ )
      cout.put('*');

    // Запрос выполнен - немедленно освобождаем соединение
    pool->release( cp );
    // Задержка от 1 до 4 секунд прежде чем данное соединение можно будет
    // использовать повторно. В тех случаях, когда время задержки превышает 
    // макс. время ожидания, создаётся новое соединение в след.итерации цикла
    sleep( rand()%4 + 1 );
  }
  // Оповещение основной программы о том, что данный поток больше не используется
  *reinterpret_cast<bool*>(running_flag) = false;
  mysqlpp::Connection::thread_end();
  return 0;
}

static int create_thread( (void *)(*worker)(void *), (void *)arg )
{
  pthread_t ptrd;
  return pthread_create( &ptrd, 0, worker, arg );
}
#endif

Во втором файле - my_pool.cpp - содержится исходный код самой программы:

#include "my_pool.h"

int main( int argc, char *argv[] )
{
#if defined(WITH_THREADS)
  pool = new MyConnectionPool( "test_db", "localhost", "tdb_user", "tdb_password" );
  try
  {
    mysqlpp::Connection *con = pool->grab();
    if( !con->thread_aware() )
    {
      cerr << "MySQL++ собрана без поддержки потоков" << endl;
      return -1;
    }
    pool->release( cp );
  }
  catch( mysqlpp::Exception &ex )
  {
    cerr << "Ошибка при инициализации пула соединений: " << ex.what() << endl;
    return -1;
  }
  cout << "Пул соединений успешно создан. Начинаем работу с потоками..." << endl;
  srand( time(0) );

  bool running[] = { true, true, true, true, true, true, true, true, true, true };
  const size_t num_threads = sizeof(running) / sizeof(running[0]);

  for( size_t i=0; i < num_threads; i++ )
  {
    if( int err = create_thread( work_thread, (running+i) ) )
    {
      cerr << "Ошибка при создании потока с номером " << i << 
              ": код ошибки " << err << endl;
      return -1;
    }
  }

  // Проверка флагов активности потоков каждую секунду до тех пор,
  // пока выполнение всех потоков не будет завершено
  cout << "Ожидание завершения потоков..." << endl;
  cout.flush();
  do
  {
    sleep(1);
    i = 0;
    while( i < num_threads && !running[i] )
      i++;
  } while( i < num_threads );
  cout << endl << "Все потоки остановлены" << endl;
  delete pool;
  cout << "Завершение работы программы" << endl;
#else
  cout << "Программе " << argv[0] << " необходима поддержка многопоточности" << endl;
#endif
  return 0;
}

Вообще говоря, пул соединений на основе класса ConnectionPool можно использовать и в программе с одним потоком.

3. Статические методы управления потоками в MySQL++

Объект Connection содержит несколько статических методов, предназначенных как раз для тех случаев, когда MySQL++ применяется в многопоточном приложении.

3.1. Детектор многопоточного режима

Метод Connection::thread_aware() вызывается для того, чтобы определить, включена ли в текущей версии MySQL++ (и в "нижелещащей" библиотеке MySQL C API) поддержка работы с потоками. Следует отметить, что поддержка вовсе не означает гарантию корректности одновременной работы нескольких потоков. Корректность и безопасность многопоточного режима должен обеспечить сам разработчик.

3.2. Активизация потока

Если в приложении стратегия управления соединениями позволяет некоторому потоку вместо инициализации собственного соединения воспользоваться Connection-объектом, созданным другим потоком, то из первого потока обязательно должен быть вызван метод Connection::thread_start() до того, как этот поток начнёт что-либо делать с помощью MySQL++. Если поток создаёт новое соединение раньше, чем использует "чужое", то нет необходимости в вызове метода Connection::thread_start(), поскольку все требуемые ресурсы уже неявно выделены данному потоку.

Именно поэтому безотказно работает простейшая стратегия "одно соединение на один поток": каждый поток, использующий MySQL++, создаёт собственное соединение, и при этом ему неявно распределяются все необходимые ресурсы. Таким образом, вызов Connection::thread_start() здесь не требуется, хотя если метод будет всё-таки вызван, то ничего страшного не произойдёт.

Противоположным примером является пул соединений (ConnectionPool): приходится вызывать Connection::thread_start() в начале каждого стартующего потока, поскольку на практике невозможно предсказать, получит ли этот поток вновь созданное соединение из пула, или ему будет предоставлено повторно используемое соединение, ранее возвращённое в пул другим потоком. Конечно, можно полагаться на такие ситуации, в которых гарантируется, что при самом первом обращении к пулу "клиент" будет всегда получать новое соединение, вызывая метод ConnectionPool::grab(), но лучше пойти безопасным путём и всегда вызывать Connection::thread_start() в момент начала работы активизируемого потока.

3.3. Деактивизация потока

Вполне логично, что для завершения работы потока предназначен метод Connection::thread_end(). Авторы библиотеки в документации сообщают, что "вызов этого метода, вообще говоря, не является обязательным", и обосновывают это малым размером памяти, выделяемой потоку, и отсутствием тенденции к её росту. Позволю себе не согласиться с подобным подходом, так как предпочитаю не оставлять в программах потенциальные источники "утечек памяти".

4. Совместное использование структур данных несколькими потоками

Результаты запроса кроме собственно строк данных содержат ещё и информацию о столбцах каждой строки. Так как информация о столбцах одинакова для всех строк в наборе результатов, то до версии 3.0 библиотеки MySQL++ эта информация сохранялась в объекте ResultSet, а каждая строка содержала обратную ссылку на этот "родительский" объект, чтобы при необходимости получать характеристики столбцов. Но в многопоточных программах достаточно часто запросы выполняет один поток, а результаты запросов обрабатывают другие потоки. Возникают проблемы "жизненного цикла" объектов запроса и проблемы совместного доступа к ним, которые требуют выработки тщательно продуманной стратегии блокировок.

В версии 3.0 и более поздних авторы библиотеки MySQL++ сделали время существования ("жизненный цикл") таких совместно используемых структур данных независимым от ResultSet-объекта, который их создаёт. Эти независимые совместно используемые структуры данных существуют до тех пор, пока не будет удалён последний объект, который к ним обращался. Тем не менее, авторы продолжают работу по обеспечению безопасности при совместном использовании структур данных несколькими потоками. На сегодняшний день их рекомендации таковы: хранить все данные о запросе в одном потоке.

Заключение

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

В этом цикле статей были рассмотрены многие аспекты практического использования библиотеки MySQL++. Подводя итоги, хочу сказать, что достоинств обнаружилось больше, чем недостатков, и библиотека вполне может быть названа удобным инструментальным средством для разработчиков приложений, имеющих дело с СУБД MySQL.


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=514246
ArticleTitle=Практическое использование MySQL++: Часть 7. Применение в многопоточных приложениях
publish-date=08312010