Лабораторна робота 5

Рефлексія та багатопотоковість

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

За допомогою засобів WPF розробити програму графічного інтерфейсу користувача, яка призначена для побудови графіку визначеної функції та знаходження її коренів (з певною точністю). Користувач повинен увести дійсні значення a і b, функції f(x) і g(x) у вигляді рядків, які відповідають синтаксису C# і можуть включати арифметичні операції, дужки, виклик стандартних функцій, умовну операцію тощо. Алгоритм знаходження коренів полягає в послідовному переборі з певним кроком точок інтервалу, знаходженні інтервалів, на яких функція h(x) змінює знак і виведенні середніх арифметичних початків та кінців інтервалів – коренів рівняння. Обчислення функції h(x) здійснюється відповідно до індивідуального завдання:

Номери варіантів
Функція h(x)
Номери варіантів
Функція h(x)
1
13
af(x) – b∙g(x)
7
19
f(a + x) + b∙g(x)
2
14
f(x + a) + g(x – b)
8
20
f(a / x) – g(b∙x)
3
15
(a-f(x))(b + g(x))
9
21
f(x – a) ∙g(x + b)
4
16
f(a∙x) – g(b∙x)
10
22
f(a / x) + g(b / x)
5
17
f(x / a)∙ g(x + b)
11
23
a∙f(x) + b∙g(x)
6
18
f(a / x) – g(b / x)
12
24
af(x)g(b∙x)

Користувач також повинен мати можливість вводити діапазон пошуку й точність та отримувати графік і корені. Пошук коренів слід виконувати в окремому потоці виконання. Реалізувати можливість призупинення, відновлення потоку, а також повного припинення і повторного обчислення з новими даними.

Примітка: задання повинно бути реалізоване для роботи на платформі .NET Framework.

1.2 Обчислення π за допомогою асинхронних делегатів

Реалізувати програму обчислення π с точністю до заданого ε як суму послідовності:

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

Реалізувати підхід, побудований на використанні асинхронних делегатів.

1.3 Обчислення π з використанням класу Thread

Реалізувати попередню задачу з використанням об'єкта класу Thread.

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

Створити консольний застосунок, в якому в одному потоці обчислюється добуток елементів деякого масиву і заноситься в статичне поле, а в іншому потоці обчислюється сума елементів того ж масиву і заноситься в інше статичне поле, після чого в другому потоці обчислюється і виводиться середнє арифметичне знайдених добутку й суми. Для синхронізації використовувати метод Join().

1.5 Створення WPF-застосунку для отримання простих множників чисел (додаткове завдання)

За допомогою засобів WPF розробити застосунок графічного інтерфейсу користувача, в якому користувач вводить діапазон чисел (від і до), а у вікні відображаються числа і їх прості множники. Реалізувати можливість призупинення, відновлення потоку, а також повного припинення і повторного обчислення з новими даними.

2 Методичні вказівки

2.1 Рефлексія

Відображення або рефлексія (reflection) – це механізм, який дозволяє програмі відстежувати і модифікувати власну структуру та поведінку під час виконання. Інформація про типи, яка може бути отримана через механізм рефлексії, міститься в метаданих складання. Мова C# надає класи Assembly, MemberInfo, MethodInfo, PropertyInfo, FieldInfo та інші типи простору імен System.Reflection.

Під час виконання дані про тип можна отримати, вказавши рядок з повною назвою типу (включаючи простір імен і вкладені простори імен):

int k = 100;
Type type = Type.GetType("System.Int32");
MemberInfo[] members = type.GetMembers();
foreach(MemberInfo member in members)
{
    Console.WriteLine(member);
}

Результатом роботи програми буде відносно великий перелік полів, методів (включаючи статичні) і властивостей, визначених у структурі System.Int32 і її базових типах.

Для отримання інформації про тип можна також створити змінну.

int k = 100;
Type type = k.GetType();
Console.WriteLine(type); // System.Int32

Можна окремо отримати інформацію про методи, поля і властивості:

FieldInfo[] fields = type.GetFields();
MethodInfo[] methods = type.GetMethods();
PropertyInfo[] properties = type.GetProperties();

Як і в Java, за допомогою рефлексії можна створювати об'єкти типів, імена яких визначаються рядком. Можна викликати методи, працювати з полями та властивостями через імена, визначені під час виконання програми. Наприклад, так можна створити екземпляр визначеного класу (MySpace.MyClass) і завантажити викликати його метод (MyFunc):

// Створюємо екземпляр (об'єкт) класу:
object o = assembly.CreateInstance("MySpace.MyClass");
// Отримуємо інформацію про тип:
Type t = o.GetType();
// Отримуємо інформацію про метод з указаним ім'ям: 
MethodInfo mi = t.GetMethod("MyFunc");
// Викликаємо метод з параметром x:
object result = mi.Invoke(o, new object[] { x });;

Засоби рефлексії C# дозволяють нам, окрім публічних даних, властивостей і методів, отримувати також інформацію про приватні елементи типів. Наприклад, так можна отримати інформацію про всі поля деякого типу:

FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

Оскільки через механізми рефлексії можна не тільки отримувати інформацію, а також змінювати значення полів, викликати методи тощо, рефлексія фактично дозволяє нам обходити обмеження інкапсуляції.

2.2 Метапрограмування у C#

Метапрограмування – це парадигма побудови коду інформаційної системи з динамічним зміною поведінки або структури залежно від даних, дій користувача або взаємодії з іншими системами. Метапрограмування передбачає створенням програм, які породжують інші програми як результат своєї роботи, або програм, які змінюють себе під час виконання.

У випадку розумного використання, метапрограмування забезпечує:

  • підвищення абстракції коду та його гнучкості,
  • покращення можливостей повторне використання,
  • прискорення розробки,
  • спрощення міжсистемної інтеграції.

