Nunit, XML и HttpWebRequest

На этой неделе вожусь с тестированием  фукнционала, который присобачила за последние 2 недели. 

Для начала немножко об NUnit. Давно я уже его не видела, курса эдак со 2го, да и тогда он, можно сказать, прошел мимо меня, потому что я наивно полагала, что в этой жизни он мне нафиг не упал. О! Как я ошибалась! Похоже, в первый месяц мои работодатели решили разом поднять все, что я недоучила в вузе: xml, кэширование, Nunit… только многопоточности еще не хватает.

В принципе знать о модульном тестировании много не надо: 

  1. делаешь класс с атрибутом [TestFixture]
  2. в нем пишешь методы-тесты с атрибутом [Test]
  3. если результатом теста должна быть ошибка пишешь еще атрибут [ExpectedException(typeof(<тип ошибки>))]
  4. в конце метода, если он не ошибковозвращающий используется класс Assert и его чудо-методы, который определяет, успешно ли пройден тест. 

Далее о самих тестах этого понедельника.

Тест 1: сделать запрос страницы, о которой уже упомяналось ранее и проверить, что возвращенный XML именно то, что мы хотели получить. Чтобы не писать кучу проверок, сделала только проверки на наличие ключевых узлов и атрибутов. 

Кочка номер один: послать запрос на сервер. 

Проще простого на деле просто пишешь код вида:

	XmlDocument resultXML = new XmlDocument();
	HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(connection);
	using (var response = new StreamReader(request.GetResponse().GetResponseStream()))
	{
		resultXML.LoadXml(response.ReadToEnd());
	}

Кочка номер два: выбрать ключевые узлы и проанализировать их.

По незнанию сперва сделала быдло-методом. Стыдно, конечно, но больше такой ошибки не повторю:

	XmlNodeList resultNode = resultXML.GetElementsByTagName("Result");
	if (resultNode.Count != 0)
	{
		XmlNode rootChild = resultNode[0].FirstChild;
		if (rootChild.Name == "Item" && rootChild.Attributes["type"].Value == "ItemType")
		{
			for (int i = 0; i < rootChild.ChildNodes.Count; i++)
			{
				if (rootChild.ChildNodes[i].Name == "name" && rootChild.ChildNodes[i].InnerText.ToLower() == param.ToLower())
				{
					testRes = true;
					break;
				}
			}
		}
	}

Уже после того, как мой мудрый суgервайзер сказал, что это бред, сделала как надо:

	XPathNavigator nav = resultXML.CreateNavigator();

	XPathNodeIterator iterator = (XPathNodeIterator)nav.Select("//Result/Item[@type='ItemType']/name['Part']");

	if (iterator.Count != 0)

		testRes = true;

Тест 2: как послать повторный запрос на сервер, чтобы в итоге от вернул 304?

string connection = ConnectionInfo.InnovatorServerURL.ToString() + “/ItemType.aspx”;

HttpWebResponse response = (HttpWebResponse)HttpWebRequest.Create(connection).GetResponse();

HttpWebRequest resRequest = (HttpWebRequest)HttpWebRequest.Create(connection);

resRequest.IfModifiedSince = response.LastModified;

HttpWebResponse resResponse = (HttpWebResponse)resRequest.GetResponse();

Так и только так, потому что заголовок IfModifiedSince выставляет клиент, а не сервер, а так как в данном случае браузер мы не используем, данный параметр прописываем на сервере ручками. 

Источники

  1. поиск по документу с xpath
  2. доступ к атрибутам модели dom
  3. запрос к вебстранице из кода
  4. работа с xml в .net

Для прокачки мозга смотрела очередной доклад Гайдара Магданурова на этот раз на тему оптимизации asp.net приложений. Пришла в голову мысль, что проверка валидности введенных данных на сервере проводилась у меня довольно-таки просто (даже тупо): нет ни красоты ни грамотности. Поэтому решила это дело загуглить. И нашла эту замечательную статейку. Надо при оказии проверить на практике. 

Сериализация времени выполнения

Сериализация - это процесс, при котором данные объекта переносятся из памяти в поток данных для сохранения. Десериализация - обратный процесс, заключающийся в восстановлении состояния объекта из потока. Сохранение полей объекта требует сохранения всех данных агрегируемых объектов, т.е. требуется сохранить граф зависимых объектов. Механизм сериализации:

  • Сериализация времени выполнения
  • Сериализация контрактов данных
  • XML-сериализация

Сериализация времени выполнения (runtime serialization) - сериализация в двоичном формате обеспечивают классы из System.Runtime.Serialization.Formatters.Binary. Сериализуемый тип - тип, помеченный атрибутом [Serializable]. Сериализация некоторых полей может не иметь смысла (например, конфиденциальные данные). Для таких полей можно применить атрибут [NonSerialized]. Метод Serialize() класса BinaryFormatter принимает принимает 2 аргумента: поток сериализации и сериализуемый объект:

var formatter = new BinaryFormatter();
using (Stream s = File.Create("group.dat"))
{
    formatter.Serialize(s, group);
}
//Метод Deserialize() класса BinaryFormatter выполняет десериализацию:
using (Stream s = File.OpenRead("group.dat"))
{
    group = (Group)formatter.Deserialize(s);
}

Метод десериализации размещает объект в памяти и возвращает ссылку на него. Тип может реализовать интерфейс IDeserializationCallback, который содержит единственный метод OnDeserialization(), вызываемый автоматически после десериализации объекта. 

[Serializable]
public class Group : Collection<Student>, IDeserializationCallback
{
    // после десериализации заполним поле BestStudent
    // CLR не поддерживает работу с параметром sender
    void OnDeserialization(object sender)
    {
        FindTheBest();
    }

    // неизменившиеся элементы класса не показаны
}

Альтернативой IDeserializationCallback являются атрибуты [OnDeserializing], [OnDeserialized], [OnDeserializing], [OnDeserialized], применимые к методам. Помеченные методы вызываются автоматически до и после сериализации соответственно. Метод, который помечен одним из указанных атрибутов, должен принимать в качестве аргумента объект класса StreamingContext и не возвращать значений. 

[Serializable]
public class Group : Collection<Student>
{
    [OnDeserialized]
    private void AfterDeserialization(StreamingContext context)
    {
        FindTheBest();
    }

    // неизменившиеся элементы класса не показаны
}

Атрибут [OptionalField] применяется к полю и подавляет при десериализации генерацию исключения, если помеченное поле не найдено в потоке данных. Если не устраивает способ организации потока сериализуемых данных, он может повлиять на этот процесс, реализуя в сериализуемом типе интерфейс ISerializable. Этот интерфейс позволяет выполнить любые действия, созданные с формированием данных для сохранения. Метод GetObjectsData() вызывается автоматически при выполнении сериализации. Реализация метода подразумевает заполнение объекта SerializationInfo набором данных вида “ключ-значение”, которые соответствуют полям сохраняемого объекта. Тип, реализующий интерфейс ISerializable, должен содержать специальный private-конструктор, который будет вызывать CLR после выполнения десериализации. Конструктор должен иметь параметр типа SerializationInfo и параметр типа StreamingContext. 

