В недавние старые добрые времена дисковая операционная система ( DOS ) была однозадачной и выполняла только одно приложение, не считая резидентных программ фонового режима и ядра самой загруженной DOS. Это приложение через сервисы операционной системы или напрямую получало неограниченный доступ к оборудованию и к любым участкам памяти. Приложению, как единственному дитя, позволялось все на его страх и риск. Если от неверных действий приложения компьютер зависал - от этого страдало только само приложение.
С появлением многозадачной операционной системы нестабильная работа одного приложения не должна была влиять на работу соседей, выполняющихся в то же самое время. Поэтому возникла задача обеспечить изоляцию каждого отдельного приложения от всех остальных, а также защитить саму операционную систему от влияния любого из них. Воплощением этой идеи явились процессы, которые представляют собой механизм изоляции работы отдельных приложений.
Процесс - это некий абстрактный контейнер, который окутывает запущенное приложение, предоставляя ему все необходимое для автономной работы, но в то же время ограничивая его полномочия. Код одного процесса не может влиять на данные и код другого процесса, за этим строго следит операционная система. За процессами закрепляются приоритеты на обслуживание процессором и выделяется собственное виртуальное адресное пространство размером 4 Гб для 32-разрядных систем (или 16 Гб для 64-разрядных). Такой размер оперативной памяти, закрепляемой за процессом, физически не может быть предоставлен, но в пределе, за счет хитроумного механизма многостраничной адресации с привлечением дисковой памяти это возможно.
Для обычных приложений операционная система сама назначает права доступа к своим сервисам и полностью блокирует прямой доступ к оборудованию компьютера. Изоляция параллельных процессов не абсолютная, но взаимодействие процессов осуществляется строго под контролем операционной системы посредством механизма IPC ( Inter Process Communication -
Чтобы наглядно убедиться в существовании процессов операционной системы, достаточно вызвать диспетчер задач либо через контекстное меню панели задач, либо нажатием заветной комбинации клавиш Ctrl-Alt-Del. Приложение, которое выполняется в данный момент, учитывается операционной системой как процесс.


Закройте выполняемое приложение, сразу же завершится выделенный для него процесс. Таким образом, процессы - это механизм изоляции и поддержки выполняемых приложений на уровне операционной системы.
По аналогии с процессами в операционной системе, общеязыковая среда исполнения CLR ( ) также реализует подобный механизм, который называется доменами. Вспомним, что CLR является виртуальной машиной (набором программных средств), исполняющей код приложений .NET. По умолчанию приложение помещается в основной домен среды, но далее оно может создавать дополнительные домены и размещать в них отдельные объекты. Это повышает отказоустойчивость приложения в целом, поскольку гибель одного домена никак не повлияет на работу остальных частей приложения, размещенных в других доменах в рамках одного процесса.
Таким образом, домен отвечает за изолирование кода и данных в рамках одного процесса на уровне CLR точно также, как процесс отвечает за изолирование кода и данных на уровне операционной системы.
В удаленных WEB -приложениях не только сама прикладная программа может создавать изолированные домены для выполнения отдельных частей приложения. Среда исполнения, в случае большой загруженности приложения в одном домене или обновлении исполняемого кода в режиме online, может сама запустить еще одну версию приложения в новом домене для работы с новыми запросами, не прекращая работу кода в старом домене с его прежними клиентами. Когда же количество запросов к удаленному WEB -приложению спадет или заканчиваются клиенты домена старой копии, число доменов с копиями приложения может автоматически уменьшиться.
По аналогии с IPC -механизмом организации взаимодействия между процессами на уровне операционной системы, на уровне CLR также организуется взаимодействие между доменами. Технология междоменного взаимодействия называется .NET Remoting. Она реализуется через специальные каналы, так называемые, прозрачные переходники ( Transparent Proxy ), которые следят за правами доступа и безопасностью.
Приведем пример размещения отдельных частей одного приложения в разных доменах. При этом домен, который автоматически создается средой исполнения для запущенного приложения, будем называть основным, а дополнительный домен, созданный кодом нашего приложения для размещения объекта, пользовательским. Пользовательским доменам можно назначать осмысленные имена. Имена основным доменам назначает среда исполнения.
using System;
using System.Runtime.Remoting; // Для меж_доменного взаимодействия
namespace Test
{
// Объект для размещения в пользовательском домене
class SecondClass : MarshalByRefObject
{
// Поле для хранения текста сообщения
private string message;
// Конструктор заполняет поле именем текущего домена
public SecondClass()
{
// Читаем имя текущего домена
message = AppDomain.CurrentDomain.FriendlyName;
}
// Сервис доступа к полю
public String Message
{
get { return message; }
}
}
// Весь последующий код будет размещен в основном домене
class BaseClass
{
int i = 0; // Счетчик сообщений печати
// Конструктор
public BaseClass()
{
// Выводим имя основного домена
Console.WriteLine("{1}) Имя основного домена:
{0}", AppDomain.CurrentDomain.FriendlyName, ++i);
// Создаем именованный пользовательский домен
AppDomain userDomain = AppDomain.
CreateDomain("Пользовательский домен");
// Получим имя текущей сборки и полное имя
// типа "Test.UserDomain"
string assemblyName = System.Reflection.Assembly.
GetAssembly(typeof(BaseClass)).GetName().Name;
string typeName = typeof(SecondClass).FullName;
// Создаем удаленный объект типа "Test.UserDomain",
// находящийся в текущей сборке
ObjectHandle objHandle = userDomain.CreateInstance
(assemblyName, typeName);
// Получаем ссылку для работы с удаленным объектом
SecondClass obj = (SecondClass)objHandle.Unwrap();
// Проверяем, действительно ли установлен
// канал связи Transparent Proxy
if (RemotingServices.IsTransparentProxy(obj))
{
Console.WriteLine("{0}) Канал связи с
удаленным объектом установлен!", ++i);
Console.WriteLine("{1}) Имя пользовательского
домена:\n\t'{0}'", obj.Message, ++i);
}
else
Console.WriteLine("{0}) Нет связи с
другим доменом", ++i);
}
}
// Запуск - тоже в основном домене
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Механизм взаимодействия
доменов приложения";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 48;
Console.WindowHeight = 5;
new BaseClass();// Исполняем
Console.ReadLine();
}
}
}
Рассмотрим пример, в котором родительское приложение из основного домена запускает исполняемые сборки в других отдельных доменах. Пример оформим в виде упражнения с пошаговыми инструкциями.
Командой File/New/Project создайте новый проект с именем ConsoleApp в одноименном решении
Командой File/Add/New Project добавьте к созданному решению еще 3 новых проекта с именами
После таких действий структура решения в панели Solution Explorer будет выглядеть так

Откройте файлы Program.cs для дочерних приложенийи поместите в функцию Main() каждого из них одинаковый код, выводящий на консоль имя текущего домена
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.Title = "Загрузка исполняемых файлов в домены";
Console.WriteLine("Приложение из домена {0}; ID={1}",
AppDomain.CurrentDomain.FriendlyName,
AppDomain.CurrentDomain.Id);
}
}
}
Выполните команду Build/Build Solution, чтобы откомпилировать дочерние сборки
В панели Solution Explorer через контекстное меню для проекта ConsoleApp добавьте к нему исполнимые файлы из дочерних проектов командой Add/Existing Item
Установите для каждого из этих файлов свойство Copy to Output Directory в значение Copy always (копировать всегда), чтобы оболочка поместила их в один каталог с родительской исполнимой сборкой ConsoleApp.exe
Откройте файл Program.cs проекта ConsoleApp и заполните его следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
// Настройка консольного окна вывода
Console.Title = "Загрузка исполняемых
файлов в домены";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 42;
Console.WindowHeight = 5;
Console.WriteLine("Приложение из домена {0};
ID={1}",
AppDomain.CurrentDomain.FriendlyName,
AppDomain.CurrentDomain.Id);
for (int i = 1; i <= 3; i++)
{
string name = "ConsoleApp" + i.ToString();
// Создаем именованный дочерний домен
AppDomain secondDomain = AppDomain.CreateDomain(name);
// Запускаем в него приложение
secondDomain.ExecuteAssembly(name + ".exe");
}
Console.ReadLine();
}
}
}
В панели Solution Explorer щелкните правой кнопкой мыши на узле родительского проекта ConsoleApp, командой Properties вызовите окно свойств и настройте флажки вкладки Debug в соответствии с рисунком
В панели Solution Explorer щелкните правой кнопкой мыши на узле родительского проекта ConsoleApp и сделайте его стартовым командой Set as StartUp Project
Запустите приложение, в результате чего получится такой результат
Обратите внимание, что имя основного домена совпадает с именем родительской сборки, а имена остальным доменам мы задаем произвольно при создании домена. Свойство FriendlyName содержит удобное имя домена для нас, программистов, а среда исполнения CRL оперирует с идентификатором ID.
Процесс, и домены внутри процесса, создают для исполняемого приложения комфортную среду обитания. Они изолируют код и данные от других исполняемых программ и предоставляют им необходимые сервисы. Но Windows является многозадачной операционной системой и должна выполнять все загруженные в нее задачи. Поэтому, имея в наличии только один процессор, она вынуждена эмулировать многозадачность выделением каждой задаче небольшого кванта времени, создавая иллюзию их одновременного выполнения. Такое поведение вполне приемлемо, потому что многие процессы представляют диалоговые приложения и в большинстве случаев находятся в состоянии простоя, ожидая ( idle ) действий пользователя.
Не все части процесса, ожидающего действий пользователя, могут простаивать одновременно. Некоторые части исполняемого приложения должны продолжать взаимодействовать с пользователем, ожидая его указаний, а другие в это время - исполнять уже полученные указания. Для реализации такого подхода код процесса разбивают на несколько завершенных частей, способных выполняться самостоятельно, и эти части называются потоками ( thread ). Таким образом, поток, это часть кода процесса, который способен реально выполняться процессором в данный момент.
Поток есть ничто иное, как контейнер для контролируемого исполнения какой-то функции. Когда функции выполняются только в одном потоке, работой каждой из них можно управлять только изнутри. Другое дело, когда функция выполняется в отдельном потоке-контейнере. В этом случае работой функции мы можем управлять из создавшего поток внешнего кода. Например, мы можем приостановить выполнение функции, назначить ей приоритет по отношению к функциям других потоков, перевести в
Процесс по отношению к потокам является контейнером, предоставляет им закрытое адресное пространство и следит за соблюдением политики операционной системы. Но реальный код приложения исполняют потоки. Все они имеют доступ к общей памяти приложения, выделенной им процессом. Но каждый из потоков имеет еще и свою Thread ), к которой имеет доступ только он сам.
Различают управляемые и неуправляемые потоки. Управляемые потоки создаются и действуют под контролем среды CRL, а неуправляемые - под контролем самой операционной системы. Любой поток, управляемый или неуправляемый, представлен экземпляром класса System.Threading.Thread.Оперируя сервисами этого класса мы можем контролировать работу потоков. Ниже приведены некоторые из этих сервисов.
| Член | Пояснения |
|---|---|
Thread() |
Конструктор с перегрузками для создания потока |
Abort() |
Уничтожение потока |
|
Приостановление работы потока на неопределенное время |
Sleep() |
Приостановление работы потока на заданное время |
Join() |
Ожидание завершения работы потока |
CurrentThread |
Свойство для извлечения текущего работающего потока |
CurrentThread.Name |
Имя текущего потока |
Priority |
Свойство для получения или изменения |
Name |
Свойство для получения или изменения имени потока |
IsBackground |
Булево свойство для подтверждения работы потока в фоновом режиме |
ApartmentState |
Свойство, позволяющее получить или изменить тип COM -апартамента для данного потока |
CurrentContext |
Статическое свойство чтения текущего контекста взаимодействия для данного потока |
CurrentCulture |
Свойство для изменения региональных установок текущего потока |
CurrentPrincipal |
Статическое свойство, управляющее защитой текущего потока на основе модели ролей |
CurrentUICulture |
Свойство, представляющее собой идентификатор для |
IsAlive |
Булево свойство только для чтения, позволяющее определить, исполняется ли в данный момент поток |
IsThreadPoolThread |
Булево свойство только для чтения, позволяющее определить, поступил ли данный поток из пула (накопительного буфера) потоков |
ThreadState |
Свойство только для чтения, позволяющее определить состояние текущего потока |
AllocateDataSlot() |
Статический метод, который резервирует неименованный слот в общей памяти всех потоков |
AllocateNamedDataSlot() |
Статический метод, который резервирует именованный слот (участок памяти) в общей памяти всех потоков |
FreeNamedDataSlot() |
Статический метод, который освобождает именованный слот в общей памяти всех потоков |
GetDomain() |
Статический метод, возвращающий текущий домен, в котором исполняется данный поток |
GetDomainID() |
Статический метод, возвращающий целое число, которое представляет собой идентификатор домена с исполняемым текущим потоком |
SetData() |
Статический метод, записывающий данные в указанный слот персональной памяти TLS текущего исполняемого потока |
GetNamedDataSlot() |
Статический метод, возвращающий данные именованного слота для текущего потока из его персональной памяти TLS |
Interrupt() |
Прерывает исполнение потока |
ResetAbort() |
Отменяет запрос на уничтожение потока, инициированный методом Abort(), после чего поток начинает выполняться с самого начала |
|
Возобновляет работу ранее приостановленного потока |
Start() |
Запускает поток на исполнение |
Для любого приложения, запущенного на исполнение и загруженного в процесс, автоматически создается хотя бы один поток, который называется основным. Далее сама программа может создавать для отдельных своих частей дополнительные потоки и управлять ими. Этими частями являются методы, содержащие исполнимый код. Рассмотрим создание дополнительного потока и помещения в него одной функции на примере.
using System;
using System.Threading;
namespace Test
{
class MyClass
{
// Закрытые поля с маркерами потоков
int markerBaseThread, markerNewThread;
// Открытые поля с идентификаторами доменов
public int domainBaseThread, domainNewThread;
// Свойства доступа закрытых полей (только для чтения)
public int MarkerBaseThread
{
get { return markerBaseThread; }
}
public int MarkerNewThread
{
get { return markerNewThread; }
}
// Конструктор исполняется в основном потоке
public MyClass()
{
// Извлечем маркер текущего потока
markerBaseThread = Thread.CurrentThread.GetHashCode();
// Извлекаем идентификатор домена
domainBaseThread = Thread.GetDomainID();
// Создаем специальный делегат со ссылкой на функцию
// Создаем новый поток и присоединяем к нему исполняемый код
// Запускаем код функции в новом потоке из текущего потока
ThreadStart del = new ThreadStart(Func); // Объект-делегат
Thread th = new Thread(del); // Новый поток
th.Start(); // Выполнить функцию
}
// Метод будет исполняться в дополнительном потоке
private void Func()
{
// Извлекаем маркер текущего потока
markerNewThread = Thread.CurrentThread.GetHashCode();
// Извлекаем идентификатор домена
domainNewThread = Thread.GetDomainID();
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Выполнение кода в
дополнительном потоке";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 57;
Console.WindowHeight = 4;
MyClass obj = new MyClass();// Исполняем
// Показываем маркеры
Console.WriteLine("Поток выполнения
конструктора MyClass(): {0} (в домене {1})",
obj.MarkerBaseThread, obj.domainBaseThread);
Console.WriteLine("Поток выполнения метода
Func(): {0} (в домене {1})",
obj.MarkerNewThread, obj.domainNewThread);
Console.WriteLine("Поток выполнения метода
Main(): {0} (в домене {1})",
Thread.CurrentThread.GetHashCode(), Thread.GetDomainID());
Console.ReadLine(); // Ждем нажатия клавиши Enter
}
}
}

