Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows



Pdf просмотр
страница29/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12597
Скачиваний0
1   ...   25   26   27   28   29   30   31   32   ...   68
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ работаем с ресурсом g_SomeSharedData
M
} // Примечание LeaveCriticalSection вызывается, когда gDummy
// выходит за пределы области видимости.
Следующий C++ класс, CInterlockedType, содержит все, что нужно для создания объекта данных, безопасного в многопоточной среде. Я сделал CInterlockedType классом шаблона, чтобы его можно было применять для любых типов данных. Поэтому
Вы можете использовать его, например, с целочисленной переменной, строкой или произвольной структурой данных.
Каждый экземпляр объекта CInterlockedType содержит два элемента данных. Первый это экземпляр шаблонного типа данных, который Вы хотите сделать безопасным в многопоточной среде. Он является закрытыми им можно манипулировать только через функции члены класса CInterlockedType. Второй элемент данных представляет собой экземпляр объекта CResGuard, так что класс, производный от CInter lockedType, может легко защитить свои данные.
Предполагается, что Вы всегда будете создавать свой класс, используя класс CInter lockedType как базовый. Ранее я уже говорил, что класс CInterlockedType предоставляет все необходимое для создания объекта, безопасного в многопоточной среде, но производный класс должен сам позаботиться о корректном использовании элементов Класс CInterlockedType содержит всего четыре открытые функции конструктор,
инициализирующий объект данных, и конструктор, не инициализирующий этот объекта также виртуальный деструктор, который ничего не делает, и оператор приведения типа (cast operator). Последний просто гарантирует безопасный доступ к данным,
охраняя ресурс и возвращая текущее значение объекта. (Ресурс автоматически раз блокируется при выходе локальной переменной
x за пределы ее области видимости.)
Этот оператор упрощает безопасную проверку значения объекта данных, содержащегося в классе.
В классе CInterlockedType также присутствуют три невиртуальные защищенные функции, которые будут вызываться производным классом. Две функции
GetVal возвращают текущее значение объекта данных. В отладочных версиях файла обе эти функции сначала проверяют, охраняется ли объект данных. Если бы он не охранялся могла бы вернуть значение объекта, а затем позволить другому потоку изменить его до того, как первый поток успеет что то сделать с этим значением. Я предполагаю, что вызывающий поток получает значение объекта для того, чтобы как то изменить его. Поэтому функции
GetVal требуют от вызывающего потока охраны доступа к данным. Определив, что данные охраняются, функции
GetVal возвращают текущее значение.
Эти функции идентичны стем исключением, что одна из них манипулирует константной версией объекта. Благодаря этому Вы можете без проблем писать код, работающий как с константными, таки с неконстантными данными.
Третья невиртуальная защищенная функция член —
SetVal. Желая модифицировать данные, любая функция член производного класса должна защитить доступ к этим данным, а потом вызвать функцию
SetVal. Как и GetVal, функция SetVal сначала проводит отладочную проверку, чтобы убедиться, не забыл ли код производного класса защитить доступ к данным. Затем
SetVal проверяет, действительно ли данные изменяются. Если да,
SetVal сохраняет старое значение, присваивает объекту новое значение и вызывает виртуальную защищенную функцию член
OnValChanged, передавая ей оба значения. В классе CInterlockedType последняя функция реализована так, что она