[Serializable]
public class Student : IComparable<Student>, ISerializable
{
    // не показаны свойства и метод CompareTo()

    // неявная реализация интерфейса ISerializable
    void ISerializable.GetObjectData(SerializationInfo info,
                                     StreamingContext ctx)
    {
        info.SetType(typeof(Student));
        info.AddValue("Name", Name);
        info.AddValue("Age", Age);
        info.AddValue("Mark", (int)(GPA * 10));
    }

    private Student(SerializationInfo info, StreamingContext ctx)
    {
        Name = info.GetString("Name");
        Age = info.GetInt32("Age");
        GPA = info.GetInt32("Mark") / 10.0;
    }

    public Student() { }
}
Тестовое задание Qulix

Начну с главного и вернусь ко вторичному. Получила вчера тестовое задание в компании Qulix Systems. Необходимо было написать небольшое приложение на asp.net MVC за сутки. Все подробности задания здесь. Основными проблемами стали использование чистого ado.net (без EF и LINQ) и собственно само MVC, т.к. по большому счету это тестовое задание и стало моим первым проектом с использованием этой технологии. До сих пор не могу толком понять, как я умудрилась организовать доступ к данным, но счастлива, что это работает. Что касается MVC, то в целом проблем было не много, если не считать необходимость организации DropDownList на котором я и повисла на полдня. И снова спасибо ГайдарМа за то, что повисла я только на этом, потому что без тьюториалов это задание мне можно было бы даже не начинать - дохлый номер. 

Результат моих 16 часовых трудов можно лицезреть тут

Далее что касается самого собеседования, то ничего примечательного, как и говорилось в отзывах в нем действительно не было. Я, разумеется, как обычно налажала на, в общем-то, несложных вопросах технического характера (думаю, это и стало причиной того, что я получила в плечи тестовое задание). Надо отметить, что было много вопросов и личного характера. Примерный список приведу ниже:

1. Почему решили устраиваться на работу?

2. Что такое ООП?

3. Что такое полиморфизм? (привести пример)

4. Что такое MVC и в чем его преимущество?

5. Почему вам нравится ASP.NET (тест англиского)?

6. Где вы изучали английский? Как оцениваете свой уровень?

7. С какими людьми вам тяжело работать?

8. Какую обстановку считаете рабочей?

9. Какие методы обучения предпочитаете?

10. Сколько можете работать подряд?

11. Сколько времени ушло на изучение MVC?

12. Какую зарплату желаете получать (испытательный срок, работа)?

13. Перечислите этапы жизни страницы?

14. Какие события генерируются? Опишите их все

15. С какой задачей (в программировании) вы сталкивались (считаете сложной и интересной)?

16. Знаете ли вы что-либо о наше компании?

17. Готовы ли вы работать с .net платформой, а не только asp.net?

18. Какие виды join в базах данных вы знаете? (описание) 

XML-сериализация

Сериализацию в формате XML можно выполнить при помощи класса XmlSerializer из пространства имен System.Xml.Serialization. Сохраняются public элементы объекта, тип объекта должен быть открытым и иметь public-конструктор без параметров. 

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public double GPA { get; set; }
}

var student = new Student {Name = "Smirnov", Age = 18, GPA = 9};
// при создании XmlSerializer требуется указать сериализуемый тип
var serializer = new XmlSerializer(typeof (Student));

// сериализация
using (Stream stream = File.Create("student.xml"))
{
    serializer.Serialize(stream, student);
}

// десериализация
using (Stream stream = File.OpenRead("student.xml"))
{
    student = (Student) serializer.Deserialize(stream);
}

Настройка XML-сериализации может быть выполнена при помощи атрибутов:

  • [XmlRoot] - применяется к типу и задает корневой элемент в XML-файле.
  • [XmlElement] - имя и пространство имен XML-элемента.
  • [XmlAttribute] - сохранение члена класса, как атрибута.
  • [XmlIgnore] - элементы, которые не должны сохраняться.
  • [XmlArray] - настройка коллекции элементов.
  • [XmlArrayItem] - настройка имени отдельного элемента коллекции.
public class Student
{
    [XmlAttribute("name")]
    public string Name { get; set; }
    [XmlAttribute("age")]
    public int Age { get; set; }
    [XmlIgnore]
    public double GPA { get; set; }
}

[XmlRoot("students")]
public class Group
{
    public Student BestStudent { get; set; }
    [XmlArray("list")]
    [XmlArrayItem("student")]
    public List<Student> List { get; set; }
}
Сериализация контрактов данных

Контракт данных - это тип (класс или структура), описывающий информационный фрагмент. Если в качестве контракта данных используется обычный класс, информационный фрагмент образуют открытые поля и свойства. Можно пометить тип атрибутом [DataContract]. Тогда информационный фрагмент будут составлять поля и свойства, помеченные атрибутом [DataMember] (в этом случае видимость роли не играет):

[DataContract]
public class Student
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public int Age { get; set; }

    [DataMember]
    public double GPA { get; set; }
}

Для сериализация контракта данных используются следующие классы:

  • DataContractSerializer - сериализация контракта в XML-формате.
  • NetDataContractSerializer - сериализация данные и тип контракта в XML-контракта.
  • DataContractJsonSerializer - сериализация данных в формате Json.
// сериализация в формате JSON
var jsonds = new DataContractJsonSerializer(typeof(Student));
using (Stream s = File.Create("student.json"))
{
    jsonds.WriteObject(s, student);
}

// используя NetDataContractSerializer, тип указывать не нужно
var netds = new NetDataContractSerializer();
using (Stream s = File.Create("studentNDS.xml"))
{
    netds.WriteObject(s, student);
}

Атрибут [DataContract] имеет свойства Name и Namespace для указания имени и пространства имен корневого XML-элемента.  У атрибута [DataMember] есть свойство Name, Order (порядок сериализации членов контракта), IsRequired (обязательный элемент в потоке), EmitDfaultValue (запись в поток значений по умолчанию). Если контракт является коллекцией объектов, он маркируется атрибутом [CollectionDataContract]. Для методов контракта применимы атрибуты [OnSerializing], [OnSerialized], [OnDeserializing], [OnDeserialized].

Работа с транзакциями

Транзакция - набор операций, которые для обеспечения целостности и корректного поведения системы должны быть выполнены успешно или неудачно только все вместе. Транзакция характеризуется четырьмя свойствами, часто называемыми свойствами ACID:

  1. неделимость (Atomic) - все шаги транзакции должны быть выполнены или не выполнены вместе. 
  2. согласованность (Consistent) - транзакция переводит базу данных из одного стабильного состояния в другое. 
  3. изолированность (Isolated) - транзакция не должна влиять на другую транзакцию, выполняющуюся в одно и то же время. 
  4. долговечность (Durable) - транзакция успешна только тогда, когда изменения, которые произошли во время транзакции, сохраняются на каком-то долговременном носителе. 

Транзакции можно разделить на две категории - локальные и распределенные. Локальная транзакция использует поддерживающие транзакции источник данных и не выходит за рамки одного соединения. Когда все участвующие в транзакции данные содержатся в одной базе данных, сама база обеспечивает выполнение правил ACID. 

