- Зміст статті Відомо, що додатки бувають однопоточні і багатопотокові. Single thread софт кодіть...
- Concurrent Queue
- висновок
Зміст статті
Відомо, що додатки бувають однопоточні і багатопотокові. Single thread софт кодіть легко і приємно: розробнику не треба замислюватися про синхронізацію доступу, блокування, взаємодії між нитками і так далі. У багатопотокової середовищі всі ці проблеми часто стають кошмаром для програміста, особливо якщо досвіду роботи з тред у нього ще не було. Щоб полегшити собі життя в майбутньому і зробити multi thread додаток надійним і продуктивним, потрібно заздалегідь продумати, як буде влаштована робота з потоками. Ця стаття допоможе твоєму мозку побачити правильний напрямок!
Асинхронне програмування набирає обертів. Користувачі люблять, коли софт виконує всі дії практично моментально, а UI працює плавно і без лагів. Як би не росла продуктивність заліза, але відповідати таким високим вимогам можна тільки за допомогою тредов і асинхронности, а це, в свою чергу, створює безліч дрібних і не дуже проблем, які доведеться вирішувати звичайним програмістам.
Для того щоб зрозуміти, що за підводні камені таїть в собі робота в багатопотокової середовищі, потрібно поглянути на наступний код:
Singlethread код int Foo () {int res; // Щось довго робимо і повертаємо результат retrun res; } // В основному потоці викликаємо Foo auto x = Foo (); // ...
У нас є функція Foo, яка виконує деякі дії і повертає результат. У головному потоці програми ми запускаємо її, отримуємо результат роботи і йдемо робити далі свої справи. Все добре, за винятком того, що виконання Foo займає досить тривалий час і її виклик в GUI-потоці призведе до замерзання всього інтерфейсу.
Це погано, дуже погано. Сучасний користувач такого не переживе. Тому ми, трохи подумавши, вирішуємо винести дії, що виконуються в нашій функції, в окремий потік. GUI НЕ залипне, а Foo спокійно відпрацює в своєму тред.
Асинхронний виклик функції
З виходом C ++ 11 жити стало простіше. Тепер для створення свого тред не треба використовувати складні API Майкрософт або викликати застарілу _beginthread. У новому стандарті з'явилася нативна підтримка роботи з потоками. Зокрема, зараз нас цікавить клас std :: thread, який є не чим іншим, як STL поданням потоків. Працювати з ним - одне задоволення, для запуску свого коду достатньо лише передати його в конструктор std :: thread у вигляді функціонального об'єкта, і можна насолоджуватися результатом.
Варто також відзначити, що ми можемо дочекатися, коли потік закінчить свою роботу. Для цього нам знадобиться метод thread :: join, який якраз і служить для цих цілей. А можна і зовсім не чекати, зробивши thread :: detach. Наш попередній однопотоковий приклад може бути перетворений в multi thread всього лише додаванням одного рядка коду.
Нить за допомогою std :: thread // ... auto thread = std :: thread (Foo); // Foo виконується в окремому потоці // ...
Здавалося б, все добре. Ми запустили Foo в окремому потоці, і, поки вона відпрацьовує, ми спокійно займаємося своїми справами, не змушуючи основний потік чекати завершення тривалої операції. Але є одне але. Ми забули, що Foo повертає якийсь результат, який, як не дивно, нам потрібен. Найсміливіші можуть спробувати зберігати результат в якусь глобальну змінну, але це не наш метод - занадто непрофесійно.
Спеціально для таких випадків в STL є чудові std :: async і std :: future - шаблонна функція і клас, які дозволяють запустити код асинхронно і отримати за запитом результат його роботи. Якщо переписати попередній приклад з використанням нових примітивів, то ми отримаємо приблизно наступне:
Пробуємо std :: async і std :: future // ... std :: future <int> f = std :: async (std :: launch :: async, Foo); // Працюємо далі в основному потоці // Коли нам потрібно, отримуємо результат роботи Foo auto x = f.get ();
У std :: async ми передали прапор std :: launch :: async, який означає, що код треба запустити в окремому потоці, а також нашу функцію Foo. В результаті ми отримуємо об'єкт std :: future. Після чого ми знову продовжуємо займатися своїми справами і, коли нам це знадобиться, звертаємося за результатом виконання Foo до змінної f, викликаючи метод future :: get.
Приклад використання std :: thread
Виглядає все ідеально, але досвідчений програміст напевно запитає: «А що буде, якщо на момент виклику future :: get функція Foo ще не встигне повернути результат своїх дій?» А буде те, що головний потік зупиниться на виклик get до тих пір, поки асинхронний код не завершиться.
Таким чином, при використанні std :: future головний потік може блокуватися, а може і ні. У підсумку ми отримаємо нерегулярні лаги нашого GUI. Наша мета - повністю уникнути таких блокувань і домогтися того, щоб головний тред працював швидко і без фризів.
Concurrent Queue
У прикладі з future :: get ми фактично використовували м'ютекс. Під час спроби отримання значення з std :: future код шаблонного класу перевіряв, чи закінчив свою роботу потік, запущений за допомогою std :: async, і якщо немає, то очікував його завершення. Для того щоб один потік ніколи не чекав, поки відпрацює інший, розумні програмісти придумали потокобезпечна чергу.
Будь-кодер знає такі структури даних, як вектор, масив, стек і так далі. Черга - це одна з різновидів контейнерів, що працює за принципом FIFO (First In First Out). Thread-safe чергу відрізняється від звичайної тим, що додавати і видаляти елементи можна з різних потоків і при цьому не боятися, що ми одночасно спробуємо записати або видалити що-небудь з черги, тим самим з великою часткою ймовірності отримавши падіння програми або, що ще гірше, невизначене поведінку.
Concurrent queue можна використовувати для безпечного виконання коду в окремому потоці. Виглядає це приблизно так: в потокобезпечна чергу ми кладемо функціональний об'єкт, а в цей час в робочому потоці крутиться нескінченний цикл, який на кожній ітерації звертається до черги, дістає з неї переданий їй код і виконує його. Щоб краще зрозуміти, можна поглянути на код:
Реалізація потокобезпечна черзі команд class WorkQueue {public: typedef std :: function <void (void)> CallItem; WorkQueue (): done (false), thread ([=] () {while (! Done) queue.pop () ();}) {} void PushBack (CallItem workItem) {queue.push (workItem); } // ... private: concurrent_queue <CallItem> queue; bool done; std :: thread thread; };
Тепер наш код виконується в черзі, в іншому потоці. Більш того, ми можемо відправити на виконання не тільки Foo, а й інші функції, які виконуються дуже довго. При цьому ми не будемо кожен раз створювати окремий thread для кожної функції.
Документація шаблонного класу std :: feature
Але ми знову забули про яке значення. Одним із способів вирішення цієї проблеми буде зворотний виклик. Ми повинні передавати в робочу чергу не тільки код, що вимагає виконання, а й callback-функцію, яка буде викликана обчисленим значенням в якості параметра.
Callback для повернення значення class WorkQueue {public: typedef std :: function <int (void)> WorkItem; typedef std :: function <void (int)> CallbackItem; WorkQueue (): done (false), thread ([=] () {while (! Done) queue.pop () ();}) {} void PushBack (WorkItem workItem, CallbackItem callback) {queue.push ([= ] () {auto res = workItem (); callback (res);}); } // ... private: typedef std :: function <void (void)> CallItem; concurrent_queue <CallItem> queue; bool done; std :: thread thread; };
Але тут слід пам'ятати, що зворотний виклик буде зроблений в робочому потоці, а не в клієнтському, тому заздалегідь слід подбати про безпечну передачу значення. Якщо все додаток побудовано на основі архітектури потокобезпечна черг, тобто у кожного об'єкта є своя чергу команд, то рішення даної проблеми стає очевидним - ми просто будемо доручати виконання зворотного виклику черзі, в якій працює об'єкт, що запитав у нас виконання Foo.
Ще один варіант використання concurrent queue
На словах це звучить досить просто, але на практиці доведеться вирішувати безліч проблем, які так чи інакше пов'язані з багатопоточність. Так як все відбувається асинхронно, то цілком може бути, що після закінчення виконання будь-якого коду об'єкт, що запитав це виконання, вже не буде існувати і, повертаючи йому значення обчислень, ми можемо зламати всю програму. Такі нюанси треба враховувати з самого початку, щоб на етапі проектування виключити всі подібні ситуації.
висновок
Асинхронне програмування нині в тренді. Призначені для користувача інтерфейси iOS і Windows Phone працюють плавно і без лагів якраз через те, що в їх основу закладені принципи, що дозволяють уникати блокувань потоків в очікуванні результатів роботи тих чи інших тривалих дій. І чим далі, тим більш яскраво буде виражено рух в сторону асинхронности роботи ПО.