Реалізація метапрограмування зазвичай базується на наявності в певній мові механізму рефлексії.

Інша складова метапрограмування – можливість програмного створення коду та його компіляції. Це, зокрема, класи CodeDomProvider, CompilerParameters та інші типи простору імен System.CodeDom.Compiler. В сполученні з рефлексією ці можливості можуть бути використані для повноцінного метапрограмування. У прикладі 3.1 створюється складання на підставі інформації, введеної користувачем під час виконання програми. Автоматично згенерований клас цього складання використовується в програмі через механізм рефлексії.

Примітка: засоби створення складання і компіляції доступні на платформі .NET Framework (не на .NET Core і .NET 5).

2.3 Концепції потоків виконання

Процес (process) – це екземпляр комп'ютерної програми, завантаженої в пам'ять. У сучасних операційних системах процес може виконуватися паралельно з іншими процесами. Процесу виділяється окремий адресний простір, причому цей простір фізично недоступний для інших процесів.

Потоки виконання (нитки, threads) використовують для реалізації незалежних підзадач у межах одного процесу з метою реалізації фонових процесів, моделювання паралельного виконання певних дій або підвищення зручності користувацького інтерфейсу.

Якщо процес створив кілька потоків, то всі вони виконуються паралельно, причому час центрального процесора (або декількох центральних процесорів у мультипроцесорних системах) розподіляється між цими потоками. Розподілом часу центрального процесора займається спеціальний модуль операційної системи, так званий планувальник (scheduler). Планувальник по черзі передає управління окремим потокам, так що навіть в однопроцесорній системі створюється ілюзія паралельної роботи завантажених потоків. Розподіл часу виконується за перериваннями системного таймеру.

Потоки виконання використовуються для реалізації незалежних підзадач у межах одного процесу з метою реалізації фонових процесів, моделювання паралельного виконання певних дій. Механізм потоків – обов'язкова риса сучасних операційних середовищ. За рахунок потоків забезпечується масштабованість програмних систем, оптимальне використання апаратних ресурсів, висока швидкість відгуку на запити користувачів. Техніка створення та взаємодії потоків управління отримала назву багатопотоковості (multithreading).

2.4 Реалізація мовою C# роботи з потоками виконання

2.4.1 Загальні концепції

Функцію управління багатопотоковістю CLR зазвичай делегує операційній системі. Для реалізації роботи з окремими нитками .NET Framework пропонує такі варіанти:

  • використання асинхронних делегатів
  • безпосередня робота з класом Thread
  • використання пулів потоків
  • використання класу BackgroundWorker
  • потоковий таймер

Є також інші способи, пов'язані з видаленими серверами та web-сервісами.

Для кожного потоку виконання створюється свій стек викликів. Основна проблема багатопотоковості – забезпечення обміну даними та синхронізації ниток.

2.4.2 Робота з асинхронними делегатами

Найпростіший шлях створення окремого потоку виконання є асинхронний виклик делегату. Асинхронний виклик означає, що виконання для методу буде створена окрема нитка. В такий спосіб можна викликати будь-яку функцію. Для асинхронного виклику функції необхідно створити тип делегату, який відповідає цій функції. Далі слід скористатися методом BeginInvoke(). Цей метод визначається автоматично під час створення типу делегату. Перші його параметри збігаються з параметрами функції, яку треба викликати. Останні два параметри забезпечують зворотний асинхронний виклик та будуть розглянуті нижче. Якщо зворотний виклик не реалізується, цим параметрам встановлюють значення null.

Метод BeginInvoke() повертає об'єкт, який реалізує інтерфейс System.IAsyncResult. Цей об'єкт можна використати для того, щоб, скажімо, перевірити, чи було завершено виконання асинхронного методу (властивість IsCompleted), або отримати деяку додаткову інформацію, наприклад, загальний час виконання методу.

Для того, щоб отримати результат методу, який був асинхронно виконаний, для об'єкта-делегату необхідно викликати функцію EndInvoke() з параметром – отриманим раніше об'єктом типу IAsyncResult.

Наприклад, описано такий тип делегату:

public delegate int SomeDelegate(double x);    

Описана також функція відповідає типу створеного раніше делегату:

static int F(double x)
{
    // обчислюємо і повертаємо певний результат
}    

Тепер можна викликати цю функцію асинхронно:

SomeDelegate func = F;
IAsyncResult ar = func.BeginInvoke(4.5, null, null);

Для виконання функції буде створено окремий потік виконання, а головний потік не буде чекати завершення функції F(). Він може виконувати далі свою роботу. Після того, як властивість IsCompleted отримає значення true, головний потік зможе використати результат функції F():

while (!ar.IsCompleted)
{
    // Виконання паралельної роботи в головному потоці
}
int result = func.EndInvoke(ar); // можна використовувати результат    

Мова C# також підтримує використання асинхронних делегатів зі зворотним викликом.

2.4.3 Робота з класом Thread

Підхід, побудований на використанні класу Thread, надає більш гнучкі можливості управління потоком. У найпростішому випадку об'єкт-нитка створюється за допомогою конструктору з параметром типу System.Threading.ThreadStart. Це тип делегату, який описує функцію з одним параметром і типом результату void:

public delegate void ThreadStart();

Будь-яка функція, яка відповідає описаному делегату, може бути використана для ініціалізації об'єкта типу Thread. Для запуску нитки на виконання слід скористатися методом Start(). Наприклад:

static void Hello() 
{
    Console.WriteLine("Hello, Thread!");
}
    
static void Main(string[] args)
{
    Thread t = new Thread(Hello);
    t.Start();
}