Распределенная транзакция охватывает несколько источников данных, и ей может потребоваться читать сообщения с сервера очереди сообщений, выбирать данных SQL Server и записывать их в другие базы данных. 

Провайдеры данных имеют собственные реализации для класса транзакций. Все они реализуют интерфейс IDbTransaction.  Метод Commit() идентифицирует транзакцию как успешную. Метод Rollback() помечает транзакцию как неудачную, и все ожидающие изменения аннулируются. 

using (var conn = new SqlConnection(". . ."))
{
    conn.Open();
    // получить транзакцию можно, если подключение открыто!
    var trans = conn.BeginTransaction();
    command1.Transaction = trans;
    command2.Transaction = trans;
    try
    {
        command1.ExecuteNonQuery();
        command2.ExecuteNonQuery();
        trans.Commit();
    }
    catch (Exception)
    {
        trans.Rollback();
        throw;
    }
    finally
    {
        conn.Close();
    }
}

Важной характеристикой транзакции является уровень изоляции. Уровень изоляции - степень видимости внутри транзакции изменений, выполненных за пределами этой транзакции. Он определяет, насколько чувствительна транзакция к изменениям, выполненным другими транзакциями. 

  • Durty reads - “грязное” чтение. 1 пользователь начинает транзакцию, изменяющую данные. В это время 2 пользователь (или новая транзакция) начинает считывание частично измененной (неверной) информации. 
  • Non-repeatable reads - неповторяемое чтение. 1 пользователь начинает транзакцию, изменяющую данные. В это время 2 пользователь начинает и завершает другую транзакцию. 1 пользователь при повторном чтении получает другой набор записей. 
  • Phantom reads - чтение фантомов. 1 пользователь начинает транзакцию , выбирающую данные из таблицы. В это время 2 пользователь начинает и завершает транзакцию, вставляющую или удаляющую записи. 1 пользователь получит не измененную информацию, содержащую не измененные или удаленные данные (фантомы).

Уровни изоляции описываются перечислением IsolationLevel:

  • ReadUncommitted - транзакция может считывать данные, с которыми работают другие транзакции. 
  • ReadCommited - транзакция не может считывать данные, с которыми работают другие транзакции. (исключение “грязного” чтения)
  • RepeatableRead - транзакция не может считывать данные, с которыми работают другие транзакции, другие транзакции тоже не могут считывать данные, с которыми работает эта транзакция. (исключение всего, кроме чтения фантомов)
  • Snapshot - уменьшает вероятность установки блокировки строк, сохраняя копию данных, которое одно приложение может читать, в то время как другие модифицирует эти же данные. В SQL Server 2005 изоляция Snapshot перед использованием должна быть разрешена на уровне самой базы данных. Это можно сделать с помощью следующей SQL команды: ALTER DATABASE ИмяБД SET ALLOW SNAPSHOT ISOLATION ON
  • Serializable - транзакция полностью изолирована от других транзакций. (исключение всех проблем).
using (var myTrans = new TransactionScope())
{
    using (var conn_1 = new SqlConnection("..."))
    {
        conn_1.Open();
        var myCmd = conn_1.CreateCommand();
        myCmd.CommandText =
                  "Insert into Credits (CreditAmount) Values (100)";
        myCmd.ExecuteNonQuery();
    }
    Console.WriteLine("Первая операция завершена");
    Console.ReadLine();
    using (var conn_2 = new SqlConnection("..."))
    {
        conn_2.Open();
        var myCmd = conn_2.CreateCommand();
        myCmd.CommandText =
                  "Insert into Debits (DefeitAmount) Values (100)";
        myCmd.ExecuteNonQuery();
    }
    myTrans.Complete();
}
Гексаграмма WPF

На матмоделировании дали задание: сгенерировать методом середины квадрата и конгруэнтным методом n случайных чисел и протестировать, насколько эти числа получились случайными (независимость и равномерность распределения). Для этого необходимо было подсчитать сколько значений попало в определенный интервал (шаг) и вывести на экран гексаграмму. Сами методы у меня заняли недолго. Загвоздка оказалась именно в графике. Думала сделать все в winForms, да не тут-то было. Технология вывода графиков тут не менее запутана, чем в WPF, а то и еще хуже. Оказалось, у меня осталась лабораторная работа с позапрошлого семестра по методам численного анализа, где, собственно, я уже выводила график (по точкам). Для вывода того графика я использовала WPF Toolkit, которую можно скачать тут. Reference на WPFToolkit, тем не менее, не добавляется, пока не перезагрузишь компьютер. Однако, и этого оказалось мало. Поэтому пришлось просто скопировать ту самую лабораторную (номер 4) и попытаться ее модифицировать. И все бы ничего, если бы графики по точкам не строились совершенно непонятным для меня образом. Казалось бы, что может быть проще, чем соединить 2 точки одной линией, но нет — у него своя, непостижимая мною, технология. В итоге, так ничего и не сделав, легла спать. Сутра увидела (вива twitter), что друг посоветовал использовать Visifire. Нашла, скачала, установила. Очень повезло, что в упаковке с библиотекой было еще и пара примерчиков использования. В общей сложности создание нового окна wpf, обработка событий и модификация графика с visifire заняло у меня окло 2 часов с учетом еще и отладки. Программу можно скачать здесь.

Веб-службы и их кеширование

*FQN = fully qualified assembly name (или fully qualified name)

Постановка задачи в общем-то все та же: обработать GET запрос, выдать в ответ некий XML и, вернувшийся ответ, закешировать на сервере и на клиенте. 

Как я уже упоминала, на работе из меня в первую очередь решили вытянуть то, что я в вузе изучила плохо. Веб-службы были как раз среди того, что прошло мимо меня. Была у нас одна лабораторная работа, для выполнения которой хватило одного 10-минутного ролика ГайдарМа и на этом все закончилось. 

Сейчас же вопрос встал четко: есть приложение, нужно интегрировать в него веб-службу. Крутись, детка!
Для чего все это вообще нужно? В общем-то идея простой веб-странички (вернее обработчика запроса A.aspx) вполне себе ничего. Как сказал шеф, то, что он угадывает по параметрам, какой метод нужно выполнить и какие данные вернуть больше смахивает на искусственный интеллект, но это можно пережить. Но цель вообще мероприятия была вполне однозначной: приклеить функциональность, при этом не изменив (не сломав) работу проекта, при этом чтобы у этой функциональности была возможность расширения, легкого удаления и модификации. В контексте небольшого проекта все это есть и у обычного httpHandlerа, но проект уже довольно большой, и я решила, что не хочу, чтобы пришедший на мое место студент через десяток лет смотрел на мою фитчу и, матерясь, думал, какое быдло все это писало, как думаю на некоторый местный функционал сейчас я.  

Итак, закончив с долгой прелюдией расскажу по порядку. 

1) Шаг 1 создадим новую asmx “страничку”, назовем ее TestPg.asmx. Никакого особого содержимого в ней нет и не будет. Достаточно указать директиву:

