2013-01-29 2 views
2

Я недавно сыграл с одним демопроектом opensource для базовой функциональности сервера TCP/IP INDY10 и наткнулся на проблему внутренней многозадачной реализации INDY и ее взаимодействия с компонентами VCL. Поскольку в SO есть много разных тем, я решил сделать простое клиент-серверное приложение и проверить некоторые предлагаемые решения и подходы, по крайней мере те, которые я правильно понял. Ниже я хотел бы обобщить и рассмотреть подход, который ранее был предложен на SO, и, если возможно, выслушать свое мнение экспертов по этому вопросу.Класс обертки для потокобезопасных объектов

Проблема: инкапсуляция VCL для поточного использования внутри клиент-серверного приложения на основе indy10.

Описание окр развития .: Delphi Версия: Delphi® XE2 Версия 16,0 INDY Версия 10.5.8.0 младший матрос Windows 7 (32Bit)

Как упоминалось в статье ([Является ли VCL Thread-safe?]) (Извините, у меня недостаточно репутации, чтобы опубликовать ссылку) особое внимание следует уделить тому, когда вы хотите использовать любой вид компонентов VCL в многопоточном (многозадачном) приложении. VCL не является потокобезопасным, но может использоваться в потоковом режиме! Как и почему обычно зависит от приложения, но можно попытаться обобщить немного и предложить какой-то общий подход к этой проблеме. Прежде всего, как и в случае INDY10, не нужно явно распараллеливать его код, т. Е. Создавать и выполнять несколько потоков, чтобы подвергать VCL взаимоблокировкам и взаимозависимостям данных.

В каждом приложении sclient-server сервер должен иметь возможность обрабатывать несколько запросов одновременно, поэтому, естественно, INDY10 внутренне реализует эту функциональность. Это означало бы, что набор классов INDY10 отвечает за управление процессами создания, выполнения и уничтожения программы внутри.

Наиболее очевидным местом, где наш код подвергается внутренней работе INDY10 и, следовательно, возможных конфликтов потоков, является метод IdTCPServerExecute (TIdTCPServer onExecute).

Естественно, INDY10 предоставляет классы (обертки), которые обеспечивают поточный поток программ, но поскольку мне не удалось получить достаточное количество объяснений относительно их применения и использования, я предпочитаю индивидуальный подход.

Ниже я описываю метод (предложенный метод основан на предыдущем комментарии я нашел в SO How to use TIdThreadSafe class from Indy10), что попытки (и, предположительно, преуспевает) в решении этой проблемы:

Вопрос, который я решить ниже является : Как создать определенный класс «MyClass» ThreadSafe?

Основная идея состоит в том, чтобы создать класс оболочки, который инкапсулирует «MyClass» и ставит в очередь потоки, которые пытаются получить к нему доступ в принципе First-In-First-Out. Основными объектами, которые используются для синхронизации, являются [Объекты критической секции Windows.].

В контексте клиент-серверного приложения «MyClass» будет содержать все небезопасные функциональные возможности нашего сервера, поэтому мы постараемся гарантировать, что эти процедуры и функции не будут выполняться одновременно более чем одним рабочим потоком. Это, естественно, означает потерю параллелизма нашего кода, но поскольку подход прост и, по-видимому, в некоторых случаях, это может быть полезным подходом.

Упаковочный Класс реализации:

constructor TThreadSafeObject<T>.Create(originalObject: T); 
begin 
    tsObject := originalObject; // pass it already instantiated instance of MyClass 
    tsCriticalSection:= TCriticalSection.Create; // Critical section Object 
end; 

destructor TThreadSafeObject<T>.Destroy(); 
begin 
    FreeAndNil(tsObject); 
    FreeAndNil(tsCriticalSection); 
    inherited Destroy; 

end; 


function TThreadSafeObject<T>.Lock(): T; 
begin 
    tsCriticalSection.Enter; 
    result:=tsObject; 

end; 

procedure TThreadSafeObject<T>.Unlock(); 
begin 
    tsCriticalSection.Leave; 
end; 

procedure TThreadSafeObject<T>.FreeOwnership(); 
begin 
    FreeAndNil(tsObject); 
    FreeAndNil(tsCriticalSection); 
end; 

MyClass Определение:

MyClass = class 

    public 
    procedure drawRandomBitmap(abitmap: TBitmap); //Draw Random Lines on TCanvas 
    function decToBin(i: LongInt): String; //convert decimal number to Bin. 
    procedure addLineToMemo(aLine: String; MemoFld: TMemo); // output message to TMemo 
    function randomColor(): TColor; 
    end; 

Использование:

Поскольку потоки выполняются в порядке и ждать поток, который имеет текущую собственность критической секции (tsCriticalSection.Enter и tsCriticalSection.Leave;) логично, что если вы хотите управлять этим правом собственности, вам нужен один уникальный экземпляр TThreadSafeObject (вы можете использовать одноэлементный шаблон). так включают в себя:

tsMyclass:= TThreadSafeObject<MyClass>.Create(MyClass.Create); 

в Form.Create и

tsMyclass.Destroy; 

в Form.Close; Здесь tsMyclass является глобальной переменной типа MyClass.

Использование:

Что касается использования MyClass попробовать следующее:

with tsMyclass.Lock do 
try 
    addLineToMemo('MemoLine1', Memo1); 
    addLineToMemo('MemoLine2', Memo1); 
    addLineToMemo('MemoLine3', Memo1); 
finally 
    // release ownership 
    tsMyclass.unlock; 
end; 

, где Memo1 является экземпляром компонента ТМето на форме.