Після того, як потік створений, він може знаходитися в одному з можливих станів, яке визначає перелічення System.Threading.ThreadState. Зокрема, після того, як об'єкт створено, його стан встановлюється в ThreadState.Unstarted. Після виклику функції Start() стан змінюється на ThreadState.Running. Після завершення роботи потоку його стан встановлюється в ThreadState.Stopped. Для перевірки стану крім властивості ThreadState, можна використовувати властивість IsAlive, яка повертає true після виклику функції Start() і до завершення потоку. Кожен потік може бути виконаний лише один раз. Якщо спробувати вдруге викликати метод Start(), ми отримаємо виняток System.Threading.ThreadStateException.

Якщо виникає потреба в передачі даних потоку для його виконання, можна створити окремий клас, встановити необхідні значення даних цього класу, а потім передати конструктору один з методів цього класу. Більш простий спосіб – передати конструктору метод, який відповідає іншому стандартному делегату:

public delegate void ParameterizedThreadStart(object obj);

Тепер відповідні дані передаються як параметр функції Start(). Параметр може бути довільного типу, оскільки всі типи походять від object. Наприклад:

static void Hello(object msg) 
{
    Console.WriteLine((string) msg);
}
    
static void Main(string[] args)
{
    Thread t = new Thread(Hello);
    t.Start("Hello, Thread!");
}

Іноді потокам зручно надати імена. Зокрема, це зручно під час зневадження програми. Для призначення імені використовують властивість Name об'єкта типу Thread. Ім'я можна визначити лише один раз і потім не можна змінити. Отримати доступ до головної (поточної) нитки можна за допомогою статичної властивості CurrentThread класу Thread, наприклад:

Thread.CurrentThread.Name = "main";

Потоки бувають двох типів – основні та фонові. Основні потоки не дозволяють завершити роботу застосунку до завершення своїх основних функцій. Виконання функцій фонових потоків автоматично припиняється, коли закінчується робота головного потоку. Якщо потік виконання запущений за допомогою методу Start(), він автоматично створюється як основний. Для того, щоб перетворити його на фоновий, до виклику методу Start() необхідно властивості IsBackground об'єкта-нитки присвоїти значення true.

Можна визначати пріоритети потоків. Існує 5 градацій пріоритету потоку:

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

Встановлення пріоритету потоку на максимум ще не означає роботу в реальному часі, оскільки існують ще пріоритети процесів застосунків.

Винятки, які виникають в окремому потоці, повинні бути оброблені в том ж потоці, оскільки кожен потік використовує свій стек викликів.

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

Синхронізація – це механізм забезпечення явного управління порядком виконання коду, якщо завдання повинні виконуватися в певній послідовності. Метою синхронізації є запобігання проблемам, що виникають під час використання одного ресурсу одночасно декількома потоками, а також організація інформаційної взаємодії між окремими потокам виконання.

Важливою складовою синхронізації є механізм блокування. Заблокований потік негайно перестає отримувати час процесора, встановлює властивість ThreadState в WaitSleepJoin і залишається в такому стані, поки не розблокується.