<%@ WebService Language=”VB(or C#)" Debug="true" Class="FQN.MyServiceClass"%>

Эта страничка будет мирно лежать на сервере, именно ее имя мы будем прописывать в URL для обращения к нашему чудо-сервису, а пока едем дальше.

2) как можно уже было догадаться далее мы напишем наш класс, где и будут лежать все методы нашей службы. В уже существующем sln создаем новый  компонент (Component.vb) с именем MyServiceClass.vb и напишем в нем код примерно такого вида:

Namespace FQN
	<WebService()> _
	<ScriptService(UseHttpGet:=True)>
	Public Class MyServiceClass
		Inherits WebService
		<WebMethod(EnableSession:=True, CacheDuration:=100)> _
		<ScriptMethod(UseHttpGet:=True)>
		Public Function HelloWorld() As XmlDocument
			Dim doc As XmlDocument = New XmlDocument()
			doc.LoadXml(String.Format(CultureInfo.InvariantCulture, "<root>{0}</root>", Context.Request.QueryString(0)))
			Return doc
		End Function	
	End Class
End Namespace

Даже несмотря на параметр атрибута UseHttpGet:=True, запрос на чудо-страницу TestPg.asmx будет, вероятно, все равно типа POST. Ну да, вот такая вот эта служба… 

Чтобы заставить принимать еще и GET запросы, нужно прописать в web.config вот такую штуку:

<webServices>
    <protocols>
        <add name="HttpGet"/>
        <add name="HttpPost"/>
    </protocols>
</webServices>

Ах да, еще одна деталька, может быть так, что ваш сервис не будет работать вообще никак из-за вот этой ошибки:

CS0618: Warning as Error: ‘System.Uri.Uri(string, bool)’ is obsolete: ‘The constructor has been deprecated. Please use new Uri(string).

Проблема связана с версиями Frameworkа. Если переставить на iis Framework на 4, все заработает. Вроде бы это можно поставить и в файле конфигурации, но у меня на этом выходил фейл другого рода:

Unrecognized attribute ‘targetFramework’

Это было такое ругательство на блок:

<compilation debug="true" targetFramework="4.0">
    <assemblies>
    </assemblies>
</compilation>

Итак, мы создали сервис, который “понимает” GET запросы. Что касается кеширования его ответов, но делать мы это будет уже привычным для нас способом: выставим заголовок ответа 

context.Response.Cache.AppendCacheExtension(“max-age=31104000”)

а для кеширования на сервере проведем хак вида:

Dim strIfNoneMatch As String = context.Request.Headers.Get("If-None-Match")
			Dim responseHash As Integer = _response.InnerXml.ToString().GetHashCode()
			If String.IsNullOrEmpty(strIfNoneMatch) Then
				context.Response.Headers.Set("ETag", responseHash.ToString(CultureInfo.InvariantCulture))
				Return
			Else
				Dim IfNoneMatch As Integer = Int32.Parse(strIfNoneMatch, CultureInfo.InvariantCulture)
				If IfNoneMatch <> responseHash Then
					context.Response.Headers.Set("ETag", responseHash.ToString(CultureInfo.InvariantCulture))
					Return
				End If
			End If
			'if hashcode we generated this page last time is actual we don't send data to user once again
			context.Response.StatusCode = 304

Этот вариант кеширования довольно-таки жесткий. Для обычного метода HelloWorld все это разумеется лишнее. 

Выставление заголовка max-age хорошо, если вы возвращаете статический ресурс, который, вероятно, никогда не изменится. 

Второй способ кеширования вообще очень классный: он снижает количество трафика, получаемого клиентом и, хоть и несильно, уменьшает время получение ответа. По скорости, он будет уступать первому, но данные, которые получает пользователь всегда будут актуальны за счет проверки If-none-Match заголовка запроса (подробнее в других статьях). 

ЗЫ: И еще кое-что интересное, касающееся этого таска:

Неприятная, очень скользкая ошибка “A severe error occurred on the current command.  The results, if any, should be discarded.” Ошибка эта баз данных, в моем случае это некорректный откат транзакций. Не могу сказать точнее, т.к. до сих пор толком ее не засолвила, но это связано с данными пользователя и пулом потоков (что-то вроде в пуле остаются некорректные данные).

Источники:

  1. стратегии кеширования 
  2. вызов веб-служб из клиентского сценария
  3. вызов веб-сервиса из js
  4. пишем http запрос
  5. gonfig and Web-service
  6. get и post: что лучше?
  7. вызов веб-службы из jquery
  8. Get and Post are disabled by default
  9. config options
XML to XSLT

xml это данные для html странички, а xslt это шаблон этой странички. пишется сначала xslt, а xml для него формирую автоматически из объектов. Например:

Исходный XML-документ:

<?xml version="1.0"?>
<persons>
  <person username="MP123456">
    <name>Иван</name>
    <surname>Иванов</surname>
  </person>
  <person username="PK123456">
    <name>Пётр</name>
    <surname>Петров</surname>
  </person>
</persons>

Таблица XSLT-стилей (преобразования):

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
 
    <xsl:output method="xml" indent="yes"/>
 
    <xsl:template match="persons">
        <transform>
            <xsl:apply-templates/>
        </transform>
    </xsl:template>
 
    <xsl:template match="person">
        <record>
            <xsl:apply-templates select="@*|*"/>
        </record>
    </xsl:template>
 
    <xsl:template match="@username">
        <username>
            <xsl:value-of select="."/>
        </username>
    </xsl:template>
 
    <xsl:template match="name">
        <fullname>
            <xsl:apply-templates/>
            <xsl:apply-templates select="following-sibling::surname" mode="fullname"/>
        </fullname>
    </xsl:template>
 
    <xsl:template match="surname"/>
 
    <xsl:template match="surname" mode="fullname">
        <xsl:text> </xsl:text>
        <xsl:apply-templates/>
    </xsl:template>
 
</xsl:stylesheet>

Результирующий XML-документ:

<?xml version="1.0" encoding="UTF-8"?>
<transform>
   <record>
      <username>MP123456</username>
      <fullname>Иван Иванов</fullname>
   </record>
   <record>
      <username>PK123456</username>
      <fullname>Пётр Петров</fullname>
   </record>  
</transform>

Далее встает вопрос, как же программно “приклеить” xml к xslt? 

// Create the XslTransform.
System.Xml.Xsl.XslTransform xslt = new System.Xml.Xsl.XslTransform();

// Load the stylesheet.
xslt.Load("numbers.xsl");

// Load the XML data file.
System.Xml.XPath.XPathDocument doc = new 
  System.Xml.XPath.XPathDocument("numbers.xml");

// Create the XmlTextWriter to output to the console.
System.Xml.XmlTextWriter writer = new 
  System.Xml.XmlTextWriter(System.Console.Out);

// Transform the file.
xslt.Transform(doc, null, writer);

// Be careful when using the console,
// closing it will not allow you to write any 
// additional information to it
writer.Close();

И еще пара полезных ресурсеков на всякий случай: раз, два

пользовательский HttpHandler и его регистрация

В целом понятно, что основная фишка пользовательского HttpHandlerа это реализация интерфейса IHttpHandler на этом останавливаться не буду написать свойство и 1 метод может любой дурак. Самое забавное это “присобачивание” этого чудо-класса к запросу. 