259
Г ЛАВА 10
Полезные средства для синхронизации потоков ничего не делает. Вы можете использовать эту функцию член для того, чтобы расширить возможности своего производного класса, но об этом мы поговорим, когда дойдем до рассмотрения класса До сих пор речь шла в основном об абстрактных классах и концепциях. Теперь посмотрим, как пользоваться этой архитектурой на благо всего человечества. Я представлю Вам CInterlockedScalar — класс шаблона, производный от CInterlockedType. Сего помощью Вы сможете создавать безопасные в многопоточной среде скалярные
(простые) типы данных — байт, символ, 16 , 32 или 64 битное целое, вещественное значение (с плавающей точкой) и т. д. Поскольку CInterlockedScalar является производным от класса CInterlockedType, у него нет собственных элементов данных. Конструктор просто обращается к конструктору CInterlockedType, передавая ему начальное значение объекта скалярных данных. Класс CInterlockedScalar работает только с числовыми значениями, ив качестве начального значения я выбрал нуль, чтобы наш объект всегда создавался в известном состоянии. Ну а деструк тор класса CInterlockedScalar вообще ничего не делает.
Остальные функции члены класса CInterlockedScalar отвечают за изменение скалярного значения. Для каждой операции над ним предусмотрена отдельная функция член. Чтобы класс CInterlockedScalar мог безопасно манипулировать своим объектом данных, все функции члены перед выполнением какой либо операции блокируют доступ к этому объекту. Функции очень просты, и я не стану подробно объяснять их;
просмотрев исходный код, Вы сами поймете, что они делают. Однако я покажу, как пользоваться этими классами. В следующем фрагменте кода объявляется безопасная в многопоточной среде переменная типа BYTE и над ней выполняется серия операций:
CInterlockedScalar b = 5; // безопасная переменная типа BYTE
BYTE b2 = 10;
// небезопасная переменная типа BYTE
b2 = b++;
// b2=5, b=6
b *= 4;
// b=24
b2 = b;
// b2=24, b=24
b += b;
// b=48
b %= 2;
// Работа с безопасной скалярной переменной также проста, как и с небезопасной.
Благодаря замещению (перегрузке) операторов в C++ даже код в таких случаях фактически одинаков С помощью C++ классово которых я уже рассказал, любую небезопасную переменную можно легко превратить в безопасную, внеся лишь минимальные изменения в исходный код своей программы.
Проектируя все эти классы, я хотел создать объект, чье поведение было бы противоположно поведению семафора. Эту функциональность предоставляет мой класс CWhenZero, производный от CInterlockedScalar. Когда скалярное значение равно, объект CWhenZero пребывает в свободном состоянии, а когда оно неравно в занятом.
Как Вам известно, C++ объекты не поддерживают такие состояния — в них могут находиться только объекты ядра. Значит, в CWhenZero нужны дополнительные элементы данных с описателями объектов ядра событие. Я включил в объект CWhenZero два элемента данных
m_hevtZero (описатель объекта ядра событие, переходящего в свободное состояние, когда объект данных содержит нулевое значение) и
m_hevt
NotZero (описатель объекта ядра событие, переходящего в свободное состояние,
когда объект данных содержит ненулевое значение).
Конструктор CWhenZero принимает начальное значение для объекта данных, а также позволяет указать, какими должны быть объекты ядра событие — со сбросом

260
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
вручную (по умолчанию) или с автосбросом. Далее конструктор, вызывая
CreateEvent,
создает два объекта ядра событие и переводит их в свободное или занятое состояние в зависимости оттого, равно ли нулю начальное значение. Деструктор CWhenZero просто закрывает описатели этих двух объектов ядра. Поскольку CWhenZero открыто наследует от класса CInterlockedScalar, все функции члены перегруженного оператора доступны и пользователям объекта Помните защищенную функцию член
OnValChanged, объявленную внутри класса Так вот, класс CWhenZero замещает эту виртуальную функцию. Она отвечает за перевод объектов ядра событие в свободное или занятое состояние в соответствии со значением объекта данных.
OnValChanged вызывается при каждом изменении этого значения. Ее реализация в CWhenZero проверяет, равно ли нулю новое значение. Если да, функция устанавливает событие
m_hevtZero и сбрасывает событие
m_hevtNotZero. Нет — все делается наоборот.
Теперь, если Вы хотите, чтобы поток ждал нулевого значения объекта данных, от
Вас требуется лишь следующее:
CWhenZero b = 0; // безопасная переменная типа BYTE
// немедленно возвращает управление, так как b равна 0
WaitForSingleObject(b, INFINITE);
b = 5;
// возвращает управление, только если другой поток присваивает b нулевое значение, Вы можете вызывать
WaitForSingleObject именно таким образом, потому что класс включает и функцию член оператора приведения, которая приводит объект к типу HANDLE объекта ядра. Иначе говоря, передача C++ объекта любой Windows функции, ожидающей HANDLE, приводит к автоматическому вызову функции члена оператора приведения, возвращаемое значение которой и передается Windows функции. В данном случае эта функция член возвращает описатель объекта ядра событие
m_hevtZero.
Описатель события
m_hevtNotZero внутри класса CWhenZero позволяет писать код,
ждущий ненулевого значения объекта данных. К сожалению, в класс нельзя включить второй оператор приведения HANDLE — для получения описателя
m_hevtNotZero.
Поэтому мне пришлось добавить функцию член
GetNotZeroHandle, которая используется так:
CWhenZero b = 5; // безопасная переменная типа BYTE
// немедленно возвращает управление, так как b неравна возвращает управление, только если другой поток присваивает b ненулевое значение, INFINITE);
Программа-пример InterlockedType
Эта программа, «10 InterlockedType.exe» (см. листинг на рис. 10 2), предназначена для тестирования только что описанных классов. Файлы исходного кода и ресурсов этой