С этим мы должны гарантировать, что все, что происходит, когда tsMyClass заблокировано будет выполняться только по одному потоку за раз. Однако очевидным недостатком этого подхода является то, что, поскольку у меня есть только один экземпляр tsMyclass, даже если один поток пытается рисовать, например. на холсте, в то время как другой пишет в Memo, первый поток должен будет дождаться завершения второго, и только тогда он сможет выполнить свою работу.

Мои вопросы здесь:

  1. ли выше предложенный метод правильно? Я все еще свободна от гонки условий или у меня есть некоторые «лазейки» в коде, откуда могут возникнуть конфликты данных ?
  2. Как можно, в общем, проверить нить небезопасность его применения?

Я хотел бы подчеркнуть, что вышеупомянутый подход никоим образом не является моим собственным. Это в основном резюме решения, найденного в 2. Тем не менее, я решил снова опубликовать сообщение, пытаясь получить какое-то закрытие по теме или какое-то доказательство действительности для предлагаемого решения. Кроме того, повторение - это мать всех знаний, как говорится.

+2

Этот вопрос длинный, я даже не знаю с чего начать. –

ответ

2

Блокировка на всем объекте может быть слишком грубой.

Представьте случаи, когда некоторые свойства или методы не зависят от других. Если блокировка работает на «глобальном» уровне, многие операции будут заблокированы без необходимости.

От Reduce lock granularity – Concurrency optimization

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

1

Первые вещи сначала: Вам не нужно осуществлять блокировку для каждого из объектов, Delphi сделал это для вас с TMonitor класса:

TMonitor.Enter(WhateverObject); 
try 
    // Your code goes here. 
finally TMonitor.Leave(WhateverObject); 
end; 

просто убедитесь, что вы освободить WhateverObject когда ваше приложение отключится, иначе вы столкнетесь с ошибкой, которую я открыл в QC: http://qc.embarcadero.com/wc/qcmain.aspx?d=111795

Во-вторых, создание многопоточности приложения несколько более активно. Вы не можете просто обернуть каждый вызов между вызовами Enter/Leave: ваша «блокировка» должна учитывать, что делает объект и каков шаблон доступа. Обертывание вызовов внутри Enter/Leave просто гарантирует, что только один поток запускает этот метод в любое время, но условия гонки намного сложнее и могут возникать в результате последовательных вызовов ваших заблокированных методов. Даже каждый из этих методов заблокирован, и только один поток когда-либо называл эти методы в любой момент времени, состояние заблокированного объекта может меняться как следствие активности другого потока.

Такого кода будет просто отлично в однопоточном приложении, но блокировка на уровне методы не хватает при переключении на многопоточный:

if List.IndexOf(Something) = -1 then 
    List.Add(Something); 
+3

Я бы не касался TMonitor, слишком багги IMHO ... – whosrdaddy

+0

@whosrdaddy +1 «TMonitor сильно сломан» - это то, что я читаю большую часть времени ... возможно, он был исправлен в XE3 или будет в XE4 – mjn

+0

@mjn, «сильно сломан», может быть немного. В QC имеется около 4 отчетов об ошибках, относящихся к одной и той же проблеме: приложение зависает при выходе, если вы не освобождаете объект, который использовался для 'TMonitor.Enter()'. –

4

С этим, мы должны убедитесь, что все, что происходит, когда tsMyClass заблокировано, будет выполняться только по одному потоку за раз. Однако очевидным недостатком этого подхода является то, что, поскольку у меня есть только один экземпляр tsMyclass, даже если один поток пытается рисовать для , например. на холсте, в то время как другой пишет в Memo, первый поток должен будет дождаться окончания второго, и только тогда он сможет выполнить свою работу.

Я вижу здесь одну большую проблему: VCL (формы, рисунок и т. Д.) Живет на основной теме. Даже если вы блокируете параллельный доступ к потоку, обновления должны выполняться в контексте основного потока. Это та часть, где вам нужно использовать Synhronize(), большая разница с блокировкой (Criticalsection) заключается в том, что синхронизированный код запускается в контексте основного потока. Конечный результат в основном тот же, ваш потоковый код сериализуется, и вы теряете преимущество использования потоков в первую очередь.

+2

Да, вы определенно ** НЕ МОЖЕТ ** использовать VCL UI-элементы управления вне контекста основного потока. Период. Включение блокировки потока вокруг элемента управления пользовательского интерфейса не будет работать, потому что основной поток не соблюдает блокировку при доступе к элементам управления. Кроме того, 'TWinControl.Свойство Handle' имеет нить-сродство, и плохие вещи случаются, если эта сродство изменяется во время выполнения. –

+1

Если вам нужен потоковый код для доступа к пользовательскому интерфейсу, вы ** ДОЛЖНЫ ** делегировать основной поток для безопасного доступа. 'TThread' имеет' Synchronize() 'и' Queue() 'методы для этой цели. Для этой цели Indy также имеет свои собственные классы TIdSync и 'TIdNotify'. –

+0

ОК, ребята, а как насчет того, переопределяю MyClass наследовать от TThread и вызывать его методы, используя, например, «Synchronize (addLineToMemo)» и т. Д. Насколько я понял, Synchronize принимает в качестве входных только процедуры, которые не имеют входных параметров. Поэтому, чтобы вызвать addLineToMemo с правильными входными данными, я должен установить входные данные как частные поля MyClass (используя общедоступный метод обёртки), а затем вызвать Synchronize (addLineToMemo) из этой оболочки. Это безопасный поток? – kenny

Смежные вопросы