Во-первых, для различных версий серверов секции регистрации HttpHandlerов в web.configе различные. Для версий ниже 7 это

<system.web>
	<httpHandlers>
		<add verb="GET" path="ItemType.aspx" type="XXX.XXX.XXX.MyHttpHandler" />
	</httpHandlers>
</system.web>

а для более крутых версий 

<system.webServer>
        <handlers accessPolicy="Read, Script">
            <add name="ItemType" verb="GET" path="ItemType.aspx" type="XXX.XXX.XXX.MyHttpHandler" />
	</handlers>
</system.webServer>

Если не знаешь точно, с каким именно iis-ом будет использоваться приложение, нужно прописать и то и другое. 

При реализации IHttpHandler предоставляется доступ только к вызывающему контексту, для доступа к сессии необходимо также, чтобы handler реализовывал еще один интерфейс: IRequiresSessionState. Черт знает, как это работает, потому что никаких дополнительных методов или свойств больше описывать не нужно, но сессия будет в вашем распоряжении. 

Источники:

  1. Session in HttpHandler
  2. Session values from HttpHandler
  3. IRequiresSessionState
  4. Load Handler from Web.config
  5. пошаговое создание синхронного Http обработчика
  6. Asp.net HttpHandler
  7. элемент <httpHandlers> в web.config
кэширование asp.net

Богатой на новый для меня опыт получилась неделя. Куча черновиков валяется сейчас на tumblrе. Начну с наболевшего. 

Постановка задачи такова: есть страничка, которая достает из запроса параметры, по этим параметрам формирует xml-запрос, этот запрос обрабатывается и возвращается ответ в soap-“конверте”. Все это реализовано с помощью пользовательского HttpHandlerа, о котором я уже упоминала и о котором еще скажу. Нужно организовать кэширование данного запроса и при повторном обращении с одними и теми же параметрами возвращать кэшированную версию странички. Как известно, если страница не была изменена, сервер возвращает ответ 304 (not modified), в противном случае - 200 (Для тех, кто в танке: Коды статуса HTTP).

Очень не хочется нецензурно выражаться, поэтому просто скажу, что очень разочаровалась в доселе любимом msdn, потому что тут механизм кэширования освящен до безобразия плохо. Для человека, вообще не сведущего в этой области в местных статьях не ясно ровным счетом ничего. Спасибо, конечно, капитанушки, что разместили статьи о AllowResponseInBrowserHistory, OutputCacheProfile, добавлении элементов в кэш и их извлечении, но не нужно быть гением, чтобы залезть в IntelliSence и прочесть тот же список методов с кратким описанием там. В итоге после 5 часового и почти безрезультатного дрейфа по просторам интернета, написав кипу умных строчек вида:

   context.Response.Cache.SetValidUntilExpires(True)
   context.Response.Cache.SetETag("test")
   context.Response.Cache.VaryByParams("name") = True
   сontext.Response.Cache.SetCacheability(HttpCacheability.Public)
   context.Response.Cache.SetAllowResponseInBrowserHistory(True)

и ровным счетом не поняв ничего я оказалась дома с Макдональдом. По прочтении 5 страниц механизм кэширования стал понятен как “доброе утро”. Тем не менее инфы для решения проблемы не хватало. Отсылаешь запросы, страница вроде должна кэшироваться, но в ответ является все тот же 200, вместо 304. 

Придя на следующее утро в офис, я наткнулась на интересную статейку, результатом которой стал ультра-мега адский хак вида:

context.Response.Cache.AppendCacheExtension("post-check=900,pre-check=3600")

Который говорит, что предполагается (вероятно, может быть) содержимое страницы не изменится в течение следующего часа, но на всякий случай стоит это дело поверять в течение 15 минут. Хорошая попытка, но мимо: эта строчка кэширует страницу на стороне клиента, т.е. первый запрос - ответ 200, а последующее обновление страницы в течение часа не приведет ни к посылке запросов, ни к их получению. Да, не 200, но и не 304!

Уже изрядно заколебавшись, решила посмотреть, работает ли кэширование так, как мне нужно при наличии aspx страницы. Да, работает. Создаем простую aspx страницу, прописываем в директиве Page атрибут CodeFile равный полностью определенному имени файла с нашим пользовательским HttpHandlerом, на самой страничке что-то вроде:

HttpHandler h = new MyHttpHandler();
h.ProcessRequest(context);

и, конечно, гвоздь программы - директива OutputCache:

<%@ OutputCache Duration="60" VaryByParam="*" %>

Страница в этом случае кэшируется на сервере в течение 60 секунд. Для различных параметров запроса кэшируются различные версии страницы. Да, при первом запросе страницы приходит 200, при повторе запроса в течение 60 секунд - 304. Но вот проблема: что если информация на моей странице обновляется каждые 15 секунд. В этом случае мой пользователь получает неактуальную информацию. То есть и это не вариант. 

Так появилась задача номер 2: как сделать так, чтобы пользователь получал новую информацию, если она обновилась и not modified в противном случае?

Начну издалека. Как уже упоминалось в статье выше есть в HTTP ответе-запросе заголовки, отвечающие за политику кэширования. Некоторые из них можно выставить программными средствами .net, а вот некоторые - нет. К примеру есть у нас метод Response.Cache.SetExpires(DateTime.Now.AddHours(1)) , который устанавливает, до какого времени кэшированная версия страницы будет валидной. Это метод на деле выставляет заголовок HttpResponsа как-то так:

Expires: Sun, 25 Jun 2006 14:57:12 GMT //дата здесь левая, просто к примеру

На сервере проверяется именно эта дата, и, если она еще не наступила - возвращается Not modified (304). В asp.net еще предусмотрена зависимость ответа от того, модифицирован ли ранее сохраненный файл, но здесь о нем говорить не буду, т.к. к решению моей задачи отношения это не имеет. 

Предусмотрены так же методы 

context.Response.Cache.SetETag("123");
context.Response.Cache.SetLastModified(DateTime.Now);

Работают эти штуки по схожему принципу: мы запрашиваем страницу, кэшируем ее и выставляем, к примеру, LastModified(DateTime.Now). При следующем запросе страницы одним из заголовком запроса приходит “If-Modified-Since” который сравнивается с, к примеру, датой последней модификации какого-нибудь файла, и, если файл не был изменен - возвращается 304 и 200 в противном случае. ETag работает по абсолютно тому же принципу, только сравнение идет не по дате, а по строке заголовка и при повторном запросе сравнение идет по заголовку “If-None-Match”. 

Теперь нужно воспользоваться этой инфой. Чтобы проверить актуальность информации, которую мы собираемся возвратить клиенту, на в любом случае придется обработать запрос и подготовить для него xml-ответ. В этом чудесном ответе у нас есть нода с датой последней модификации - с ней и работаем. Таким образом готовим XmlDocument response -> выбираем ноды “modified_on” -> сравниваем их содержимое с пришедшим нам в заголовке параметром “If-Modified-Since”. Если всякий элемент domа ответа не модифицировался после того, как последний раз пользователь получил ответ с этой страницы - возвращаем 304. В противном случае формируем новый ответ и возвращаем пользователю новые данные полноценным responsом (200). 