261
Г ЛАВА 10
Полезные средства для синхронизации потоков программы находятся в каталоге 10 InterlockedType на компакт диске, прилагаемом к книге. Как я уже говорил, такие приложения я всегда запускаю под управлением отладчика, чтобы наблюдать за всеми функциями и переменными — членами классов.
Программа иллюстрирует типичный сценарий программирования, который выглядит так. Поток порождает несколько рабочих потоков, а затем инициализирует блок памяти. Далее основной поток пробуждает рабочие потоки, чтобы они начали обработку содержимого этого блока памяти. В данный момент основной поток должен приостановить себя до тех пор, пока все рабочие потоки не выполнят свои задачи.
После этого основной поток записывает в блок памяти новые данные и вновь пробуждает рабочие потоки.
На примере этого кода хорошо видно, насколько тривиальным становится решение этой распространенной задачи программирования при использовании C++. Класс дает нам гораздо больше возможностей — не один лишь инверсный семафор. Мы получаем теперь безопасный в многопоточной среде объект данных, который переходит в свободное состояние, когда его значение обнуляется! Вы можете не только увеличивать и уменьшать счетчик семафора на 1, но и выполнять над ним любые математические и логические операции, в том числе сложение, вычитание,
умножение, деление, вычисления по модулю Так что объект CWhenZero намного функциональнее, чем объект ядра «семафор».
С этими классами шаблонов C++ можно много чего придумать. Например, создать класс CInterlockedString, производный от CInterlockedType, и сего помощью безопасно манипулировать символьными строками. А потом создать класс CWhenCertain
String, производный от CInterlockedString, чтобы освобождать объект ядра «событие»,
когда строка принимает определенное значение (или значения. В общем, возможности безграничны.
IntLockTest.cpp
/******************************************************************************
Модуль: Автор Copyright (c) 2000, Джеффри Рихтер (Jeffrey Richter)
******************************************************************************/
#include "..\CmnHdr.h"
/* см. приложение А */
#include
#include "Interlocked.h"
///////////////////////////////////////////////////////////////////////////////
// присваиваем TRUE, когда рабочие потоки должны завершиться volatile BOOL g_fQuit = FALSE;
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI WorkerThread(PVOID pvParam) {
CWhenZero& bVal = * (CWhenZero *) pvParam;
// должен ли рабочий поток завершиться (!g_fQuit) {
Рис. 10-2.
Программа-пример InterlockedType
см. след. стр.

262
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Рис. 10-2.
продолжение
// ждем какой нибудь работы, INFINITE);
// если мы должны выйти — выходим if (g_fQuit)
continue;
// что то делаем chMB("Worker thread: We have something to do");
bVal
; // сделали ждем, когда остановятся все рабочие потоки, INFINITE);
}
chMB("Worker thread: terminating");
return(0);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {
// инициализируем объект так, чтобы указать ни одному рабочему потоку делать нечего
CWhenZero bVal = 0;
// создаем рабочие потоки const int nMaxThreads = 2;
HANDLE hThreads[nMaxThreads];
for (int nThread = 0; nThread < nMaxThreads; nThread++) {
DWORD dwThreadId;
hThreads[nThread] = CreateThread(NULL, 0,
WorkerThread, (PVOID) &bVal, 0, &dwThreadId);
}
int n;
do {
// делать что то еще или остановиться?
n = MessageBox(NULL,
TEXT("Yes: Give worker threads something to do\nNo: Quit"),
TEXT("Primary thread"), MB_YESNO);
// сообщаем рабочим потокам, что мы выходим if (n == IDNO)
InterlockedExchangePointer((PVOID*) &g_fQuit, (PVOID) TRUE);
bVal = nMaxThreads; // пробуждаем рабочие потоки if (n == IDYES) {

263
Г ЛАВА 10
Полезные средства для синхронизации потоков
Рис. 10-2.
продолжение
// есть работа, ждем, когда рабочие потоки ее сделают, INFINITE);
}
} while (n == IDYES);
// работы больше нет, процесс надо завершить ждем завершения рабочих потоков, hThreads, TRUE, INFINITE);
// закрываем описатели рабочих потоков for (nThread = 0; nThread < nMaxThreads; nThread++)
CloseHandle(hThreads[nThread]);
// сообщаем пользователю, что процесс завершается chMB("Primary thread: terminating");
return(0);
}
//////////////////////////////// Конец файла //////////////////////////////////
Interlocked.h
/******************************************************************************
Модуль:
Interlocked.h
Автор: Copyright (c) 2000, Джеффри Рихтер (Jeffrey Richter)
******************************************************************************/
#pragma once
///////////////////////////////////////////////////////////////////////////////
// к экземплярам этого класса будет обращаться множество потоков, поэтому все его члены (кроме конструктора и деструктора) должны быть безопасны в многопоточной среде class CResGuard {
public:
CResGuard()
{ m_lGrdCnt = 0; InitializeCriticalSection(&m_cs); }
CResGuard() { DeleteCriticalSection(&m_cs); }
// IsGuarded используется для отладки IsGuarded() const { return(m_lGrdCnt > 0); }
public:
class CGuard {
public:
CGuard(CResGuard& rg) : m_rg(rg) { m_rg.Guard(); };
CGuard() { m_rg.Unguard(); }
см. след. стр.

264
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Рис. 10-2.
продолжение
private:
CResGuard& m_rg;
};
private:
void Guard() { EnterCriticalSection(&m_cs); m_lGrdCnt++; }
void Unguard() { m_lGrdCnt
; LeaveCriticalSection(&m_cs); }
// к Guard/Unguard может обращаться только вложенный класс CGuard friend class CResGuard::CGuard;
private:
CRITICAL_SECTION m_cs;
long m_lGrdCnt; // число вызовов EnterCriticalSection
};
///////////////////////////////////////////////////////////////////////////////
// к экземплярам этого класса будет обращаться множество потоков, поэтому все его члены (кроме конструктора и деструктора) должны быть безопасны в многопоточной среде template
class CInterlockedType {
public: // открытые функции члены Примечание конструкторы и деструкторы всегда безопасны в многопоточной среде) { }
CInterlockedType(const TYPE& TVal) { m_TVal = TVal; }
virtual
CInterlockedType()
{ }
// оператор приведения, который упрощает написание кода с использованием безопасного в многопоточной среде типа данных operator TYPE() const {
CResGuard::CGuard x(m_rg);
return(GetVal());
}
protected: // защищенная функция, которую должен вызывать производный класс GetVal() {
chASSERT(m_rg.IsGuarded());
return(m_TVal);
}
const TYPE& GetVal() const {
assert(m_rg.IsGuarded());
return(m_TVal);
}
TYPE SetVal(const TYPE& TNewVal) {
chASSERT(m_rg.IsGuarded());

265
Г ЛАВА 10
Полезные средства для синхронизации потоков
Рис. 10-2.
продолжение
TYPE& TVal = GetVal();
if (TVal != TNewVal) {
TYPE TPrevVal = TVal;
TVal = TNewVal;
OnValChanged(TNewVal, TPrevVal);
}
return(TVal);
}
protected: // замещаемые функции virtual void OnValChanged(
const TYPE& TNewVal, const TYPE& TPrevVal) const {
// здесь ничего не делается защищенный член класса, охраняющий ресурс используется функциями производного класса mutable CResGuard m_rg;
private: // закрытые элементы данных m_TVal;
};
///////////////////////////////////////////////////////////////////////////////
// к экземплярам этого класса будет обращаться множество потоков, поэтому все его члены (кроме конструктора и деструктора) должны быть безопасны в многопоточной среде template
class CInterlockedScalar : protected CInterlockedType {
public:
CInterlockedScalar(TYPE TVal = 0) : CInterlockedType(TVal) {
}
CInterlockedScalar() { /* ничего не делает */ }
// C++ не разрешает наследование оператора приведения типа operator TYPE() const {
return(CInterlockedType::operator TYPE());
}
TYPE operator=(TYPE TVal) {
CResGuard::CGuard x(m_rg);
return(SetVal(TVal));
}
TYPE operator++(int) { // постфиксный оператор увеличения на 1
CResGuard::CGuard x(m_rg);
TYPE TPrevVal = GetVal();
см. след. стр.

266
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Рис. 10-2.
продолжение
SetVal((TYPE) (TPrevVal + 1));
return(TPrevVal); // возвращаем значение ДО увеличения operator
(int) { // постфиксный оператор уменьшения на 1
CResGuard::CGuard x(m_rg);
TYPE TPrevVal = GetVal();
SetVal((TYPE) (TPrevVal
1));
return(TPrevVal); // возвращаем значение ДО уменьшения operator += (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() +
op)); }
TYPE operator++()
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() + 1)); }
TYPE operator
= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal()
op)); }
TYPE operator
()
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal()
1)); }
TYPE operator *= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() *
op)); }
TYPE operator /= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() /
op)); }
TYPE operator %= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() %
op)); }
TYPE operator ^= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() ^
op)); }
TYPE operator &= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() &
op)); }
TYPE operator |= (TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() |
op)); }
TYPE operator <<=(TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() << op)); }
TYPE operator >>=(TYPE op)
{ CResGuard::CGuard x(m_rg); return(SetVal(GetVal() >> op)); }
};
///////////////////////////////////////////////////////////////////////////////
// к экземплярам этого класса будет обращаться множество потоков, поэтому все его члены (кроме конструктора и деструктора) должны быть безопасны в многопоточной среде template
class CWhenZero : public CInterlockedScalar {
public:
CWhenZero(TYPE TVal = 0, BOOL fManualReset = TRUE)
: CInterlockedScalar(TVal) {
// это событие должно освобождаться при TVal, равном 0
m_hevtZero = CreateEvent(NULL, fManualReset, (TVal == 0), NULL);

267
Г ЛАВА 10
Полезные средства для синхронизации потоков
Рис. 10-2.
продолжение
// а это событие должно освобождаться при TVal, НЕ равном 0
m_hevtNotZero = CreateEvent(NULL, fManualReset, (TVal != 0), NULL);
}
CWhenZero() {
CloseHandle(m_hevtZero);
CloseHandle(m_hevtNotZero);
}
// C++ не разрешает наследование оператора =
TYPE operator=(TYPE x) {
return(CInterlockedScalar::operator=(x));
}
// возвращаем описатель события, которое переходит в свободное состояние при нулевом значении operator HANDLE() const { return(m_hevtZero); }
// возвращаем описатель события, которое переходит в свободное состояние при ненулевом значении GetNotZeroHandle() const { return(m_hevtNotZero); }
// C++ не разрешает наследование оператора приведения типа operator TYPE() const {
return(CInterlockedScalar::operator TYPE());
}
protected:
void OnValChanged(const TYPE& TNewVal, const TYPE& TPrevVal) const {
// для большего быстродействия избегайте перехода в режим ядра без веских причин if ((TNewVal == 0) && (TPrevVal != 0)) {
SetEvent(m_hevtZero);
ResetEvent(m_hevtNotZero);
}
if ((TNewVal != 0) && (TPrevVal == 0)) {
ResetEvent(m_hevtZero);
SetEvent(m_hevtNotZero);
}
}
private:
HANDLE m_hevtZero;
// освобождается, когда значение равно 0
HANDLE m_hevtNotZero;
// освобождается, когда значение неравно Конец файла //////////////////////////////////
Синхронизация в сценарии
«один писатель/группа читателей»
Во многих приложениях возникает одна и та же проблема синхронизации, о которой часто говорят как о сценарии один писатель/группа читателей (single writer/
multiple readers). В чем ее суть Представьте произвольное число потоков пытается