Існує декілька способів здійснити блокування потоку. Потік можна призупинити за допомогою статичного методу Thread.Sleep(), аргумент якого – визначена кількість мілісекунд (або об'єкт TimeSpan). Цей метод повідомляє, що потоку не повинен виділятися час у зазначений період.

Потік можна заблокувати до завершення іншого потоку викликом методу Join(). Виклик цього методу всередині потоку t1 для потоку t2 обумовить призупинення поточного потоку (t1) до завершення t2, як показано в наступному прикладі:

using System;
using System.Threading;

namespace JoinTest
{
    class Program
    {
        static Thread t1, t2;

        static void RunFirst()
        {
            Console.WriteLine("First запущений");
            Thread.Sleep(1000);
            Console.WriteLine("Основна робота First закiнчена");
            t2.Join();
            Console.WriteLine("First завершено");
        }

        static void RunSecond()
        {
            Console.WriteLine("Second запущений");
            Thread.Sleep(3000);
            Console.WriteLine("Second завершено");
        }

        static void Main(string[] args)
        {
            t1 = new Thread(RunFirst);
            t1.Start();
            t2 = new Thread(RunSecond);
            t2.Start();
        }
    }
}

Проблеми, пов'язані з синхронізацією, унаочнює такий приклад. Припустимо, деякий метод встановлює протилежне значення деякій змінній стану. Припустимо також, що ми стартували дві нитки:

using System;
using System.Threading;

namespace StateChanger
{
    class Program
    {
        static bool state = false;

        static void ChangeState()
        {
            Console.WriteLine(state);
            Thread.Sleep(10); // Виконуємо певну роботу
            state = !state;
        }

        static void Main(string[] args)
        {
            new Thread(ChangeState).Start();
            new Thread(ChangeState).Start();
        }
    }
}

Програма виведе двічі False замість False і True, як ми очікували. Це пов'язане з тим, що одна нитка ще не встигла змінити значення state, а друга його вже вивела. Необхідно запровадити якийсь механізм, який не дозволяє виконувати функцію ChangeState() одночасно двома нитками. У найпростішому випадку це спеціальна конструкція

lock (об'єкт_синхонізації) 
{ 
    код 
}

Якщо код попереднього прикладу змінити, ми можемо отримати очікуваний результат:

using System;
using System.Threading;

namespace StateChanger
{
    class Program
    {
        static bool state = false;
        static object locker = new object();

        static void ChangeState()
        {
            lock (locker)
            {
                Console.WriteLine(state);
                Thread.Sleep(10); // Виконуємо певну роботу
                state = !state;
            }
        }

        static void Main(string[] args)
        {
            new Thread(ChangeState).Start();
            new Thread(ChangeState).Start();
        }
    }
}

Об'єкт синхронізації – це будь-який нелокальний об'єкт типу-посилання. Для нестатичних методів у якості об'єкта синхронізації рекомендовано використання посилання this.

Наведена вище конструкція – це спрощений механізм використання монітору. Монітор (Monitor) – це високорівневий механізм взаємодії і синхронізації процесів, що забезпечує доступ до неподільних ресурсів. Реалізацію функцій монітору забезпечує статичний клас System.Threading.Monitor. Якщо в попередньому прикладі клас використати явно, тіло функції ChangeState() виглядатиме так:

static void ChangeState()
{
    Monitor.Enter(locker);
    try
    {
        Console.WriteLine(state);
        Thread.Sleep(10); // Виконуємо певну роботу
        state = !state;
    }
    finally
    {
        Monitor.Exit(locker);
    }
}

Звичайно, в такому випадку найчастіше використовують конструкцію lock () { }. Але клас Monitor також надає інші можливості, зокрема методи Wait() (звільняє блокування об'єкта і блокує поточний потік доти, доки той не отримає блокування знову), Pulse() (повідомляє потік в черзі очікування про зміну стану об'єкта з блокуванням), PulseAll() (Повідомляє всі потоки, що очікують, про зміну стану об'єкта), та деякі інші.

Можна також керувати синхронізацією за допомогою атрибутів. Атрибут [Synchronization] описаний у просторі імен System.Runtime.Remoting.Contexts. Клас, якому передує ця анотація, повинен походити від System.ContextBoundObject:

using System.Runtime.Remoting.Contexts;
...

[Synchronization]
class ThreadSafeClass : ContextBoundObject
{
  ...
}    

Усі методи такого класу будуть потокобезпечними.

Для того, щоб призупинити потік, найкраще використовувати, метод Thread.Sleep() у сполученні з методом Interrupt(), який у стеку об'єкта потоку генерує виняток ThreadInterruptedException, що обумовлює переривання стану WaitSleepJoin. Наприклад:

using System;
using System.Threading;

namespace SleepInterruption
{
    class Program
    {
        static void Sleeper()
        {
            try
            {
                Console.WriteLine("Переходжу в стан сну");
                Thread.Sleep(Timeout.Infinite); // вічний сон
            }
            catch (ThreadInterruptedException)
            {
                Console.WriteLine("Прокинувся");
      
            }

        }

        static void Main(string[] args)
        {
            Thread t = new Thread(Sleeper);
            t.Start();
            Thread.Sleep(1000);
            t.Interrupt();
        }
    }
}

Можна також у циклі викликати метод Sleep() з перевіркою змінних стану.

Для зупинки та відновлення роботи потоку існують також методи Suspend() і Resume() класу Thread. Ці методи вважаються застарілими й небажаними, оскільки вони не дозволяють відслідковувати стан потоку й можуть призвести до взаємного блокування (deadlock).

2.5 Використання потоків виконання в застосунках графічного інтерфейсу користувача

У багатопотокових GUI-застосунках заборонено викликати методи і властивості елементів управління з потоків, відмінних від того, в якому вони були створені. Це обумовлено тим, що керування спільними візуальними компонентами з різних потоків може призвести до непередбачуваних наслідків. Через те у WPF-застосунках замість безпосереднього звернення до властивостей візуальних компонентів, слід використовувати методи, які викликатимуться з основного потоку, який отримує й обробляє події. Слід скористатися властивістю Dispatcher типу Dispatcher, яка присутня в об'єкті-вікні. Зокрема, функція диспетчеру CheckAccess() повертає true, якщо до компонентів можна звертатися безпосередньо. В іншому випадку викликаємо функцію Invoke(), яка також присутня в класі Dispatcher.

Альтернативний спосіб реалізації багатопотоковості – застосування компоненту BackgroundWorker. Цей компонент використовує керовану подіями модель багатопотоковості. Потік, який створює елементи управління виконує оброблювачі подій ProgressChanged і RunWorkerCompleted.

3 Приклади програм

3.1 Створення WPF-застосунку для побудови графіку довільної функції

Припустимо, нам необхідно засобами WPF створити програму для .NET Framework, яка дозволяє увести довільну функцію та побудувати її графік. Користувач уводить функцію в рядку введення (TextBox). Необхідно надати можливість уводити вираз довільної складності, який може включати арифметичні операції, дужки, виклик стандартних функцій, умовну операцію – взагалі усе що може бути використане у виразі, який повертає дійсне значення. Єдиною змінною може бути x, усі інші значення визначаються явними константами.

У середовищі Visual Studio створюємо новий проект – застосунок WPF App (.NET Framework) з ім'ям WPFGraph. Текст заголовку головного вікна (Title) змінюємо на "Побудова графіку довільної функції". Одночасно доцільно обмежити мінімальні розміри вікна (MinWidth та MinHeight), визначивши, наприклад, значення 464 та 115.

Головну сітку (Grid) слід розділити на дві горизонтальних частини (візуальними засобами або вручну):

<Grid.RowDefinitions>
    <RowDefinition Height="90*" />
    <RowDefinition Height="334*" />
</Grid.RowDefinitions>    

Для того, щоб верхня частина сітки мала фіксовану висоту, слід видалити зірочку після значення Height (у нашому випадку – після 90).

До верхньої частини сітки додаємо іншу сітку, для якої слід скинути значення властивостей Height, HorizontalAlignment, Margin, VerticalAlignment та Width у значення за умовчанням. Це можна зробити через контекстне меню властивостей (Reset Value), або шляхом видалення відповідних атрибутів зі XAML-коду. Для нової сітки одразу можна визначити сірий фон – встановити значення LightGray властивості Background. До нової сітки додаємо рядок введення (TextBox) з ім'ям TextBoxFunction, кнопку (Button) з ім'ям ButtonRedraw, дві однакових групи елементів (GroupBox), в кожній з яких розміщуємо дві мітки та два рядка введення для визначення діапазону графіку за кожній з координат. У текстові поля доцільно одразу записати які-небудь початкові значення діапазонів.

До нижньої частини форми додаємо компонент CanvasGraph типу Canvas. Це – звичайна панель, до якої, зокрема, можна додати графічні примітиви. Компоненту CanvasGraph теж слід скинути значення властивостей Height, HorizontalAlignment, Margin, VerticalAlignment та Width. Після необхідних налаштувань отримаємо XAML-код на кшталт такого:

<Window x:Class="WPFGraph.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFGraph"
        mc:Ignorable="d"
        Title="Побудова графіку довільної функції" 
        Height="460" Width="468" MinWidth="464" MinHeight="115">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="90" />
            <RowDefinition Height="334*" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" Background="LightGray">
            <TextBox Height="24" HorizontalAlignment="Left" Margin="6,6,0,0" 
                     Name="TextBoxFunction" VerticalAlignment="Top" Width="343" />
            <GroupBox Header="X" Height="51" HorizontalAlignment="Left" Margin="8,36,0,0" 
                      VerticalAlignment="Top" Width="220">
                <Grid>
                    <Label Content="Від" Height="32" HorizontalAlignment="Left" Margin="6,6,0,0" 
                           Name="LabelXFrom" VerticalAlignment="Top" Width="33" />
                    <TextBox Height="23" HorizontalAlignment="Right" Margin="0,6,112,0" 
                             Name="TextBoxXFrom" VerticalAlignment="Top" Width="62" Text="-5" />
                    <Label Content="До" Height="26" HorizontalAlignment="Left" Margin="110,6,0,0" 
                           Name="LabelXTo" VerticalAlignment="Top" Width="33" />
                    <TextBox Height="23" HorizontalAlignment="Left" Margin="138,6,0,0" 
                             Name="TextBoxXTo" VerticalAlignment="Top" Width="62" Text="5" />
                </Grid>
            </GroupBox>
            <GroupBox Header="Y" Height="51" HorizontalAlignment="Left" Margin="230,36,0,0" 
                      VerticalAlignment="Top" Width="220">
                <Grid>
                    <Label Content="Від" Height="32" HorizontalAlignment="Left" Margin="6,6,0,0" 
                           Name="labelYFrom" VerticalAlignment="Top" Width="33" />
                    <TextBox Height="23" HorizontalAlignment="Left" Margin="34,6,0,0" 
                             Name="TextBoxYFrom" VerticalAlignment="Top" Width="62" Text="-5" />
                    <Label Content="До" Height="26" HorizontalAlignment="Left" Margin="110,6,0,0" 
                           Name="LabelYTo" VerticalAlignment="Top" Width="33" />
                    <TextBox Height="23" HorizontalAlignment="Left" Margin="138,6,0,0" 
                             Name="TextBoxYTo" VerticalAlignment="Top" Width="62" Text="5" />
                </Grid>
            </GroupBox>
            <Button Content="Перемалювати" Height="25" HorizontalAlignment="Left" Margin="355,5,0,0" 
                    Name="ButtonRedraw" VerticalAlignment="Top" Width="94" />
        </Grid>
        <Canvas Grid.Row="1" Name="CanvasGraph" />
    </Grid>
</Window>

Після завантаження на виконання головне вікно майбутнього застосунку матиме такий вигляд:

Головною проблемою проекту є реалізація введення довільної функції. Для того, щоб розпізнати функцію у ряду, який увів користувач, у загальному випадку необхідно побудувати синтаксичний аналізатор. Реалізація синтаксичного аналізатору – це складна задача програмування, яка вимагає створення власного компілятору (інтерпретатору) складних виразів. Але завдяки можливостям рефлексії замість створення власного компілятору можна скористатися можливостями компілятору C#.

Додаємо до проекту новий клас (Project | Add Class...) з ім'ям FunctionFromString, код якого буде створено в файлі FunctionFromString.cs. Оскільки наш клас використовуватиме рефлексію, до using-директив слід додати підключення простору імен System.Reflection. Для використання компілятору придасться простір імен System.CodeDom.Compiler. Вихідний код матиме такий вигляд:

// FunctionFromString.cs
using System;
using System.Text;
using System.Reflection;
using System.CodeDom.Compiler;

namespace WPFGraph
{
    public class FunctionFromString
    {
        // Посилання на складання, яке буде створене програмно:
        private Assembly assembly = null;

        // Здійснює компіляцію програми, яка обчислює задану функцію
        public bool Compile(string str)
        {
            // Клас, який надає можливості компіляції:
            CodeDomProvider icc = CodeDomProvider.CreateProvider("CSharp");

            // Параметри компілятора:
            CompilerParameters cp = new CompilerParameters();
            cp.ReferencedAssemblies.Add("system.dll"); // підключаємо складання
            cp.CompilerOptions = "/t:library"; // створюємо бібліотеку
            cp.GenerateInMemory = true; // створюємо складання у пам'яті       

            // Створюємо рядок, який містить вихідний код класу Func
            StringBuilder sb = new StringBuilder("");
            sb.Append("using System;\n");
            sb.Append("namespace Func{ \n");
            sb.Append("public class Func{ \n");
            sb.Append("public double MyFunc(double x){\n");
            // З функції MyFunc повертаємо вираз, отриманий у вигляді рядку:
            sb.Append("return " + str + "; \n");
            sb.Append("} \n");
            sb.Append("} \n");
            sb.Append("}\n");

            // Здійснюємо компіляцію:
            CompilerResults cr = icc.CompileAssemblyFromSource(cp, sb.ToString());
            if (cr.Errors.Count > 0)
            {
                return false;
            }
            assembly = cr.CompiledAssembly;
            return true;
        }

        public double? F(double x)
        {
            if (assembly == null)
            {
                return null; // Складання не було створене
            }
            // Створюємо екземпляр (об'єкт) класу:
            object o = assembly.CreateInstance("Func.Func");
            // Отримуємо інформацію про тип:
            Type t = o.GetType();
            // Отримуємо інформацію про метод з указаним ім'ям: 
            MethodInfo mi = t.GetMethod("MyFunc");
            // Викликаємо метод з параметром x:
            object result = mi.Invoke(o, new object[] { x });
            return (double)result;
        }

    }
}

Тепер можна повернутися до створення застосунку графічного інтерфейсу користувача. Слід додати функції-оброблювачі подій, пов'язаних з натисненням кнопки ButtonRedraw та зміною розмірів компоненту CanvasGraph:

    private void ButtonRedraw_Click(object sender, RoutedEventArgs e)
    {

    }

    private void CanvasGraph_SizeChanged(object sender, SizeChangedEventArgs e)
    {

    }    

Малювання здійснюється шляхом додавання до панелі геометричних фігур, зокрема, ліній. Створюємо функцію DrawGraph(). Текст файлу MainWindow.xaml.cs матиме такий вигляд:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WPFGraph
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private bool draw = false;
        private FunctionFromString function = new FunctionFromString();
        private double xMin = -5, xMax = 5, yMin = -5, yMax = 5;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void ButtonRedraw_Click(object sender, RoutedEventArgs e)
        {
            // Читання даних з елементів TextBox
            // Якщо текст не є числом, відтворюємо попередні значення:
            try   { xMin = double.Parse(TextBoxXFrom.Text); }
            catch { TextBoxXFrom.Text = xMin + "";          }
            try   { xMax = double.Parse(TextBoxXTo.Text);   }
            catch { TextBoxXTo.Text = xMax + "";            }
            try   { yMin = double.Parse(TextBoxYFrom.Text); }
            catch { TextBoxYFrom.Text = yMin + "";          }
            try   { yMax = double.Parse(TextBoxYTo.Text);   }
            catch { TextBoxYTo.Text = yMax + "";            }

            if (xMax <= xMin || yMax <= yMin) // Хибний діапазон
            {
                MessageBox.Show("Помилка визначення діапазону", "Помилка", MessageBoxButton.OK, MessageBoxImage.Error);
                draw = false;
                DrawGraph();
                return;
            }

            if (function.Compile(TextBoxFunction.Text)) // // Функція визначена вірно
            {
                draw = true;
                DrawGraph();
            }
            else
            {
                MessageBox.Show("Помилка у функції", "Помилка", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        private void CanvasGraph_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            DrawGraph();
        }

        // Додавання лінії визначеного типу
        private void AddLine(Brush stroke, double x1, double y1, double x2, double y2)
        {
            CanvasGraph.Children.Add(new Line() { X1 = x1, X2 = x2, Y1 = y1, Y2 = y2, Stroke = stroke });
        }

        // Додавання тексту
        private void AddText(string text, double x, double y)
        {
            TextBlock textBlock = new TextBlock();
            textBlock.Text = text;
            textBlock.Foreground = Brushes.Black;
            // Визначення координат блоку. "Приєднані" властивості 
            Canvas.SetLeft(textBlock, x);
            Canvas.SetTop(textBlock, y);
            CanvasGraph.Children.Add(textBlock);
        }

        // Основна функція малювання графіку з осями та сіткою
        private void DrawGraph()
        {
            CanvasGraph.Children.Clear();
            if (!draw)
            {
                return;
            }
            double width = CanvasGraph.ActualWidth;
            double height = CanvasGraph.ActualHeight;
            double xScale = width / (xMax - xMin);
            double yScale = height / (yMax - yMin);
            double x0 = -xMin * xScale;
            double y0 = yMax * yScale;

            // Сітка:
            double xStep = 1; // Крок сітки
            while (xStep * xScale < 25)
            {
                xStep *= 10;
            }
            while (xStep * xScale > 250)
            {
                xStep /= 10;
            }
            for (double dx = xStep; dx < xMax; dx += xStep)
            {
                double x = x0 + dx * xScale;
                AddLine(Brushes.LightGray, x, 0, x, height);
                AddText(string.Format("{0:0.###}", dx), x + 2, y0 + 2);
            }
            for (double dx = -xStep; dx >= xMin; dx -= xStep)
            {
                double x = x0 + dx * xScale;
                AddLine(Brushes.LightGray, x, 0, x, height);
                AddText(string.Format("{0:0.###}", dx), x + 2, y0 + 2);
            }
            double yStep = 1;  // Крок сітки
            while (yStep * yScale < 20)
            {
                yStep *= 10;
            }
            while (yStep * yScale > 200)
            {
                yStep /= 10;
            }
            for (double dy = yStep; dy < yMax; dy += yStep)
            {
                double y = y0 - dy * yScale;
                AddLine(Brushes.LightGray, 0, y, width, y);
                AddText(string.Format("{0:0.###}", dy), x0 + 2, y - 2);
            }
            for (double dy = -yStep; dy > yMin; dy -= yStep)
            {
                double y = y0 - dy * yScale;
                AddLine(Brushes.LightGray, 0, y, width, y);
                AddText(string.Format("{0:0.###}", dy), x0 + 2, y - 2);
            }

            // Вісі
            AddLine(Brushes.Black, x0, 0, x0, height);
            AddLine(Brushes.Black, 0, y0, width, y0);
            AddText("0", x0 + 2, y0 + 2);
            AddText("X", width - 10, y0 - 14);
            AddText("Y", x0 - 10, 2);

            // Малюємо функцію, якщо вона визначена:
            {
                Polyline polyline = new Polyline() { Stroke = Brushes.Red, ClipToBounds = true };
                if (function.F(-x0 / xScale) == null)
                {
                    return;
                }
                for (int x = 0; x < width; x++)
                {
                    double dy = (double)function.F((x - x0) / xScale);
                    if (double.IsNaN(dy) || double.IsInfinity(dy))
                    {
                        continue;
                    }
                    // Отримали "нормальне" число
                    polyline.Points.Add(new Point(x, y0 - dy * yScale));
                }
                CanvasGraph.Children.Add(polyline);
            }
        }

    }
}

Функція DrawGraph() використовує допоміжні функції додавання лінії та тексту AddLine() та AddText().

3.2 Використання асинхронних делегатів

Припустимо, необхідно створити функцію, яка обчислює π через знаходження точки мінімуму функції cos(x). Оскільки для знаходження мінімуму ми скористаємося прямим пошуком (проходження визначеного інтервалу з кроком, який, фактично, визначає точність знаходження π), обчислювальний процес може зайняти багато часу. Тому для нього ми створюємо окремий потік виконання, а в головному потоці користувач може виконувати інші дії, наприклад, обчислювати квадрати чисел. Програма матиме такий вигляд:

using System;

namespace Asynchronous
{
    public delegate int SomeDelegate(double x);

    public delegate double SearchProcedure(double from, double to, double eps); 
  
    class Program
    {
        static double GetPi(double from, double to, double eps)
        {
            double min = from;
            for (double x = from; x <= to; x += eps)
            {
                if (Math.Cos(x) < Math.Cos(min))
                {
                    min = x;
                }
            }
            return min;
        }

        static void Main(string[] args)
        {
            SearchProcedure proc = GetPi;
            Console.WriteLine("Починаємо обчислювати пi");
            IAsyncResult ar = proc.BeginInvoke(2, 4, 0.0000001, null, null);
            Console.WriteLine("Поки порахуємо квадрати");
            while (!ar.IsCompleted)
            {
                Console.Write("Уведiть число: ");
                double x = double.Parse(Console.ReadLine());
                Console.WriteLine("Квадрат числа: " + x * x);
            }
            double pi = proc.EndInvoke(ar);
            Console.WriteLine("Нарештi знайшли: " + pi);
        }
    }
}

Для кожного числа гарантовано буде обчислено й виведено квадрат, навіть якщо окрема нитка вже завершила роботу.

3.3 Створення застосунку WPF для обчислення і відображення простих чисел

Припустимо, необхідно розробити програму отримання простих чисел у діапазоні від 1 до певного значення, яке може бути досить великим. Для знаходження простих чисел будемо застосовувати найпростіший алгоритм послідовної перевірки усіх чисел від 2 до квадратного кореня з числа, що перевіряється. Така перевірка може зайняти багато часу. Для створення зручного інтерфейсу користувача доцільно використовувати окремий потік виконання, у якому здійснюється перевірка. Завдяки цьому можна буде призупиняти та відновлювати пошук, виконувати стандартні маніпуляції з вікном, включаючи його закриття до завершення процесу пошуку.

Створюємо новий проект WPF з ім'ям PrimeApp. Заголовок вікна змінюємо на "Прості числа". У головній сітці визначаємо три рівні (рядки):

<Grid.RowDefinitions>
    <RowDefinition Height="38"/>
    <RowDefinition Height="365*"/>
    <RowDefinition Height="30"/>
</Grid.RowDefinitions>

У нульовому рядку розташовуємо мітку з текстом "До:" і поле введення тексту TextBoxTo (з текстом "10000"). Також додаємо кнопки "Стартувати" (ButtonStart), "Призупинити" (ButtonSuspend), "Продовжити" (ButtonResume) и "Завершити" (ButtonFinish).

До рядку з індексом 1 додаємо поле редагування тексту (TextBoxPrimeNumbers). В останньому рядку (з індексом 2) розташовуємо індикатор виконання ProgressBar (ProgressBarPercentage). Файл MainWindow.xaml матиме такий вигляд:

<Window x:Class="PrimeApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PrimeApp"
        mc:Ignorable="d"
        Title="Прості числа"
        Height="468"
        Width="727">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="38"/>
            <RowDefinition Height="365*"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <Label Content="До"
               HorizontalAlignment="Left"
               Margin="10,10,0,0"
               VerticalAlignment="Top"/>
        <TextBox x:Name="TextBoxTo"
                 HorizontalAlignment="Left"
                 Height="23"
                 Margin="38,10,0,0"
                 TextWrapping="Wrap"
                 Text=""
                 VerticalAlignment="Top"
                 Width="90"/>
        <Button x:Name="ButtonStart"
                Content="Стартувати"
                HorizontalAlignment="Left"
                Margin="133,10,0,0"
                VerticalAlignment="Top"
                Width="111"
                Height="23"
                Click="ButtonStart_Click"/>
        <Button x:Name="ButtonSuspend"
                Content="Призупинити"
                HorizontalAlignment="Left"
                Margin="249,10,0,0"
                VerticalAlignment="Top"
                Width="111"
                Height="23"
                Click="ButtonSuspend_Click"/>
        <Button x:Name="ButtonResume"
                Content="Продовжити"
                HorizontalAlignment="Left"
                Margin="365,11,0,0"
                VerticalAlignment="Top"
                Width="111"
                Height="23"
                Click="ButtonResume_Click"/>
        <Button x:Name="ButtonFinish"
                Content="Завершити"
                HorizontalAlignment="Left"
                Margin="481,10,0,0"
                VerticalAlignment="Top"
                Width="111"
                Height="23"
                Click="ButtonFinish_Click"/>
        <TextBox x:Name="TextBoxPrimeNumbers"
                 Grid.Row="1"
                 VerticalScrollBarVisibility="Visible" />
        <ProgressBar x:Name="ProgressBarPercentage" Grid.Row="2" />
    </Grid>
</Window>

Для безпосереднього пошуку простих чисел доцільно створити окремий клас PrimeNumbers, який на кожному кроці викликає функцію, визначену делегатом з параметром цілого типу. Аналогічно викликаємо функцію, яка повинна відображати знайдені числа. Зворотний виклик дозволяє видалити залежність класу від будь-яких візуальних компонентів. Код класу матиме такий вигляд:

namespace PrimeApp
{
    public delegate void NumberSetter(int number);

    public class PrimeNumbers
    {
        public double To { get; set; }
        public NumberSetter AddNumber { get; set; }
        public NumberSetter SetPercentage { get; set; }
        public bool Stop { get; set; }

        public PrimeNumbers()
        {
            To = 0;
            AddNumber = null;
            SetPercentage = null;
            Stop = false;
        }

        public void FindPrimeNumbers()
        {
            for (int n = 2; n <= To; n++)
            {
                if (SetPercentage != null)
                {
                    SetPercentage((int)(n * 100 / To));
                }
                bool prime = true;
                for (int i = 2; i * i <= n; i++)
                {
                    if (n % i == 0)
                    {
                        prime = false;
                        break;
                    }
                }
                if (prime && AddNumber != null)
                {
                    AddNumber(n);
                }
                if (Stop)
                {
                    break;
                }
            }
        }
    }
}

У класі MainWindow додаємо посилання на клас PrimeNumbers.

Для забезпечення потокобезпечного доступу до візуальних компонентів викликаємо Dispatcher.CheckAccess() (з протилежним тлумаченням результату) і використовуємо Dispatcher.Invoke(). Додаємо оброблювачі подій. Отримуємо такий вихідний код класу MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace PrimeApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        PrimeNumbers prime;
        Thread numbersThread;
        bool paused;

        public MainWindow()
        {
            InitializeComponent();
        }

        void AddNumber(int number)
        {
            if (Dispatcher.CheckAccess())
            {
                TextBoxPrimeNumbers.Text += number + Environment.NewLine;
            }
            else
            {
                Dispatcher.Invoke(new NumberSetter(AddNumber), new object[] { number });
            }
        }

        void SetPercentage(int number)
        {
            if (!Dispatcher.CheckAccess())
            {
                Thread.Sleep(20);
                while (paused)
                {
                    Thread.Sleep(20);
                }
                Dispatcher.Invoke(new NumberSetter(SetPercentage), new object[] { number });
            }
            else
            {
                ProgressBarPercentage.Value = number;
                if (number >= 99)
                {
                    ButtonStart.IsEnabled = true;
                    ButtonSuspend.IsEnabled = false;
                    ButtonFinish.IsEnabled = false;
                    TextBoxPrimeNumbers.SelectionLength = 0;
                }
            }
        }

        private void ButtonStart_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                prime = new PrimeNumbers();
                prime.To = int.Parse(TextBoxTo.Text);
                prime.AddNumber = this.AddNumber;
                prime.SetPercentage = this.SetPercentage;
                ButtonStart.IsEnabled = false;
                ButtonSuspend.IsEnabled = true;
                ButtonFinish.IsEnabled = true;
                paused = false;
                TextBoxPrimeNumbers.Text = "";
                numbersThread = new Thread(prime.FindPrimeNumbers);
                numbersThread.IsBackground = true;
                numbersThread.Start();
            }
            catch
            {
                MessageBox.Show("Перевірте дані!", "Помилка");
            }
        }

        private void ButtonSuspend_Click(object sender, EventArgs e)
        {
            ButtonSuspend.IsEnabled = false;
            ButtonResume.IsEnabled = true;
            paused = true;
        }

        private void ButtonResume_Click(object sender, EventArgs e)
        {
            ButtonSuspend.IsEnabled = true;
            ButtonResume.IsEnabled = false;
            paused = false;
        }

        private void ButtonFinish_Click(object sender, EventArgs e)
        {
            prime.Stop = true;
            ButtonStart.IsEnabled = true;
            ButtonSuspend.IsEnabled = false;
            ButtonResume.IsEnabled = false;
            ButtonFinish.IsEnabled = false;
            TextBoxPrimeNumbers.SelectionLength = 0;
        }
    }
}