Пример решения схожей задачи с кодом здесь.

Таким образом выполнены все поставленные задачи. Ничего сложного, в целом, во всем этом нету, но пришлось потратить на это довольно-таки приличное количество времени. Вива ценный урок и бесценный опыт. 

Дополнительные источники:

  1. 304 изображения из БД
  2. заставить браузер кэшировать ответ из HttpHandlerа
  3. кэширование содержимого на клиенте
  4. управление кэшем в asp.net
  5. работа с If-Modified-Since
  6. метод AppendCacheExtension
Директивы и компиляция aspx

Богатый на иноформацию день. Начнем с предпосылок.

Есть у нас, стало быть, Friend (Internal) класс, реализующий некую логику. Его использует Public класс HttpHandler унаследованный от IHttpHander. И есть страница с директивой Page вида:

<%@ Page CodePage=”65001” Language=”C#” Inherits=”HttpHandler” src=”xxx.xxx.xxx”%>

Почему это не сработает:

1. атрибут Inherits - определяет наследуемый класс, унаследованный от Page 

2. для указания класса, кот. будет компилироваться при запросе страницы испольуется атрибут ClassName

Тем не менее вышеуказанная директива скомпилится и, если бы наш Public HttpHandler не изпользовал Friend класс, логика даже отработает. 

Откуда проблема с модификаторами доступа?

Как известро Friend (Internal) модификатор делает видимым класс только в рамках данной сборки, но asp.net компилирует aspx страницы, т.е. создает динамическую сборку для каждой страницы или группы страниц, а этот класс у нас не доступен из других сборок. 

В сборке находится класс, унаследованный в классе из атрибута Codebehind (унаследованный от Page) , зависимости для определения факта изменения страницы, создание серверных элементов управления и инициализация базового класса, построение дерева элементов управления.

Friend класс будет виден, если  HttpHandler класс реализовать в той же сборке, где наш Friend класс, ибо этот HttpHandler скомпилится в той же сборке, что и Friend, а страница унаследует этот HttpHandler в другой сборке. 

Источники:

  1. IHttpHandler.ProcessRequest - метод
  2. пакетная компиляция asp.net
  3. @ Page
  4. Директивы ASP.NET Pages
ASPX страница GET запросы

Исходные данные:

есть страница Default.aspx, на которой реализована кнопка (Request) и TextBox (id=”Box”). По клику на кнопку на страницу ItemType.aspx отсылается параметризованный http зпрос вида: “http://localhost:3103/Task1/ItemType.aspx?val1=2&val2=3”. На ItemType.aspx происходит обработка параметров и возвращается xml. 

Default.aspx


    protected void Button1_Click(object sender, EventArgs e)
    {
        box.Text = "";
        string tmp = "";
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("http://localhost:3103/Task1/ItemType.aspx?val1=2&val2=3");

        webRequest.Method = WebRequestMethods.Http.Get;
        try
        {
            webRequest.UseDefaultCredentials = true;
            WebResponse response = webRequest.GetResponse();
            StreamReader reader = new StreamReader(response.GetResponseStream());
            tmp = reader.ReadLine();
            response.Close();
            box.Text += tmp;
        }
        catch (Exception ex)
        {
            box.Text += ex.Message;
        }

    }

ItemType.aspx

    protected void Page_Load(object sender, EventArgs e)
    {
        int ParamCount = HttpContext.Current.Request.QueryString.Count;

        Response.ContentType = "application/xml";
        Response.ContentEncoding = System.Text.Encoding.UTF8;

        using (StreamWriter writer = new StreamWriter(Response.OutputStream))
        {
            writer.Write("<root>");
            for (int i = 0; i < ParamCount; i++)
            {
                writer.Write(String.Format("<param type={0}><value>{1}</value></param>", HttpContext.Current.Request.QueryString.AllKeys[i], HttpContext.Current.Request.QueryString[i]));
            }
            writer.Write("</root>");
            writer.Close();
        }
    }

Источники:

  1. как отправлять и получать xml данные
  2. как отправлять данные на другие ресурсы
  3. HttpWebRequest and WebResponse tutorial
  4. Построение url-адресов
  5. Как прочитать параметры из url в ASP .NET
Библиотека параллельных расширений

Библиотека параллельных расширений (Parallel Extensions) разработана для создания многопоточных приложений. Позволяет автоматически масштабировать выполняемые задачи, подстраиваясь под фактическое число процессорных ядер, и обеспечивает 3 уровня организации параллелизма: 1. на уровне задач 2. параллелизм при императивной обработке данных 3. параллелизм при декоративной  обработке данных.

Параллелизм на уровне задач

Задача - сущность, в целом подобная потоку. Основное отличие заключается в том, что исполнением задач управляет специальный планировщик, учитывающий фактическое число процессорных ядер. 

Для создания задачи используется один из перегруженных конструкторов класса Task. При этом указывается аргумент типа Action - метод, выполняемый в задаче. 

Конструкторы Task принимаю опциональные аргументы типа CancellationToken и TaskCreationOptions задает вид задачи (например, LongRunning - долгая задача). Структура CancellationToken применяется для прерывания задачи.

Созданная задача ставится в очередь планировщика для запуска при помощи методов Start() или RunSynchronously(). Второй метод запускает задачи в текущем потоке. 

Метод ContinueWith() позволяет создать цепочку задач, запуская указанный метод после завершения текущей задачи (текущая задача передается методу в качестве параметра). CntinueWith() можно вызвать как до, так и после старта задачи. 

Методы Wait(), WaitAll(), WaitAny() останавливают основной поток до завершения задачи (или задач). Перегруженные версии методов позволяют задать период ожидания завершения и токен отмены. 

Класс Task<T> наследуется от Task и описывает задачу, возвращающую значение типа T. Дополнительно к элементам базового класса, Task<T> объявляет свойство Result для хранения вычислительного значения. Конструкторы класса Task<T> принимают аргументы типа Func<T> и Func<T, object> (опционально - аргументы типа CancellationToken и TaskCreationOptions). 

Класс TaskFactory содержит набор методов, соответствующих некоторым сценариям использования задач - StartNew(), FromAsync(), ContinueWhenAll(), ContinueWhenAny(). Этот экземпляр TaskFactory доступен через статическое свойство Task.Factory.

public class TestAction
    {
        public static void Main()
        {
            Action work = () =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("Done");
            };

            Action<object> work2 = obj =>
            {
                Thread.Sleep(1500);
                Console.WriteLine(obj.ToString());
            };
            var t1 = new Task(work, TaskCreationOptions.LongRunning);
            var t2 = new Task(work2, "Саша");
            t2.Start();//асинхронный запуск

            //t2.RunSynchronously();//синхронный запуск
            //t1.ContinueWith(task => Console.WriteLine("After task "+task.Id));

            t2.ContinueWith(task => t1.RunSynchronously());

            //Task.WaitAll(t1, t2);
            //Task.WaitAny(t1, t2);

            Func<int> func = () =>
                { Console.WriteLine("function") ; return 100; };
            var tsk = new Task<int>(func);
            tsk.Start();
            Console.WriteLine(String.Format("{0}, {1}",tsk.Result, tsk.Status));

            Task.Factory.StartNew(() => { Console.WriteLine("Task factory"); });


            Console.ReadLine();
        }
    }