268
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
получить доступ к некоему разделяемому ресурсу. Каким то потокам («писателям»)
нужно модифицировать данные, а каким то (читателям) — лишь прочесть эти данные. Синхронизация такого процесса необходима хотя бы потому, что Вы должны соблюдать следующие правила:
1.
Когда один поток что то пишет в область общих данных, другие этого делать не могут.
2.
Когда один поток что то пишет в область общих данных, другие не могут ничего считывать оттуда.
3.
Когда один поток считывает что то из области общих данных, другие немо гут туда ничего записывать.
4.
Когда один поток считывает что то из области общих данных, другие тоже могут это делать.
Посмотрим на проблему в контексте базы данных. Допустим, с ней работают пять конечных пользователей двое вводят в нее записи, трое — считывают.
В этом сценарии правило 1 необходимо потому, что мы, конечно жене можем позволить одновременно обновлять одну и туже запись. Иначе информация в записи будет повреждена.
Правило 2 запрещает доступ к записи, обновляемой в данный момент другим пользователем. Будь то иначе, один пользователь считывал бы запись, когда другой пользователь изменял бы ее содержимое. Что увидел бы на мониторе своего компьютера первый пользователь, предсказать не берусь. Правило 3 служит тем же целям, что и правило 2. И действительно, какая разница, кто первый получит доступ к данным:
тот, кто записывает, или тот, кто считывает, — все равно одновременно этого делать нельзя.
И, наконец, последнее правило. Оно введено для большей эффективности работы баз данных. Если никто не модифицирует записи в базе данных, все пользователи могут свободно читать любые записи. Также предполагается, что количество читателей превышает число «писателей».
О’кэй, суть проблемы Вы ухватили. А теперь вопрос как ее решить?
Я представлю здесь совершенно новый код. Решения этой проблемы, которые я публиковал в прежних изданиях, часто критиковались по двум причинам. Во первых, предыдущие реализации работали слишком медленно, так как я писал их в расчете на самые разные сценарии. Например, я шире использовал объекты ядра, стремясь синхронизировать доступ к базе данных потоков из разных процессов. Конечно, эти реализации работали ив сценарии для одного процесса, но интенсивное использование объектов ядра приводило в этом случае к существенным издержкам. Похоже, сценарий для одного процесса более распространен, чем я думал.
Во вторых, в моей реализации был потенциальный риск блокировки потоков писателей. Из правило которых я рассказал вначале этого раздела,
вытекает, что потоки писатели — при обращении к базе данных очень большого количества потоков читателей — могут вообще не получить доступ к этому ресурсу.
Все эти недостатки я теперь устранил. В новой реализации объекты ядра применяются лишь в тех случаях, когда без них не обойтись, и потоки синхронизируются в основном за счет использования критической секции.
1   ...   25   26   27   28   29   30   31   32   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал