Интернет-журнал "Домашняя лаборатория", 2007 №6
Шрифт:
Проверим это, испортив наш сервер для большей наглядности. В локальной переменной метода Add сохраняется текущая величина счета, после чего текущий поток засыпает на 1 миллисекунду и, проснувшись, делает текущее значение счета равным сумме значения, сохраненного в локальной переменной, и полученной от клиента величине нового вклада.
Сервер
……
using System.Threading;
……
public class Account: MarshalByRefObject, IAccumulator, IAudit {
……
public void Add(int sum) {
int s = _sum;
Thread.Sleep(1);
_sum = s + sum;
}
…..
Клиент
Клиент
…..
int sentTotal = 0;
for (int i = 0; i < 1000; i++) {
a. Add(5);
sentTotal +=5;
}
Console.WriteLine("Sent totally by this client = {0}",
sentTotal);
…..
Запустим сервер и затем с небольшим временным интервалом двух клиентов. Каждый из запущенных клиентов по завершении своей работы выведет на свою консоль следующие строки:
>МуАрр. ехе
Sent totally by this client = 5000 Bye
>
Несложно написать клиентскую программу, которая обращается к работающему серверу и выводит на свою консоль текущую величину счета. Здесь для краткости эта программа (total.ехе) не приведена, но для примера приводится результат ее работы (программа total была запущена после завершения работы обоих клиентов)
>total.ехе
Received by server from all clients = 7240
Bye
>
Здесь естественно возникает вопрос — почему итоговая сумма не равна ожидаемой величине (10000)? Причина кроется в том, что наш сервер не является потоко-безопасным. При его работе возникают ситуации, когда один поток запомнил текущую сумму счета и заснул, а тем временем другой поток успел эту сумму обновить. Проснувшись, первый поток работает со старой величиной счета, не зная об его обновлении вторым потоком. Конкретная величина расхождения суммы на счете с ожидаемыми 10000 зависит от временного интервала между моментами запуска обоих клиентов.
Простое решение проблемы — создать контекст синхронизации, в который и поместить серверный компонент. В этом случае новый поток блокируется при входе этот контекст, если в нем исполняется какой-либо другой поток. Для этого достаточно пометить наш класс Account атрибутом [Synchronization ] и привязать его к контексту, в котором он будет создаваться. Для этого класс Account должен наследовать классу ContextBoundObject, который в свою очередь является производным от класса MarshalByRefObject.
Ниже приведен полный код сервера, для которого вышеописанных проблем синхронизации больше нет
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Threading;
using System.Runtime.Remoting.Contexts;
namespace MyServer {
public interface IAccumulator {
void Add(int sum);
}
public interface IAudit {
int Total;
}
[Synchronization]
public class Account: ContextBoundObject, IAccumulator, IAudit {
protected int _sum = 0;
public void Add(int sum) {
int s = _sum;
Thread.Sleep(1);
_sum = s + sum;
}
public int Total {
return _sum;
}
}
public class AccountApp {
public static void Main {
HttpChannel myChannel = new HttpChannel(8080);
ChannelServices.RegisterChannel(myChannel);
RemotingConfiguration.RegisterWellKnownServiceType {
typeof(Account),
"Account",
WellKnownObjectMode.Singleton);
Console.WriteLine("Server is listening");
Console.ReadLine;
Console.WriteLine("Bye");
}
}
}
И
несколько комментариев. В.NET синхронизацию можно обеспечить либо с помощью критических секций, встраиваемых в код компонента, либо декларативно с помощью контекста синхронизации. Последнее похоже на использование понятия активности в СОМ+.Привязка объекта к контексту позволяет использовать при работе с этим объектом различные сервисы (например, синхронизации, поддержки транзакций и т. п.). Но зато и обращаться к такому объекту извне его контекста можно только через прокси. Однако это прозрачно для клиента, не нужно создавать и регистрировать какие-либо каналы (при работе в рамках одного домена приложения). Если объект не привязан к контексту, то он располагается в специальном контексте по умолчанию. К контексту по умолчанию имеют прямой доступ все потоки, исполняемые в данном домене приложения.
Набор сервисов, доступных объектам привязанным к контексту, можно расширять вводя новые атрибуты и реализуя перехват вызовов методов этих объектов. Подробнее эти важные вопросы будут рассмотрены в следующем разделе.
NET и аспектно-ориентированное программирование
Введение
Говоря про компонентное программирование нельзя не упомянуть про парадигму аспектно-ориентированного программирования (АОП). Элементы этой парадигмы встречаются в области технологии программирования уже достаточно давно. Это субъектно — ориентированное программирование (subject — oriented programming) [SOP1, SOP2], композиционные фильтры (composition filters) [CF1, CF2], адаптивное программирование (adaptive programming) [AP1, АР2]. В наиболее явной форме формулировка данной парадигмы представлена в работе [АОР1]. Хороший обзор по АОП представлен в диссертации [АОР2]. И, наконец, связи между АОП и .NET отражены в статье [АОР3].
С точки зрения АОП в процессе разработки достаточно сложной системы программист решает две ортогональные задачи:
• Разработка компонентов
• Разработка сервисов, поддерживающих взаимодействие компонентов
Такие языки программирования как, например, C++, VB и т. п. ориентированы прежде всего на решение первой задачи. Код компонента представляется в виде класса, т. е. он хорошо локализован и, следовательно, его легко просматривать, изучать, модифицировать, повторно использовать. С другой стороны, при программировании процессов, в которые вовлечены различные объекты, мы получаем код, в котором элементы, связанные с поддержкой такого процесса, распределены по коду всей системы. Эти элементы встречаются в коде множества классов, их совокупность в целом не локализована в обозримом сегменте кода. В результате мы сталкиваемся с проблемой "запутанного" кода (code tangling).