Параллелизм при императивной обработке данных

Класс System.Threading.Tasks.Parallel позволяет распараллеливать циклы и последовательность блоков кода. Эта функциональность реализована как набор статических методов For(), ForEach() и Invoke().

Методы Parallel.For() и Parallel.ForEach() являются параллельными аналогами циклов for и foreach. Их использование корректно в случае независимости итераций цикла. Все перегруженные версии Parallel.For() подразумевают указание начального и конечного счетчика и тела цикла в виде объекта делегата. 

Статический метод Parallel.Invoke() позволяет распараллелить исполнение блоков операторов. В базовом варианте Invoke() принимает параметр-список объектов делегата Action:

Action a1 = () =>
            {
                DateTime start = new DateTime(DateTime.Now.Millisecond);
                Parallel.For(0, 10, i =>
                {
                    Thread.Sleep(1);

                    //Console.WriteLine("Parallel cycle {0} {1}", i, DateTime.Now.Millisecond);
                });
                DateTime end = new DateTime(DateTime.Now.Millisecond);
                TimeSpan result = end - start;
                Console.WriteLine("Parallel method: {0}", result);
            };

            Action a2 = () =>
            {
                DateTime start = new DateTime(DateTime.Now.Millisecond);
                for (int i = 0; i < 10; i++)
                {
                    Thread.Sleep(1);
                    //Console.WriteLine("non parallel cycle {0} {1}", i, DateTime.Now.Millisecond);
                }
                DateTime end = new DateTime(DateTime.Now.Millisecond);
                TimeSpan result = end - start;
                Console.WriteLine("non parallel method: {0}", result);
            };
            
            
            Parallel.Invoke(a1,a2,()=>Console.WriteLine("Working..."));

Параллелизм при деклоративной обработке данных

PLINQ - параллельная реализация LINQ, в которой запросы выполняются параллельно, используя все доступные ядра и процессоры. PLINQ полностью поддерживает все операторы запросов, имеющиеся в LINQ to Objects.

Чтобы распараллелить этот запрос средствами PLINQ, достаточно применить к источнику данных метод расширения AsParallel();

            var numbers = Enumerable.Range(0, 10);
            Console.WriteLine(numbers.AsParallel().Where(IsEven).Count());

            Console.ReadLine();
        

        public static bool IsEven(int x)
        {
            return (x % 2 == 0) ? true : false;
        }

Обработка исключений и отмена выполнения задач

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

Принципы работы с исключительными ситуациями:

  • При возникновении исключительной ситуации в задаче это исключение обрабатывается средствами библиотеки и перенаправляется в ту задачу, которая ожидает завершения данной. 
  • При параллельном возникновении нескольких исключений все они собираются в единое исключение  System.AggregateException, которое перенаправляется дальше по цепочке вызовов задач.
  • Если возникла одна исключительная ситуация, то на ее основе будет создан объект AggregateException в целях единообразной обработки всех исключительных ситуации. 

Исключительные ситуации типа AggregateException могут возникать при работе со следующими конструкциями библиотеки параллельных расширений:

  1. Класс Task - исключения, возникшие в теле задачи, будут повторно возбуждены в месте вызова метода Wait() данной задачи. Кроме того исключение доступно через свойство Exception объекта Task. 
  2. Класс Task<T> - исключения, возникшие в теле задачи, будут повторно возбуждены в месте вызова метода Wait() или в месте обращения к экземплярному свойству Task<T>.Result.
  3. Класс Parallel - исключения могут возникнуть в параллельно исполняемых итерациях циклов или в параллельных блоках кода при работе Parallel.Invoke().
  4. PLINQ - из-за отложенного характера исполнения запросов PLINQ, исключения обычно возникают на этапе перебора элементов, полученных по запросу.

Многие методы задач, упоминавшиеся выше, принимают в качестве аргумент значение типа CancellationToken (токен отмены) - маркер того, что задачу можно отменить. 


Синхронизация потоков

Если метод запускается в нескольких потоках, то только локальные переменные метода будут уникальными для потока. В пространстве имен System определен атрибут [ThreadStatic], применяемый к статическим полям. Если поле помечено таким атрибутом, то каждый поток будет содержать свой экземпляр поля. Для [ThreadStatic]-полей не рекомендуется делать инициализацию при объявлении, т.к. инициализация выполняется только в одном потоке. 

Для неразделяемых статических полей класса можно использовать тип TheadLocal<T>.  Перегруженный конструктор принимает функцию инициализации поля. Значение поля хранится в свойстве Value.

Класс Thread содержит статические методы AllocateDataSlot(), AllocateNamedDataSlot(), GetNamedDataSlot(), которые предназначены для работы с локальными хранилищами данных потока. Эти хранилища могут быть альтернативой неразделяемым статическим полям. 

Синхронизация потоков - координирование действий потоков для получения предсказуемого результата. Существует несколько категорий средств синхронизации потоков:

  • приостановка выполнения потока (Suspend(), Resume(),Sleep(), Yield(), Join());
  • блокирующие конструкции;
  • конструкции подачи сигналов;
  • незадерживающие средства синхронизации.  

Приостановленный поток практически не потребляет времени процессора, но его легко возобновить системой при наступлении времени “пробуждения”.

Блокирующие конструкции обеспечивают исключительный доступ к ресурсу (поле, фрагмент кода). Для организации .net предоставляет классы Monitor, Mutex, Semaphor, SemaphorSlim, а язык C# оператор lock (критические секции).

lock(<объект синхронизации>) { <операторы критической секции> }
public class MyApp
{
    // в buffer хранятся данные, с которыми работают потоки
    private static int[] buffer;
    private static Thread writer;

    public static void Main()
    {
        // инициализируем buffer
        buffer = Enumerable.Range(1, 100).ToArray();

        // запустим поток для перезаписи данных
        writer = new Thread(WriterFunc);
        writer.Start();

        // запустим 10 потоков для чтения данных
        for (var i = 0; i < 10; i++)
        {
            new Thread(ReaderFunc).Start();
        }
    }
private static void ReaderFunc()
{
    while (writer.IsAlive)
    {
        int sum;
        lock (buffer)
            sum = buffer.Sum();
        if (sum != 5050)
        {
            Console.WriteLine("Error in sum!");
            return;
        }
    }
}
private static void WriterFunc()
{
    var rnd = new Random();
    var start = DateTime.Now;
    while ((DateTime.Now - start).Seconds < 10)
    {
        var j = rnd.Next(0, 100);
        var k = rnd.Next(0, 100);
        lock (buffer)
        {
            var tmp = buffer[j];
            buffer[j] = buffer[k];
            buffer[k] = tmp;
        }
    }
}
}