Як видно з наведеного тексту, програма може знаходитися в декількох станах. Стан визначається активністю тих чи інших кнопок.

4 Вправи для контролю

  1. За допомогою асинхронних делегатів реалізувати функцію обчислення мінімуму деякої функції на визначеному інтервалі.
  2. За допомогою класу Thread реалізувати функцію обчислення мінімуму деякої функції на визначеному інтервалі.
  3. Створити програму, яка демонструє можливості функції Join().
  4. Створити програму, яка демонструє можливості конструкції lock ().
  5. Створити програму, яка демонструє можливості призупинення та відновлення роботи потоків.

5 Контрольні запитання

  1. У чому полягає концепція метапрограмуання?
  2. У чому є зміст рефлексії?
  3. Як створити об'єкт, визначивши ім'я класу під час роботи програми?
  4. Як викликати метод, визначивши його ім'я під час роботи програми?
  5. Чим відрізняються процеси від потоків виконання?
  6. Як здійснюється робота з асинхронними делегатами?
  7. Як створити потік за допомогою класу Thread?
  8. Як передати дані функції, яка виконується в окремому потоці?
  9. Яка різниця між головними та фоновими потоками?
  10. Як визначити пріоритети потоків?
  11. У чому полягає проблема синхронізації потоків?
  12. Як призупинити виконання роботи потоку?
  13. Поясніть роботу конструкції lock ().
  14. Що таке монітор?
  15. Як призупинити й продовжити роботу потоку?
  16. Які є обмеження на використання багатопотоковості в застосунках графічного інтерфейсу користувача.

 

up