Изображение квадрата Дюрера

ООО АВТОМАТИКА плюс

Rambler's Top100

Рейтинг@Mail.ru

Замечания по потокобезопасной работе с базами данных

Версия от 20.05.2011

Статья посвящена выявлению и решению проблем, возникающих при разработке многопоточного приложения, предназначенного для работы с базой данных (БД). Статья ориентирована главным образом на Delphi-программистов, использующих в своих приложениях для работы с БД обычные компоненты, такие как TXXXConnection / TXXXDataBase, TXXXDataSet / TXXXQuery / TXXXTable, TDataModule / TRemoteDataModule и т.д. Кроме того, представлены практические советы, позволяющие повысить надежность программного обеспечения, ориентированного на работу с БД FireBird. Статью нельзя считать оконченной, поскольку настоящее количество всевозможных проблем в данной области никому не известно. При необходимости данная статья будет дорабатываться. Если считаете необходимым, то Вы можете в этом процессе участвовать. 

Предисловие

Сообщения о проблемах, возникающих при разработке многопоточного приложения, предназначенного для работы с БД, появляются в профессиональных Delphi-форумах с неизменной периодичностью. Прежде чем браться за реализацию подобного многопоточного приложения, необходимо ознакомится с проблемами, которые с высокой вероятностью могут возникнуть, а также придерживаться определенных привил. Ниже представлена более подробная информация.

1. Нельзя одновременно из нескольких потоков работать с одним компонентом-подключением!

Речь идет о компонентах (наследниках TCustomConnection), через которые организовано подключение к базе данных. К ним относятся TIBDataBase, TDataBase, TADOConnection и др. Ниже приведена информация, в которой подробно описаны причины данного ограничения.

  1. Что произойдет, если в двух параллельных потоках Т1 и Т2 создать по одному компоненту TIBDataSet (DS1 и DS2) и одновременно присоединить их к компоненту-подключению IBdb: TIBDataBase? Т.е. одновременно выполнить код DS1.Database := IBdb и DS2.Database := IBdb? Будет выполнена следующая цепочка вызовов:
    • procedure TIBCustomDataSet.SetDatabase(Value: TIBDatabase);
    • procedure TIBSQL.SetDatabase(Value: TIBDatabase);
    • procedure TIBBase.SetDatabase(Value: TIBDatabase);
    • function TIBDatabase.AddSQLObject(ds: TIBBase): Integer;
    • procedure TCustomConnection.RegisterClient(Client: TObject; Event: TConnectChangeEvent = nil);
            begin
              FClients.Add(Client);
              FConnectEvents.Add(TMethod(Event).Code);
              if Client is TDataSet then
              FDataSets.Add(Client);
            end;

      Выполняется обращение к объектам:
    • FClients: TList;
    • FDataSets: TList;
    • FConnectEvents: TList.

    Очевидно, существует вероятность, что из потоков Т1 и Т2 одновременно будет вызван код: FClients.Add(Client). Известно совершенно точно, что методы объекта TList не являются потокобезопасными. Нельзя одновременно из нескольких потоков вызывать метод TList.Add() для одного и того же объекта TList. В некоторых случаях, если повезёт, вы сразу получите сообщение об ошибке (например Access Violation at address XXXXXX), но может и не будет никакого сообщения - приложение может просто начать глючить.

  2. Что произойдет при одновременном вызове DS1.Open и DS2.Open, подключенных к одному компоненту IBdb? Для ответа на этот вопрос необходимо самым тщательным образом проанализировать исходные коды компонентов, которые Вы используете для работы с БД. Я предпочитаю СУБД FireBird и библиотеку компонентов IBX. Анализ исходных кодов библиотеки IBX, входящей в комплект Delphi 2010, вселяет уверенность, что проблем никаких не возникнет. Однако за предыдущие версии IBX не ручаюсь. Более того, старые версии клиентской библиотеки GDS32.DLL (fbclient.dll) не допускали работу с одним подключением из разных потоков (это заканчивалось ошибками, причем самыми разными). Данное ограничение исправлено в FireBird 2.5. Но если у вас возникла задача одновременного обращения к одному подключению из разных потоков - еще раз подумайте! Ваше приложение может без нареканий работать на компьютере с одноядерным процессором. Проблемы, как правило, начинаются при запуске программы на многоядерном процессоре. Возможно, наиболее простым способом решения проблемы для Вас окажется использование пула подключений. Говоря простым языком, пул подключений - это обычный список TObjectList, в которым хранятся ссылки на установленные подключения (например, созданные компоненты TIBDataBase). Подключение может извлекаться из пула, а также возвращаться в пул.
    Вместо традиционного кода:
         IBdb := TIBDataBase.Create;
         IBdb.DatabaseName := <строка подключения>
         IBdb.Params... // настройка параметров подключения и т.п.
         IBdb.Connected := True;

    достаточно вызвать одну функцию:
         IBdb := GetDataBaseFromPool(<строка подключения>, <параметры подключения>);
    Данная функция при необходимости создает новое подключение или, если в пуле подключений уже есть свободные подключения, возвращает одно из них, при этом помечает подключение как "активное".
    Вместо кода:
      IBdb.Connected := False;
      IBdb.Free;

    достаточно вызвать одну функцию:
      PutDataBaseToPool(IBdb);
    Данная функция при необходимости закрывает все открытые наборы данных, завершает активные транзакции и помечает подключение как "свободное". При этом не рекомендуется извлекать подключение из пула на длительное время. Как только потребность в подключении отпала, следует без излишнего промедления вернуть его в пул. За счет этого подключения не будут накапливаться в пуле в большом количестве, даже если
    ваше приложение активно работает с базой данных (на практике в пуле будет не более 5 подключений). Рекомендуется контролировать размер пула и своевременно закрывать подключения, которые не были востребованы длительное время.

2. При работе с FireBird нельзя одновременно из нескольких потоков выполнять подключения к БД!

Операции подключения и отключения должны быть синхронизированы с помощью любого объекта синхронизации (мьютекс, семафор, эвент, но проще всего - критическая секция) или выполняться в основном потоке. Рекомендую использовать критическую секцию, например:
     cs.Enter;
     try
       IBdb.Connected := True // или False
     finally
       cs.Leave;
     end;

Необходимо следить, чтобы коннекты и дисконнекты не выполнялись автоматически (например при вызове TIBDataSet.Open). Ими следует управлять явно! Несмотря на то, что последние версии клиентской библиотеки (начиная с FB2.5) стали намного устойчивее к одновременным подключениям (в десятки раз устойчивей, чем, например, клиентская библиотека от FB2.0), однако лучше перестраховаться путем использования объекта синхронизации. Например, на компьютере может оказаться установленной клиентская библиотека GDS32.dll от старого FireBird (бывает еще хуже - от InterBase).
Во избежание подобных проблем, рекомендую поставлять клиентскую библиотеку GDS32.dll и файл сообщений firebird.msg вместе со своим ПО. Для того, чтобы гарантированно заставить IBX использовать "правильную" библиотеку GDS32.dll, следует разместить ее в каталоге своего приложения, либо заранее загружать её ДО любого обращения к IBX с помощью следующего кода кода:
  initialization
    LoadLibrary('C:\MyProg\Firebird\GDS32.dll');

здесь же рекомендуется установить переменную окружения "FIREBIRD":
    SetEnvironmentVariable('FIREBIRD', 'C:\MyProg\Firebird\');
Переменная окружения FIREBIRD указывает клиентской библиотеке GDS32.dll каталог, в котором находятся файлы: firebird.msg, firebird.log, firebird.conf.
Наиболее важным для GDS32.dll является файл firebird.msg, в котором хранятся тексты сообщений об ошибках. Проблема в том, что на компьютере может оказаться несколько версий FireBird (на практике такое происходит очень часто), и, если явно не указать переменную "FIREBIRD", то библиотека GDS32.dll может подгрузить "чужие" сообщения об ошибках, что приведет к реальным проблемам в том случае, если логика вашего приложения зависит от точной формулировки тех или иных сообщений об ошибках.

3. При использовании Delphi 7 (или более старой версии) необходимо обновить библиотеку IBX.

Скачать IBX для Delphi 7 можно здесь http://ibase.ru/ibx/ibxdp711.zip. Компоненты IBX в стандартной поставке Delphi 7 нельзя использовать в условиях
многопоточности! Одна из основных причин заключается в том, что при создании TIBDataBase осуществлялось создание объекта TTimer, который нельзя создавать из дополнительного потока! Действительно, при создании объекта TTimer будет выполнена следующая цепочка вызовов:
     - constructor TTimer.Create(AOwner: TComponent);
     - function AllocateHWnd(AMethod: TWndMethod): HWND (модуль Classes);
     - function MakeObjectInstance(AMethod: TWndMethod): Pointer (модуль Classes);

Посмотрев внимательно на реализацию MakeObjectInstance мы обнаружим, что в данном коде отсутствует какая-либо защита от параллельных вызовов. Одновременные обращения к функции MakeObjectInstance зачастую приводят к полному зависанию приложения, либо к трудноуловимым ошибкам.

Логинов Дмитрий © 2005-2015