lock - скрытый способ работать со статическим классом Monitor, который содержит методы Enter(), Exit(), Wait() - применяется внутри критической секции и снимает с нее блокировку. Можно задать определенное время, но без параметров приостановка до момента, пока не будет вызван метод Monitor.Pulse(). 

Для блокировки ресурса так, чтобы читать его могли несколько потоков, а записывать - один, существует класс ReaderWriterLockSlim. Его экземплярные методы задают секции для чтения и для записи соответственно: EnterReadLock() (ExitReadLock()), EnterWriteLock() (ExitWriteLock()).

Потребность в синхронизации на основе сигналов возникает, когда один поток ждет прихода уведомления от другого потока. Для осуществления такой синхронизации существует класс EventWaitHandle и его наследники. Имея доступ к объекту EventWaitHandle, поток может вызвать его метод WaitOne(), чтобы ждать сигнала. Для оправки сигнала применяется вызов метода Set().

ManualResetEvent и ManualResetEventSlim - все ожидающие потоки освобождаются и продолжают выполнение. 

AutoResetEvent - ожидающие потока освобождаются и запускаются последовательно на манер очереди.

Для реализации незадерживающей синхронизации, используется статический класс System.Threading.Interlocked.

Основы многопоточности

.net каждый поток представлен объектом класс Thread. Чтобы организовать свой поток, необходимо создать объект этого класса. Конструктору может передаваться в качестве параметра делегат, инкапсулирующий метод, выполняемый в потоке. Доступно 2 типа делегатов: второй позволяет при запуске метода передать ему параметр-объект:

public Thread(ThreadStart start);
public Thread(ThreadStart start, int maxStackSize);
public Thread(ParameterizedThreadStart start);
public Thread(ParameterizedThreadStart start, int maxStackSize);

public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);

Дополнительный параметр конструктора может использоваться для указания максимального размера стека, выделяемого потоку. 

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

Основные свойства Thread:

  • CurrentThread
  • Name
  • ManagedThreadID - уникальный числовой идентификатор управляемого потока
  • ThreadState
  • IsAlive
  • Priority (ThreadPriority: Lowest, BelowNormal, Normal, AboveNormal, Hightest)
  • IsBackground
  • CurrentCulture, CurentUICulture

Помимо Start() Thread содержит методы: Suspend() (приостановка), Reasume() (возобновление), Sleep() (приостановление, можно указать кол-во милисек или TimeSpan), Yeild() (передача управления след. ожидающему потоку), Join() (дождаться завершения работы данного потока или блокировка его выполнения на опр. время).

Завершение работы выбранного потока  выполняет метод Abort(). Данный метод генерирует исключение ThreadAbortException, которое можно подавить catch блоком и может быть отслежено потоком, который кто-то пытается уничтожить, а метода ResetAbort() отклоняет запрос на уничтожение потока. 

public class MainClass
{
    public static void ThreadProc()
    {
        while (true)
        {
            try
            {
                Console.WriteLine("Do some work...");
                Thread.Sleep(1000);
            }
            catch (ThreadAbortException e)
            {
                // отлавливаем попытку уничтожения и отменяем её
                Console.WriteLine("Somebody tries to kill me!");
                Thread.ResetAbort();
            }
        }
    }

    public static void Main()
    {
        // создаём и запускаем поток
        var th = new Thread(ThreadProc);
        th.Start();
        // ждём 5 секунд
        Thread.Sleep(5000);
        // пытаемся прервать работу потока th
        th.Abort();
        // ждём завершения потока
        th.Join();
        // ... но не дождёмся, так как поток сам себя "воскресил"
    }
}

Для выполнения в отдельном потоке повторяющегося метода можно применить класс Timer из System.Threading. Конструктор таймера позволяет указать, через какой промежуток времени метода таймера должен выполниться первый раз, а так же задать периодичность выполнения (эти величины можно впоследствии изменить с помощью метода Change()).

    public class MyApp
    {
        private static bool TickNext = true;

        public static void Main()
        {
            var timer = new Timer(TickTock, null, 1000, 2000);
            Console.WriteLine("Press <Enter> to terminate...");
            Console.ReadLine();
        }

        private static void TickTock(object state)
        {
            Console.WriteLine(TickNext ? "Tick" : "Tock");
            TickNext = !TickNext;
        }
    }

.net поддерживает также специальный механизм, называемый пул потоков, который поддерживает такие технологии как асинхронные делегаты, таймеры, asp.net.

Пул состоит из 2 основных элементов: очереди методов и рабочих потоков. Характеристикой пула является его емкость - максимальное число рабочих потоков. Метод помещается в очередь, если у пула есть свободные рабочие потоки, метод извлекается из очереди и направляется  свободному потоку для выполнения. Если свободных потоков нет, но емкость пула не достигнута, для выполнения метода формируется новый рабочий поток, но с задержкой в 0.5 секунды (на случай, если освободится уже созданный поток). Несколько первых потоков в пуле, однако, создаются без задержки. 

Для работы с пулом используется класс ThreadPool. Метода SetMaxThreads() позволяет изменить емкость пула (по умолчанию 25*число процессорных ядер), а SetMinThreads() устанавливает минимальное число потоков (по умолчанию = числу процессорных ядер). Для помещения метода в пул потоков используется метод QueueUserWorkItem(). Он принимает делегат типа WaitCallBack и, возможно, параметр инкапсулируемого метода. 

public static void Main()
{
    ThreadPool.QueueUserWorkItem(Go);
    ThreadPool.QueueUserWorkItem(Go, 123);
    Console.ReadLine();
}

private static void Go(object data)
{
    Console.WriteLine("Hello from the thread pool! " + data);
}
Атрибуты

Атрибуты позволяют определить дополнительную информацию в метаданных, связанных с типом, и предоставляют механизм для обращения к этой информации в ходе компиляции и выполнения программы. 

Можно задать в квадратных скобках несколько атрибутов через запятую. Если возникает неоднозначность трактовки цели атрибутов нужно указать перед именем атрибута префикс - assembly, module, field, event, method, param, property, return, type. 

[assembly:AssemblyKeyFile]

Атрибут это класс System.Attribute, а применение его соответствует созданию объекта, поэтому после имени в круглых скобках могут задаваться параметры конструктора (для конструктора без параметров скобки можно не писать).

При применении атрибута наряду с аргументами конструктора можно указать именованные параметры, предназначенные для задания открытого поля или свойства. Они всегда записываются в конце списка аргументов конструктора. 

.net содержит много готовых атрибутов, но возможно создание и пользовательских атрибутов. Для этого класс должен удовлетворять следующим требованиям:

  • потомок System.Attribute
  • название должно заканчиваться на Attribute
  • тип открытых полей и свойств ограничен набором:bool, byte, char, short, int, long, float, double, string, System.Type, перечисления, object и одноименные массивы перечисленных выше типов. 
public class AuthorAttribute : Attribute
{
    public string Name { get; private set; }
    public string CreationDate { get; set; }

    public AuthorAttribute(string name)
    {
        Name = name;
    }
}

[Author("Developer")]
public class A { . . . }

[Author("Developer", CreationDate = "01.01.2010")]
public struct B { . . . }

Text
Photo
Quote
Link
Chat
Audio
Video