Результат показывает, что в одном домене выполняются два разных потока.
В отдельный поток можно помещать несколько функций, указав их во внутреннем списке объекта-делегата ThreadStart. При обычном выполнении приложения происходит то же самое - все функции выполняются в одном потоке. А здесь мы часть функций сами размещаем в отдельном потоке, используя делегат стандартным способом. Обратите внимание, что делегат может адресовать только функции с пустой сигнатурой и без возвращаемого значения, поскольку объявлен в библиотеке так
public delegate void ThreadStart();
Приведем пример
using System;
using System.Threading;
namespace Test
{
class MyClass
{
// Открытые поля с маркерами потоков для упрощения кода
public int mBase, mFunc1, mFunc2;
// Конструктор исполняется в основном потоке
public MyClass()
{
// Извлекаем маркер текущего потока
mBase = Thread.CurrentThread.GetHashCode();
// Создаем делегат со ссылками на две функции
ThreadStart del = new ThreadStart(Func1);
del += Func2;
Thread th = new Thread(del); // Создаем поток
th.Start(); // Выполнить функции
}
// Метод будет исполняться в дополнительном потоке
private void Func1()
{
// Извлекаем маркер текущего потока
mFunc1 = Thread.CurrentThread.GetHashCode();
}
// Этот метод также будет исполняться в дополнительном потоке
private void Func2()
{
// Извлекаем маркер текущего потока
mFunc2 = Thread.CurrentThread.GetHashCode();
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Помещение в поток
нескольких функции";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 39;
Console.WindowHeight = 5;
MyClass obj = new MyClass();// Исполняем
// Показываем маркеры
Console.WriteLine("Поток выполнения
метода MyClass(): " + obj.mBase);
Console.WriteLine("Поток выполнения
метода Func1(): " + obj.mFunc1);
Console.WriteLine("Поток выполнения
метода Func2(): " + obj.mFunc2);
Console.WriteLine("Поток выполнения
метода Main(): " + Thread.CurrentThread.GetHashCode());
Console.ReadLine(); // Ждем нажатия клавиши Enter
}
}
}
Для уничтожения потока применяется метод Abort(), который может использоваться как без параметра, так и с параметром типа Object. При использовании метода Abort() с параметром в параметре можно разместить любую информацию. Метод Abort() инициирует выброс исключения типа ThreadAbortException, в экземпляр которого будет помещена эта информация. Это исключение надо обрабатывать, иначе будет выброшено необработанное системное исключение. При вызове метода Abort() свойство состояния потока System.Threading.Thread.ThreadState устанавливается в значение одноименного перечисления System.Threading.ThreadState.AbortRequested.
При попытке уничтожения потока его работа немедленно прерывается на выполнении текущей команды. Но если поток в это время выполнял команды неуправляемого кода, например, обращался к некоторой системной функции или внешнему COM -объекту, то среда CRL дождется возвращения потока в управляемую область и сразу прервет его. Возобновить работу потока можно с помощью статического метода Thread. ResetAbort(), но в этом случае поток начнет выполняться не с прерванного состояния, а будет запущен заново.
Вот код приложения, которое прерывает само себя, уничтожая основной поток, в который оно автоматически загружается.
using System;
using System.Threading;
namespace Test
{
class MyClass
{
// Конструктор исполняется в основном потоке
public MyClass()
{
// Основной поток существует, попытаемся его прервать
// через защищенный блок кода, чтобы отловить исключение
try
{
Thread.CurrentThread.Abort();
}
catch (ThreadAbortException e)
{
Console.WriteLine("Выброшено
исключение ThreadAbortException\n" +
"Сообщение: {0}", e.Message);
Console.WriteLine("\nНажмите Enter,
чтобы завершить основной поток...");
Console.ReadLine();
}
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Уничтожение основного потока";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 49;
Console.WindowHeight = 5;
new MyClass();// Исполняем и прерываем
// Исполнение до сих пор не дойдет, прервется в конструкторе
Console.ReadLine();
}
}
}
В следующем примере мы создадим в клиентском коде основного потока дополнительный поток для одной локальной функции, запустим его на выполнение и извлечем состояние через свойство Thread.ThreadState. Затем попытаемся уничтожить этот поток из того же клиентского кода. Заключим код выполнения функции в защищенный блок, чтобы перехватить исключение ThreadAbortException. В блоке фильтра отловленного исключения восстановим поток методом Thread.ResetAbort() и опять извлечем его состояние через свойство Thread.ThreadState. Для уничтожения потока применим параметризованную Thread.Abort().
using System;
using System.Threading;
namespace Test
{
class MyClass
{
int i = 0; // Счетчик сообщений в общей памяти потоков
// Клиентский код для управления дополнительным потоком
public MyClass()
{
// Создаем дополнительный поток для функции Func()
ThreadStart del = new ThreadStart(Func);
Thread th = new Thread(del);
th.Start(); // Запускаем на выполнение
// Приостановим основной поток для устранения состязаний
Thread.Sleep(1000);
// Обследуем запущенный дополнительный поток
Console.WriteLine("{1}) Запустили Func()
в потоке {0}",
th.GetHashCode(), ++i);
Console.WriteLine("{1}) Состояние
работающего потока: {0}",
th.ThreadState.ToString(), ++i);
// Пытаемся прервать самовосстанавливающийся поток
th.Abort("Основной поток хочет уничтожить!");
// Приостановим основной поток для устранения состязаний
Thread.Sleep(1000);
// Восстановленная функция закончила работу
Console.WriteLine("{1}) Состояние
завершенного потока: {0}",
th.ThreadState.ToString(), ++i);
}
// Локальная функция, размещаемая в отдельном потоке
void Func()
{
try
{
// Эмулируем бурную деятельность, пока не прервут
for (; ; ) ;
}
catch (ThreadAbortException e)
{
// Обследуем поток
Console.WriteLine("{1}) Выброшено
исключение {2}\n\t" +
"Сообщение: {0}", e.Message,
++i, e.GetType().Name);
Console.WriteLine("{1}) Состояние
потока при " +
"попытке уничтожения:\n\t{0}",
Thread.CurrentThread.ThreadState.ToString(), ++i);
Console.WriteLine("{1}) Сообщение,
переданное " +
"через параметр Abort():\n\t{0}",
e.ExceptionState.ToString(), ++i);
// Самовосстанавливаемся
Thread.ResetAbort();
// Обследуем поток
Console.WriteLine("{1}) Состояние
потока после " +
"восстановления:\n\t{0}",
Thread.CurrentThread.ThreadState.ToString(), ++i);
Console.WriteLine("{1}) Func() выполняется
в потоке {0}",
Thread.CurrentThread.GetHashCode(), ++i);
}
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Попытка уничтожения " +
"самовосстанавливающегося потока";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 51;
Console.WindowHeight = 13;
new MyClass();// Исполняем
// Ждем нажатия клавиши Enter
Console.ReadLine();
}
}
}

В коде примера мы применили статический метод Thread.Sleep(int) для приостановки основного потока, чтобы дать возможность начать выполняться дополнительному потоку. Это нужно для того, чтобы до дополнительного потока дошла очередь и он получил свой квант процессорного времени.
Мы уже частично использовали свойство, только для чтения, Thread.ThreadState в предыдущем примере, чтобы узнать, в каком состоянии находится поток. Мы его применяли и к экземпляру th потока во внешнем коде как th.ThreadState.ToString(), и внутри потока как Thread.CurrentThread.ThreadState.ToString(). Значения этого свойства, устанавливаемые одноименным перечислением System.Threading.ThreadState, позволяют контролировать проводимые с потоком манипуляции и определять, в каком состоянии он в данный момент находится, прежде чем применять к нему какие-то действия.
В недавние старые добрые времена дисковая операционная система ( DOS ) была однозадачной и выполняла только одно приложение, не считая резидентных программ фонового режима и ядра самой загруженной DOS. Это приложение через сервисы операционной системы или напрямую получало неограниченный доступ к оборудованию и к любым участкам памяти. Приложению, как единственному дитя, позволялось все на его страх и риск. Если от неверных действий приложения компьютер зависал - от этого страдало только само приложение.
С появлением многозадачной операционной системы нестабильная работа одного приложения не должна была влиять на работу соседей, выполняющихся в то же самое время. Поэтому возникла задача обеспечить изоляцию каждого отдельного приложения от всех остальных, а также защитить саму операционную систему от влияния любого из них. Воплощением этой идеи явились процессы, которые представляют собой механизм изоляции работы отдельных приложений.
Процесс - это некий абстрактный контейнер, который окутывает запущенное приложение, предоставляя ему все необходимое для автономной работы, но в то же время ограничивая его полномочия. Код одного процесса не может влиять на данные и код другого процесса, за этим строго следит операционная система. За процессами закрепляются приоритеты на обслуживание процессором и выделяется собственное виртуальное адресное пространство размером 4 Гб для 32-разрядных систем (или 16 Гб для 64-разрядных). Такой размер оперативной памяти, закрепляемой за процессом, физически не может быть предоставлен, но в пределе, за счет хитроумного механизма многостраничной адресации с привлечением дисковой памяти это возможно.
Для обычных приложений операционная система сама назначает права доступа к своим сервисам и полностью блокирует прямой доступ к оборудованию компьютера. Изоляция параллельных процессов не абсолютная, но взаимодействие процессов осуществляется строго под контролем операционной системы посредством механизма IPC ( Inter Process Communication -
Чтобы наглядно убедиться в существовании процессов операционной системы, достаточно вызвать диспетчер задач либо через контекстное меню панели задач, либо нажатием заветной комбинации клавиш Ctrl-Alt-Del. Приложение, которое выполняется в данный момент, учитывается операционной системой как процесс.


Закройте выполняемое приложение, сразу же завершится выделенный для него процесс. Таким образом, процессы - это механизм изоляции и поддержки выполняемых приложений на уровне операционной системы.
По аналогии с процессами в операционной системе, общеязыковая среда исполнения CLR ( ) также реализует подобный механизм, который называется доменами. Вспомним, что CLR является виртуальной машиной (набором программных средств), исполняющей код приложений .NET. По умолчанию приложение помещается в основной домен среды, но далее оно может создавать дополнительные домены и размещать в них отдельные объекты. Это повышает отказоустойчивость приложения в целом, поскольку гибель одного домена никак не повлияет на работу остальных частей приложения, размещенных в других доменах в рамках одного процесса.
Таким образом, домен отвечает за изолирование кода и данных в рамках одного процесса на уровне CLR точно также, как процесс отвечает за изолирование кода и данных на уровне операционной системы.
В удаленных WEB -приложениях не только сама прикладная программа может создавать изолированные домены для выполнения отдельных частей приложения. Среда исполнения, в случае большой загруженности приложения в одном домене или обновлении исполняемого кода в режиме online, может сама запустить еще одну версию приложения в новом домене для работы с новыми запросами, не прекращая работу кода в старом домене с его прежними клиентами. Когда же количество запросов к удаленному WEB -приложению спадет или заканчиваются клиенты домена старой копии, число доменов с копиями приложения может автоматически уменьшиться.
По аналогии с IPC -механизмом организации взаимодействия между процессами на уровне операционной системы, на уровне CLR также организуется взаимодействие между доменами. Технология междоменного взаимодействия называется .NET Remoting. Она реализуется через специальные каналы, так называемые, прозрачные переходники ( Transparent Proxy ), которые следят за правами доступа и безопасностью.
Приведем пример размещения отдельных частей одного приложения в разных доменах. При этом домен, который автоматически создается средой исполнения для запущенного приложения, будем называть основным, а дополнительный домен, созданный кодом нашего приложения для размещения объекта, пользовательским. Пользовательским доменам можно назначать осмысленные имена. Имена основным доменам назначает среда исполнения.
using System;
using System.Runtime.Remoting; // Для меж_доменного взаимодействия
namespace Test
{
// Объект для размещения в пользовательском домене
class SecondClass : MarshalByRefObject
{
// Поле для хранения текста сообщения
private string message;
// Конструктор заполняет поле именем текущего домена
public SecondClass()
{
// Читаем имя текущего домена
message = AppDomain.CurrentDomain.FriendlyName;
}
// Сервис доступа к полю
public String Message
{
get { return message; }
}
}
// Весь последующий код будет размещен в основном домене
class BaseClass
{
int i = 0; // Счетчик сообщений печати
// Конструктор
public BaseClass()
{
// Выводим имя основного домена
Console.WriteLine("{1}) Имя основного домена:
{0}", AppDomain.CurrentDomain.FriendlyName, ++i);
// Создаем именованный пользовательский домен
AppDomain userDomain = AppDomain.
CreateDomain("Пользовательский домен");
// Получим имя текущей сборки и полное имя
// типа "Test.UserDomain"
string assemblyName = System.Reflection.Assembly.
GetAssembly(typeof(BaseClass)).GetName().Name;
string typeName = typeof(SecondClass).FullName;
// Создаем удаленный объект типа "Test.UserDomain",
// находящийся в текущей сборке
ObjectHandle objHandle = userDomain.CreateInstance
(assemblyName, typeName);
// Получаем ссылку для работы с удаленным объектом
SecondClass obj = (SecondClass)objHandle.Unwrap();
// Проверяем, действительно ли установлен
// канал связи Transparent Proxy
if (RemotingServices.IsTransparentProxy(obj))
{
Console.WriteLine("{0}) Канал связи с
удаленным объектом установлен!", ++i);
Console.WriteLine("{1}) Имя пользовательского
домена:\n\t'{0}'", obj.Message, ++i);
}
else
Console.WriteLine("{0}) Нет связи с
другим доменом", ++i);
}
}
// Запуск - тоже в основном домене
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Механизм взаимодействия
доменов приложения";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 48;
Console.WindowHeight = 5;
new BaseClass();// Исполняем
Console.ReadLine();
}
}
}

Рассмотрим пример, в котором родительское приложение из основного домена запускает исполняемые сборки в других отдельных доменах. Пример оформим в виде упражнения с пошаговыми инструкциями.
Командой File/New/Project создайте новый проект с именем ConsoleApp в одноименном решении
Командой File/Add/New Project добавьте к созданному решению еще 3 новых проекта с именами
После таких действий структура решения в панели Solution Explorer будет выглядеть так

Откройте файлы Program.cs для дочерних приложенийи поместите в функцию Main() каждого из них одинаковый код, выводящий на консоль имя текущего домена
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.Title = "Загрузка исполняемых файлов в домены";
Console.WriteLine("Приложение из домена {0}; ID={1}",
AppDomain.CurrentDomain.FriendlyName,
AppDomain.CurrentDomain.Id);
}
}
}
Выполните команду Build/Build Solution, чтобы откомпилировать дочерние сборки
В панели Solution Explorer через контекстное меню для проекта ConsoleApp добавьте к нему исполнимые файлы из дочерних проектов командой Add/Existing Item
Установите для каждого из этих файлов свойство Copy to Output Directory в значение Copy always (копировать всегда), чтобы оболочка поместила их в один каталог с родительской исполнимой сборкой ConsoleApp.exe
Откройте файл Program.cs проекта ConsoleApp и заполните его следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
// Настройка консольного окна вывода
Console.Title = "Загрузка исполняемых
файлов в домены";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 42;
Console.WindowHeight = 5;
Console.WriteLine("Приложение из домена {0};
ID={1}",
AppDomain.CurrentDomain.FriendlyName,
AppDomain.CurrentDomain.Id);
for (int i = 1; i <= 3; i++)
{
string name = "ConsoleApp" + i.ToString();
// Создаем именованный дочерний домен
AppDomain secondDomain = AppDomain.CreateDomain(name);
// Запускаем в него приложение
secondDomain.ExecuteAssembly(name + ".exe");
}
Console.ReadLine();
}
}
}
В панели Solution Explorer щелкните правой кнопкой мыши на узле родительского проекта ConsoleApp, командой Properties вызовите окно свойств и настройте флажки вкладки Debug в соответствии с рисунком
В панели Solution Explorer щелкните правой кнопкой мыши на узле родительского проекта ConsoleApp и сделайте его стартовым командой Set as StartUp Project
Запустите приложение, в результате чего получится такой результат
Обратите внимание, что имя основного домена совпадает с именем родительской сборки, а имена остальным доменам мы задаем произвольно при создании домена. Свойство FriendlyName содержит удобное имя домена для нас, программистов, а среда исполнения CRL оперирует с идентификатором ID.
Процесс, и домены внутри процесса, создают для исполняемого приложения комфортную среду обитания. Они изолируют код и данные от других исполняемых программ и предоставляют им необходимые сервисы. Но Windows является многозадачной операционной системой и должна выполнять все загруженные в нее задачи. Поэтому, имея в наличии только один процессор, она вынуждена эмулировать многозадачность выделением каждой задаче небольшого кванта времени, создавая иллюзию их одновременного выполнения. Такое поведение вполне приемлемо, потому что многие процессы представляют диалоговые приложения и в большинстве случаев находятся в состоянии простоя, ожидая ( idle ) действий пользователя.
Не все части процесса, ожидающего действий пользователя, могут простаивать одновременно. Некоторые части исполняемого приложения должны продолжать взаимодействовать с пользователем, ожидая его указаний, а другие в это время - исполнять уже полученные указания. Для реализации такого подхода код процесса разбивают на несколько завершенных частей, способных выполняться самостоятельно, и эти части называются потоками ( thread ). Таким образом, поток, это часть кода процесса, который способен реально выполняться процессором в данный момент.
Поток есть ничто иное, как контейнер для контролируемого исполнения какой-то функции. Когда функции выполняются только в одном потоке, работой каждой из них можно управлять только изнутри. Другое дело, когда функция выполняется в отдельном потоке-контейнере. В этом случае работой функции мы можем управлять из создавшего поток внешнего кода. Например, мы можем приостановить выполнение функции, назначить ей приоритет по отношению к функциям других потоков, перевести в
Процесс по отношению к потокам является контейнером, предоставляет им закрытое адресное пространство и следит за соблюдением политики операционной системы. Но реальный код приложения исполняют потоки. Все они имеют доступ к общей памяти приложения, выделенной им процессом. Но каждый из потоков имеет еще и свою Thread ), к которой имеет доступ только он сам.
Различают управляемые и неуправляемые потоки. Управляемые потоки создаются и действуют под контролем среды CRL, а неуправляемые - под контролем самой операционной системы. Любой поток, управляемый или неуправляемый, представлен экземпляром класса System.Threading.Thread.Оперируя сервисами этого класса мы можем контролировать работу потоков. Ниже приведены некоторые из этих сервисов.
| Член | Пояснения |
|---|---|
Thread() |
Конструктор с перегрузками для создания потока |
Abort() |
Уничтожение потока |
|
Приостановление работы потока на неопределенное время |
Sleep() |
Приостановление работы потока на заданное время |
Join() |
Ожидание завершения работы потока |
CurrentThread |
Свойство для извлечения текущего работающего потока |
CurrentThread.Name |
Имя текущего потока |
Priority |
Свойство для получения или изменения |
Name |
Свойство для получения или изменения имени потока |
IsBackground |
Булево свойство для подтверждения работы потока в фоновом режиме |
ApartmentState |
Свойство, позволяющее получить или изменить тип COM -апартамента для данного потока |
CurrentContext |
Статическое свойство чтения текущего контекста взаимодействия для данного потока |
CurrentCulture |
Свойство для изменения региональных установок текущего потока |
CurrentPrincipal |
Статическое свойство, управляющее защитой текущего потока на основе модели ролей |
CurrentUICulture |
Свойство, представляющее собой идентификатор для |
IsAlive |
Булево свойство только для чтения, позволяющее определить, исполняется ли в данный момент поток |
IsThreadPoolThread |
Булево свойство только для чтения, позволяющее определить, поступил ли данный поток из пула (накопительного буфера) потоков |
ThreadState |
Свойство только для чтения, позволяющее определить состояние текущего потока |
AllocateDataSlot() |
Статический метод, который резервирует неименованный слот в общей памяти всех потоков |
AllocateNamedDataSlot() |
Статический метод, который резервирует именованный слот (участок памяти) в общей памяти всех потоков |
FreeNamedDataSlot() |
Статический метод, который освобождает именованный слот в общей памяти всех потоков |
GetDomain() |
Статический метод, возвращающий текущий домен, в котором исполняется данный поток |
GetDomainID() |
Статический метод, возвращающий целое число, которое представляет собой идентификатор домена с исполняемым текущим потоком |
SetData() |
Статический метод, записывающий данные в указанный слот персональной памяти TLS текущего исполняемого потока |
GetNamedDataSlot() |
Статический метод, возвращающий данные именованного слота для текущего потока из его персональной памяти TLS |
Interrupt() |
Прерывает исполнение потока |
ResetAbort() |
Отменяет запрос на уничтожение потока, инициированный методом Abort(), после чего поток начинает выполняться с самого начала |
|
Возобновляет работу ранее приостановленного потока |
Start() |
Запускает поток на исполнение |
Для любого приложения, запущенного на исполнение и загруженного в процесс, автоматически создается хотя бы один поток, который называется основным. Далее сама программа может создавать для отдельных своих частей дополнительные потоки и управлять ими. Этими частями являются методы, содержащие исполнимый код. Рассмотрим создание дополнительного потока и помещения в него одной функции на примере.
using System;
using System.Threading;
namespace Test
{
class MyClass
{
// Закрытые поля с маркерами потоков
int markerBaseThread, markerNewThread;
// Открытые поля с идентификаторами доменов
public int domainBaseThread, domainNewThread;
// Свойства доступа закрытых полей (только для чтения)
public int MarkerBaseThread
{
get { return markerBaseThread; }
}
public int MarkerNewThread
{
get { return markerNewThread; }
}
// Конструктор исполняется в основном потоке
public MyClass()
{
// Извлечем маркер текущего потока
markerBaseThread = Thread.CurrentThread.GetHashCode();
// Извлекаем идентификатор домена
domainBaseThread = Thread.GetDomainID();
// Создаем специальный делегат со ссылкой на функцию
// Создаем новый поток и присоединяем к нему исполняемый код
// Запускаем код функции в новом потоке из текущего потока
ThreadStart del = new ThreadStart(Func); // Объект-делегат
Thread th = new Thread(del); // Новый поток
th.Start(); // Выполнить функцию
}
// Метод будет исполняться в дополнительном потоке
private void Func()
{
// Извлекаем маркер текущего потока
markerNewThread = Thread.CurrentThread.GetHashCode();
// Извлекаем идентификатор домена
domainNewThread = Thread.GetDomainID();
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Выполнение кода в
дополнительном потоке";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 57;
Console.WindowHeight = 4;
MyClass obj = new MyClass();// Исполняем
// Показываем маркеры
Console.WriteLine("Поток выполнения
конструктора MyClass(): {0} (в домене {1})",
obj.MarkerBaseThread, obj.domainBaseThread);
Console.WriteLine("Поток выполнения метода
Func(): {0} (в домене {1})",
obj.MarkerNewThread, obj.domainNewThread);
Console.WriteLine("Поток выполнения метода
Main(): {0} (в домене {1})",
Thread.CurrentThread.GetHashCode(), Thread.GetDomainID());
Console.ReadLine(); // Ждем нажатия клавиши Enter
}
}
}

Результат показывает, что в одном домене выполняются два разных потока.
В отдельный поток можно помещать несколько функций, указав их во внутреннем списке объекта-делегата ThreadStart. При обычном выполнении приложения происходит то же самое - все функции выполняются в одном потоке. А здесь мы часть функций сами размещаем в отдельном потоке, используя делегат стандартным способом. Обратите внимание, что делегат может адресовать только функции с пустой сигнатурой и без возвращаемого значения, поскольку объявлен в библиотеке так
public delegate void ThreadStart();
Приведем пример
using System;
using System.Threading;
namespace Test
{
class MyClass
{
// Открытые поля с маркерами потоков для упрощения кода
public int mBase, mFunc1, mFunc2;
// Конструктор исполняется в основном потоке
public MyClass()
{
// Извлекаем маркер текущего потока
mBase = Thread.CurrentThread.GetHashCode();
// Создаем делегат со ссылками на две функции
ThreadStart del = new ThreadStart(Func1);
del += Func2;
Thread th = new Thread(del); // Создаем поток
th.Start(); // Выполнить функции
}
// Метод будет исполняться в дополнительном потоке
private void Func1()
{
// Извлекаем маркер текущего потока
mFunc1 = Thread.CurrentThread.GetHashCode();
}
// Этот метод также будет исполняться в дополнительном потоке
private void Func2()
{
// Извлекаем маркер текущего потока
mFunc2 = Thread.CurrentThread.GetHashCode();
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Помещение в поток
нескольких функции";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 39;
Console.WindowHeight = 5;
MyClass obj = new MyClass();// Исполняем
// Показываем маркеры
Console.WriteLine("Поток выполнения
метода MyClass(): " + obj.mBase);
Console.WriteLine("Поток выполнения
метода Func1(): " + obj.mFunc1);
Console.WriteLine("Поток выполнения
метода Func2(): " + obj.mFunc2);
Console.WriteLine("Поток выполнения
метода Main(): " + Thread.CurrentThread.GetHashCode());
Console.ReadLine(); // Ждем нажатия клавиши Enter
}
}
}
Для уничтожения потока применяется метод Abort(), который может использоваться как без параметра, так и с параметром типа Object. При использовании метода Abort() с параметром в параметре можно разместить любую информацию. Метод Abort() инициирует выброс исключения типа ThreadAbortException, в экземпляр которого будет помещена эта информация. Это исключение надо обрабатывать, иначе будет выброшено необработанное системное исключение. При вызове метода Abort() свойство состояния потока System.Threading.Thread.ThreadState устанавливается в значение одноименного перечисления System.Threading.ThreadState.AbortRequested.
При попытке уничтожения потока его работа немедленно прерывается на выполнении текущей команды. Но если поток в это время выполнял команды неуправляемого кода, например, обращался к некоторой системной функции или внешнему COM -объекту, то среда CRL дождется возвращения потока в управляемую область и сразу прервет его. Возобновить работу потока можно с помощью статического метода Thread. ResetAbort(), но в этом случае поток начнет выполняться не с прерванного состояния, а будет запущен заново.
Вот код приложения, которое прерывает само себя, уничтожая основной поток, в который оно автоматически загружается.
using System;
using System.Threading;
namespace Test
{
class MyClass
{
// Конструктор исполняется в основном потоке
public MyClass()
{
// Основной поток существует, попытаемся его прервать
// через защищенный блок кода, чтобы отловить исключение
try
{
Thread.CurrentThread.Abort();
}
catch (ThreadAbortException e)
{
Console.WriteLine("Выброшено
исключение ThreadAbortException\n" +
"Сообщение: {0}", e.Message);
Console.WriteLine("\nНажмите Enter,
чтобы завершить основной поток...");
Console.ReadLine();
}
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Уничтожение основного потока";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 49;
Console.WindowHeight = 5;
new MyClass();// Исполняем и прерываем
// Исполнение до сих пор не дойдет, прервется в конструкторе
Console.ReadLine();
}
}
}

В следующем примере мы создадим в клиентском коде основного потока дополнительный поток для одной локальной функции, запустим его на выполнение и извлечем состояние через свойство Thread.ThreadState. Затем попытаемся уничтожить этот поток из того же клиентского кода. Заключим код выполнения функции в защищенный блок, чтобы перехватить исключение ThreadAbortException. В блоке фильтра отловленного исключения восстановим поток методом Thread.ResetAbort() и опять извлечем его состояние через свойство Thread.ThreadState. Для уничтожения потока применим параметризованную Thread.Abort().
using System;
using System.Threading;
namespace Test
{
class MyClass
{
int i = 0; // Счетчик сообщений в общей памяти потоков
// Клиентский код для управления дополнительным потоком
public MyClass()
{
// Создаем дополнительный поток для функции Func()
ThreadStart del = new ThreadStart(Func);
Thread th = new Thread(del);
th.Start(); // Запускаем на выполнение
// Приостановим основной поток для устранения состязаний
Thread.Sleep(1000);
// Обследуем запущенный дополнительный поток
Console.WriteLine("{1}) Запустили Func()
в потоке {0}",
th.GetHashCode(), ++i);
Console.WriteLine("{1}) Состояние
работающего потока: {0}",
th.ThreadState.ToString(), ++i);
// Пытаемся прервать самовосстанавливающийся поток
th.Abort("Основной поток хочет уничтожить!");
// Приостановим основной поток для устранения состязаний
Thread.Sleep(1000);
// Восстановленная функция закончила работу
Console.WriteLine("{1}) Состояние
завершенного потока: {0}",
th.ThreadState.ToString(), ++i);
}
// Локальная функция, размещаемая в отдельном потоке
void Func()
{
try
{
// Эмулируем бурную деятельность, пока не прервут
for (; ; ) ;
}
catch (ThreadAbortException e)
{
// Обследуем поток
Console.WriteLine("{1}) Выброшено
исключение {2}\n\t" +
"Сообщение: {0}", e.Message,
++i, e.GetType().Name);
Console.WriteLine("{1}) Состояние
потока при " +
"попытке уничтожения:\n\t{0}",
Thread.CurrentThread.ThreadState.ToString(), ++i);
Console.WriteLine("{1}) Сообщение,
переданное " +
"через параметр Abort():\n\t{0}",
e.ExceptionState.ToString(), ++i);
// Самовосстанавливаемся
Thread.ResetAbort();
// Обследуем поток
Console.WriteLine("{1}) Состояние
потока после " +
"восстановления:\n\t{0}",
Thread.CurrentThread.ThreadState.ToString(), ++i);
Console.WriteLine("{1}) Func() выполняется
в потоке {0}",
Thread.CurrentThread.GetHashCode(), ++i);
}
}
}
// Класс с точкой входа
class Program
{
// Исполняется в основном потоке
static void Main()
{
// Настройка консоли
Console.Title = "Попытка уничтожения " +
"самовосстанавливающегося потока";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 51;
Console.WindowHeight = 13;
new MyClass();// Исполняем
// Ждем нажатия клавиши Enter
Console.ReadLine();
}
}
}

В коде примера мы применили статический метод Thread.Sleep(int) для приостановки основного потока, чтобы дать возможность начать выполняться дополнительному потоку. Это нужно для того, чтобы до дополнительного потока дошла очередь и он получил свой квант процессорного времени.
Мы уже частично использовали свойство, только для чтения, Thread.ThreadState в предыдущем примере, чтобы узнать, в каком состоянии находится поток. Мы его применяли и к экземпляру th потока во внешнем коде как th.ThreadState.ToString(), и внутри потока как Thread.CurrentThread.ThreadState.ToString(). Значения этого свойства, устанавливаемые одноименным перечислением System.Threading.ThreadState, позволяют контролировать проводимые с потоком манипуляции и определять, в каком состоянии он в данный момент находится, прежде чем применять к нему какие-то действия.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.