Правила написания thread-safe UDF

James Arias-La Rheir
InterBase Technical Services
InterBase Software Corp.
Основная задача при написании потоко-безопасных UDF – обеспечить корректное обращение к возвращаемым параметрам при многопользовательской работе. Особенности архитектуры SuperServer (IB 4.2 for NT, 5.0 for NT, Solaris, HP-UX) полностью исключают использование глобальных переменных в DLL (shared library) дл возврата результата функции, как это было принято в архитектуре Classic (IB 4.0).

Рассмотрим корректные варианты возврата параметров:
  1. FREE_IT: Этот аргумент используется в объявлении DECLARE EXTERNAL FUNCTION для того чтобы память, занятая внутри функции с помощю malloc() была освобождена IB. Объявление выходного параметра таким способом указывает серверу, что возвращаемое значение является указателем на буфер памяти, выделенный с помощю malloc, и сервер должен освободить эту память. Возвращаемое по такому принципу значение является thread-safe.
  2. Thread-local storage: Объявите переменные thread-local (threadvars в Delphi) для хранения входных и выходных значений UDF. Поскольку thread может быть использован сервером повторно для других целей (или обработки запросов другого пользователя), эти переменные нельзя использовать дл передачи данных другим threads или для хранения статический информации (используемой в другой UDF). Возвращаемое таким образом значение функции является thread-safe.
  3. RETURNS PARAMETER N: Этот аргумент используется в объявлении DECLARE EXTERNAL FUNCTION для указания, что возвращаемое значение UDF находитс во входном параметре с порядковым номером N (начиная с 1). Эта возможность непопулярна в ISC и не сертифицирована под 5.x (прим. КДВ – и даже в результате была вычеркнута из документации, хотя вполне неплохо работает). Использовать такой способ можно только на свой риск. Во всяком случае, длина выходного параметра не может быть больше длины соответствующего входного параметра. До версии 5.x такой способ передачи выходных параметров был thread-safe.

Важно отметить разницу между:
  1. Объявлением глобальной переменной и ее синхронизацией между threads.
и
  1. Объявлением переменной thread-local (threadvar).

Ядро IB Database будет корректно работать в случае B, поскольку операционна система заботится о сохранении значения переменной в пределах thread. Также IB копирует возвращаемое значение в отдельный участок памяти сразу после выполнения UDF; это также повышает безопасность переменных между нитями. За более подробной информацией по TLS обращайтесь к Win32.HLP или к книге "Windows для Профессионалов" Джефри Рихтера.

Gregory H. Deatz
Hoagland, Longo, Moran, Dunst & Doukas
 

Threadvar в Delphi работает нормально, и используется в FreeUDFLib. Однако совсем недавно было обнаружено, что на многопроцессорных компьютерах использование threadvar может привести к "падению" dll и скорее всего ibserver.exe. Чаще всего это происходит если объявить "объектный" тип как threadvar. В таком случае более надежной является прямая работа с TLSAlloc, TLSFree, TLSGetValue, TLSSetValue (и т.д.).
Данная проблема также была упомянута в Delphi Informant (август).


Кузьменко Д. В.
Epsylon Technologies
 

Хоть уважаемый James Arias-La Rheir и говорит, что функции с RETURNS PARAMETER лучше не использовать, я решил протестировать все варианты передачи и возврата строковых параметров.

Вообще способов может быть три:

1. возврат строки при помощи входного параметра  RETURNS PARAMETER . Этот способ не описан в документации и несколько неочевиден, т. к. вместо функции необходимо писать процедуру. Основное неудобство – хранимое в БД объявление функции не совпадает с объявленным, т. е. функция после создания выглядит как обычная функция с возвращаемым в виде строки параметром (а не номером параметра).

2. возврат строки, созданной внутри UDF при помощи malloc (FREE_IT). Этот способ с точки зрения производительности самый худший, поскольку память аллокируется и уничтожается при каждом вызове функции. Если функция обрабатывает значения 10000 записей, значит те же 10000 раз память будет захвачена и освобождена. С другой стороны, скорость таких операций намного выше, чем скорость получения записей с диска, поэтому влияние на скорость обработки данных будет минимальным. Обязательным является соответствие количества аллокируемой памяти внутри функции, и длина строки в объявлении функции в БД.

3. возврат строки, которая является входным параметром. Это "трюк", поскольку IB сам аллокирует память для входного параметра. Основные неудобства – размер выходной строки должен совпадать с размером входной, и невозможность использования этого метода если функция не содержит входных строковых параметров (или по типу эти параметры отличаются).

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

Для каждого случая была написана функция (см. функции function1, 2 и 3 соответственно в файле safeudf.zip). Тестировались функции следующим образом: из 3-х экземпляров Database Explorer одновременно выполнялся запрос

SELECT FUNCTION1(string) FROM EMPLOYEE, EMPLOYEE
WHERE FUNCTION1(string) <> string

где string – уникальная строка для каждого экземпляра Database Explorer, но одинаковой длины (10 символов). В том случае, если условие FUNCTION1(string) <> string по каким-то причинам нарушалось, ответ на запрос содержал бы минимум 1 строку. Это могло произойти только если результат функции перекрывался бы другим результатом функции. Такого у thread-safe функций произойти не может в принципе.
Тест показал абсолютную безопасность всех трех способов при работе в многопользовательских средах.

Временные замеры показали, что быстрее всех выполнялась функция1 (4 мин 10 сек), чуть медленнее – функция3 (4 мин 17 секунд), и медленнее всех – функция2 (4 мин 57 секунд). Различие в скорости выполнения функций не является пропорциональным объему обрабатываемых данных. Поэтому можно сказать, что все три варианта могут быть успешно использованы.

Подпишитесь на новости Firebird в России

Подписаться