Контекст
Хоча середовище виконання .NET може працювати на Linux з 2016 року, підтримка моделі пріоритетів Linux є мінімальною, і використання API пріоритетів .NET на Linux може призвести до несподіваних результатів. Крім того, поведінка API на Linux погано документована. Документація Microsoft не згадує нічого про Linux, а пошук за запитом “dotnet thread priority linux” в Google повертає лише кілька релевантних результатів, які майже виключно є запитами на SO або питаннями на GitHub. Я виявив ці проблеми під час міграції високопродуктивного застосунку на Linux і вирішив написати цю статтю як збірку знань, яких мені бракувало до міграції.
Чому вам слід звертати увагу на пріоритети
Почнемо з короткого прикладу на Windows.
Я створив дуже наївну програму, яка запускає два багатониткові та ресурсоємні підпроцеси. Ці два підпроцеси виконують однаковий код, але один має високий пріоритет, а інший — нормальний.
> PerfTester.exe
Starting process with priority Normal
Starting process with priority High
Process with High priority completed in 00:00:07.8437039
Process with Normal priority completed in 00:00:15.2958797
Як ви можете побачити, у навантаженій системі підвищення пріоритету процесу або потоку може суттєво вплинути на час виконання. Звісно, це не панацея, і не можна встановлювати високий пріоритет для всіх процесів і очікувати, що кожен з них буде виконуватись швидше.
Це ефективно лише тоді, коли можна ідентифікувати кілька критичних процесів на машині або кілька критичних потоків у процесі.
У спокійній системі підвищення пріоритету процесу має менший вплив, але все одно може допомогти зменшити перемикання контексту або міграцію процесора. Насправді, BenchmarkDotnet завжди намагається підвищити пріоритет процесу та потоку перед виконанням бенчмарків, щоб збільшити стабільність результатів.
.NET API пріоритетів
.NET надає доступ до пріоритетів процесів і потоків через властивості Process.PriorityClass та Thread.Priority.
Ці властивості можна читати або змінювати під час виконання програми:
var process = Process.GetCurrentProcess();
Console.WriteLine($"Поточний PriorityClass: {process.PriorityClass}");
process.PriorityClass = ProcessPriorityClass.High;
Console.WriteLine($"Новий PriorityClass: {process.PriorityClass}");
var thread = new Thread(SampleThreadProc)
{
Name = "SampleThread",
Priority = ThreadPriority.AboveNormal,
};
thread.Start();
Пріоритет потоку також доступний через властивість ProcessThread.PriorityLevel, яка, на диво, використовує інший перерахунок пріоритетів (ThreadPriorityLevel
замість ThreadPriority
).
API .NET відповідає пріоритетам планування Windows.
У Windows кожен процес має клас пріоритету, кожен потік має рівень пріоритету, а кінцевий пріоритет потоку, який також називається базовим пріоритетом, є комбінацією обох. Базовий пріоритет — це число в діапазоні від 1 (найнижчий пріоритет) до 31 (найвищий пріоритет).
У Windows операційна система може вирішити тимчасово підвищити пріоритет потоку.
Отже, ефективний пріоритет потоку може відрізнятися від базового пріоритету.
API .NET настільки тісно пов'язане з API Windows, що значення перерахувань ProcessPriorityClass
та ThreadPriorityLevel
є тими самими, що й відповідні константи Win32.
Користуючись API пріоритетів .NET у Windows, можна очікувати, що:
- Ефективний пріоритет потоку — це комбінація класу пріоритету процесу, рівня пріоритету потоку та необов'язкового підвищення пріоритету, визначеного системою.
- Зміна класу пріоритету процесу впливає на всі потоки (як на вже створені, так і на майбутні).
- Немає успадкування пріоритету, процеси та потоки завжди стартують з пріоритетом “Normal”, незалежно від пріоритету батька.
Є виняток для пріоритетів процесів.
Хоча в документації Windows про успадкування зазначено, що “Дочірній процес не успадковує наступне: клас пріоритету (…)”, насправді класи пріоритету Idle і BelowNormal успадковуються. Мабуть, це запобігає процесам "втікати" з низькими пріоритетами через запуск дочірніх процесів.
Пріоритет потоків у середовищі .NET
Налаштування пріоритету потоків, які ви створюєте та контролюєте, може бути корисним. Однак, ймовірно, вам слід уникати зміни пріоритетів потоків середовища .NET. Потоки середовища .NET включають управлінські потоки, такі як GC, finalizer або EventPipe, а також очевидно потоки пулу потоків.
Ви могли б тимчасово підвищити рівень пріоритету потоку пулу потоків, використовуючи патерн try / finally, який відновлює початковий рівень пріоритету наприкінці вашої обчислювальної операції.
Проте я ніколи не застосовував цей патерн самостійно.
Більшість потоків, створених середовищем .NET, мають нормальний пріоритет, з кількома винятками, такими як потік finalizer або потоки серверного GC, які мають вищий пріоритет на Windows.
Модель пріоритетів в Linux
Процеси та потоки
Розкладник Linux працює з потоками, які в термінології Linux називаються задачами (не плутати з задачами .NET, звісно). У Linux немає концепції пріоритетів на рівні процесів. Якщо інструмент показує пріоритет процесу, це, ймовірно, пріоритет основного потоку. Однак на Linux пріоритет потоку успадковується, тому якщо ви налаштуєте пріоритет запуску вашого процесу, усі потоки вашого додатку матимуть той самий пріоритет.
Оновлення пріоритету вашого “процесу” під час виконання може бути проблематичним в .NET.
Навіть якщо ви зміните пріоритет основного потоку на самому початку вашої програми, середовище виконання .NET вже створило багато керуючих потоків, які не будуть впливати на зміну пріоритету. Однак команди Linux та функції libc можуть націлюватися на групи процесів. Якщо ви використовуєте групу процесів як цільову для зміни пріоритету, всі потоки будуть впливати на цю зміну.
Пріоритети в Linux
Найпоширеніший спосіб змінити пріоритет потоку — це налаштувати його значення nice (або доброзичливість). Значення nice — це пріоритет у просторі користувача, що варіюється від -20 (найвищий пріоритет) до 19 (найнижчий пріоритет).
Ви можете встановити значення доброзичливості (niceness) вашого процесу перед його запуском за допомогою команди nice:
$ nice -n priority program arguments
Також ви можете змінювати значення доброзичливості (niceness) ваших потоків під час виконання за допомогою setpriority або команди renice.
У обох випадках ви можете:
- Змінити пріоритет окремого потоку, передавши його ідентифікатор потоку:
setpriority(PRIO_PROCESS, threadId, priority)
абоrenice -n priority threadId
- Змінити пріоритет усіх потоків, використовуючи групи процесів:
setpriority(PRIO_PGRP, processId, priority)
абоrenice -n priority -g processId
Зверніть увагу, що хоча опція групи процесів цікава, вона явно не є еквівалентною зміні класу пріоритету процесу на Windows, оскільки вона замінить пріоритет кожного потоку, що був встановлений раніше.
Під капотом ядро Linux використовує пріоритети в просторі ядра, які варіюються від 0 (найвищий пріоритет) до 139 (найнижчий пріоритет).
Пріоритети nice від -20 до 19 відображаються на пріоритети ядра від 100 до 139, а діапазон від 0 до 99 використовується для реального часу.
Ви також можете змінити пріоритет реального часу, використовуючи sched_setparam або команду chrt. Однак зміна пріоритету реального часу для ваших потоків ефективна лише з певними політиками планування.
Теоретично операційна система також може вирішити коригувати пріоритет у просторі ядра, як це робить Windows з підвищеннями пріоритету. Однак я сам ніколи не помічав цього, переглядаючи значення пріоритетів за допомогою htop
або ps
.
htop
зазвичай відображає значення nice, але пріоритет ядра відображається як20 + nice value
для пріоритетів у просторі користувача і простоRT
для реальних пріоритетів.ps
насправді може відображати пріоритет реального часу за допомогою опції-l
, але значення зміщене на 40 через історичні причини.
Це дуже заплутано для мене.
Політики планування
Ядро Linux підтримує кілька політик планування, які по-різному обробляють пріоритети.
За замовчуванням політика планування — це SCHED_OTHER
, іноді називається SCHED_NORMAL
. Це політика, яка не є реальним часом, заснована на чесному планувальнику, і вона підтримує лише пріоритети в просторі користувача, тобто: значення nice.
Linux також надає політики планування реального часу, як-от SCHED_FIFO
або SCHED_RR
. Ці політики підтримують пріоритети реального часу, які відображаються на пріоритети ядра від 0 до 99, і вони ігнорують значення nice.
Заглиблення в деталі політик планування явно виходить за межі цієї статті.
Наразі вам потрібно лише пам'ятати, що налаштування пріоритетів — це не так просто, і навіть звична команда nice може не мати ефекту у вашому середовищі.
Дозволи
Збільшення пріоритетів на Linux, чи то за допомогою команд пріоритетів, чи за допомогою відповідних функцій libc, вимагає певних привілеїв. Команди можна просто виконати з sudo
. Однак, ймовірно, вам не слід запускати свої додатки з sudo
, тому вам потрібно правильно налаштувати середовище для виклику функцій, як-от setpriority
, або для використання членів API пріоритетів .NET, які залежать від setpriority
. Наприклад, якщо ви використовуєте служби systemd, вам потрібно налаштувати AmbientCapabilities=CAP_SYS_NICE
, щоб дозволити вашому додатку збільшувати пріоритет nice.
На Linux потоки фіналізатора та серверного GC в середовищі .NET мають нормальний пріоритет, незалежно від того, чи працює ваш додаток з політикою планування реального часу або не реального часу, і навіть якщо ваш додаток працює з необхідними привілеями.
Мабуть, більшість додатків .NET працюють без підвищених привілеїв, тому збільшення пріоритету потоків середовища виконання не було б особливо корисним.
Вплив API пріоритетів .NET на Linux
На цьому етапі вже має бути зрозуміло, що існує важливий дизайн-відмінність між моделями пріоритетів Windows і Linux. Немає очевидного або правильного способу реалізувати API пріоритетів .NET на Linux, і розробникам середовища виконання, ймовірно, було важко це зробити.
Пріоритет на рівні процесу
На Linux властивість Process.PriorityClass
реалізована викликом setpriority(PRIO_PROCESS, processId, priority)
(джерело). Отже, це змінить привабливість основного потоку, що суттєво відрізняється від зміни пріоритету на рівні процесу.
Пріоритет раніше створених потоків залишатиметься незмінним (включаючи потоки управління .NET), і нові потоки будуть змінюватися лише в разі їх запуску з основного потоку.
Можливо, було б доречно викликати setpriority(PRIO_PGRP, processId, priority)
замість цього. Однак тоді всі потоки програми зазнали б впливу, що могло б перекрити раніше налаштовані пріоритети потоків. На мою думку, це все ще могло бути трохи більш доцільним, хоча це, звичайно, дуже спірне питання.
Пріоритет на рівні потоку (Thread-level priority)
Реалізація API пріоритету на рівні процесу в операційній системі, яка не підтримує пріоритети на рівні процесу, є очевидно складною задачею. Реалізація API пріоритету на рівні потоку (Thread-level priority) може бути значно простішою. Можливо, .NET API могло б використовувати універсальний тип пріоритету на рівні користувацького простору, який підтримується за замовчуванням політикою планувальника? Було б непогано (без жодних каламбурів).
На Linux властивість Thread.Priority
не базується на значенні niceness, вона реалізована через виклик sched_setparam
(джерело), що підтримується лише реальними політиками планування (real-time scheduling policies). Тому, якщо ваша програма не працює з власною політикою планування, встановлення властивості Thread.Priority
не матиме жодного ефекту. Існує відкрите питання на GitHub з цього приводу, тому реалізація може змінитися в майбутньому.
Маппінг пріоритетів (Priority mapping)
Дозвольте дати вам легку загадку: чи існує ситуація, коли цей код може мати ефект?
var process = Process.GetCurrentProcess();
if (process.PriorityClass == ProcessPriorityClass.High)
process.PriorityClass = ProcessPriorityClass.High;
Відповідь справді така: на Linux, точніше, якщо пріоритет nice основного потоку знаходиться в межах від -12 до -14.
У цій ситуації ефект від цього коду буде полягати в тому, що пріоритет основного потоку вашого процесу буде "нормалізовано" до -11. Як було сказано раніше, всі інші потоки збережуть свій початковий пріоритет.
Пояснення дуже просте.
Ось як значення niceness відображаються на значення ProcessPriorityClass
:
return
pri < -15 ? ProcessPriorityClass.RealTime :
pri < -10 ? ProcessPriorityClass.High :
pri < -5 ? ProcessPriorityClass.AboveNormal :
pri == 0 ? ProcessPriorityClass.Normal :
pri <= 10 ? ProcessPriorityClass.BelowNormal :
ProcessPriorityClass.Idle;
А ось як значення ProcessPriorityClass
відображаються на niceness:
switch (value)
{
case ProcessPriorityClass.RealTime: pri = -19; break;
case ProcessPriorityClass.High: pri = -11; break;
case ProcessPriorityClass.AboveNormal: pri = -6; break;
case ProcessPriorityClass.BelowNormal: pri = 10; break;
case ProcessPriorityClass.Idle: pri = 19; break;
}
Це питання виникає через невідповідність між перерахунком пріоритетів процесів .NET, який має лише 6 значень, і niceness, який має 40 можливих значень.
Існує також схожа невідповідність між перерахунком пріоритетів потоків .NET, який має 5 значень, і реальними пріоритетами планувальника (real-time priority) в Linux, який має 100 можливих значень.
Немає нічого неправильного в тому, як значення пріоритету конвертуються в API .NET.
Однак, вам варто пам'ятати про правила відображення пріоритетів при використанні API пріоритетів на Linux.
Підсумок
При використанні API пріоритетів .NET на Linux можна очікувати, що:
- Зміна класу пріоритету поточного процесу оновить пріоритет основного потоку. Це не вплине на інші потоки, але вплине на майбутні потоки, якщо вони будуть створені основним потоком.
- Читання поточного класу пріоритету процесу дасть вам неточне значення, оскільки кілька значень niceness відображаються в один і той самий рівень пріоритету .NET.
- Зміна пріоритетів потоків не дасть жодного ефекту, якщо тільки ваша програма не працює з власною політикою планування.
- Вашій програмі потрібні спеціальні привілеї для підвищення пріоритетів.
- Налаштування пріоритетів як для процесу, так і для потоку неможливе, оскільки пріоритети на рівні процесу не існують на Linux.
- На Linux, якщо потік A запускає потік B, то потік B почне з тим самим пріоритетом, що й потік A.
На Windows нові потоки завжди починаються з пріоритету Normal. - На Linux всі потоки виконання мають нормальний пріоритет, тоді як на Windows потоки фіналізатора та серверного GC мають вищий пріоритет.
Особисті думки та рекомендації
Я не думаю, що є щось принципово неправильне в тому, як API пріоритетів .NET реалізовано на Linux. API пріоритетів .NET було розроблено для Windows, і немає ідеального способу реалізувати його на Linux.
Однак, я замислююсь, чи не було б більш зрозуміло відзначити все API як тільки для Windows і створити окреме API тільки для Linux.
У будь-якому разі, якщо вам потрібно налаштувати пріоритети на Linux, ось кілька особистих рекомендацій:
- Уникайте оновлення властивості
Process.PriorityClass
під час виконання на Linux. - Уникайте оновлення властивості
Thread.Priority
під час виконання на Linux. - Розгляньте можливість запуску критичних процесів з вищим пріоритетом, використовуючи nice.
Звісно, використання команди nice безпосередньо може бути не рекомендованим способом в залежності від вашої конфігурації. Наприклад, якщо ваша програма розгорнута як сервіс systemd, вам слід використовувати властивість NICE.
- Розгляньте можливість явного виклику функцій libc для оновлення пріоритетів ваших потоків під час виконання на Linux.
Останнє слово
Я занурився в ці питання, оскільки мені потрібно було мігрувати високо-продуктивний додаток на Linux.
User is working with performance profiling and tuning, specifically with managing performance settings like priorities and CPU affinities.
Мені пощастило, оскільки цей додаток використовує дуже тонку абстракцію, яка називається "профілі продуктивності" (performance profiles), що дозволяє налаштовувати параметри продуктивності, такі як пріоритети та прив'язки до CPU, централізовано. Тому в кінцевому підсумку я лише написав власну логіку для Linux у коді профілю продуктивності. Наразі я твердо вірю, що виявлення платформи Linux та реалізація власної логіки є чистішим і більш явним способом, ніж покладатися на сумнівні ефекти API пріоритетів .NET.
Говорячи про прив'язки до CPU, думаю, це може стати хорошою темою для наступної статті!
Велике спасибі @rbouallou та @Lucas_Trz за відгуки.
Перекладено з: Linux process priorities for C# devs