Розділяй і володарюй. Як працюють процеси в Python

💡 Усі статті, обговорення, новини про Python — в одному місці. Приєднуйтесь до Python спільноти!

«Хороше життя — це процес, а не стан буття. Це напрямок, а не пункт призначення»
Carl Rogers

Привіт усім шукачам істини! Це продовження серії досліджень GIL та його впливу на багатопоточність і багатопроцесність у Python. У попередніх частинах ми вже розібралися, що таке GIL, як він працює, які операції він блокує, а які залишаються поза його владою. Також ми детально дослідили потоки як засіб для паралелізму у випадках, коли основним вузьким місцем є операції вводу-виводу (англ. I/O-bound operations). Тепер настав час поговорити про процеси — фінальний акорд цієї серії. З’ясуємо, як правильно їх застосовувати та в яких сценаріях вони розкривають свій потенціал найкраще.

— Створення процесу
— Як обрати кількість процесів
— Взаємодія та обмін даними
— Черги повідомлень
— Пайпи
— Спільна пам’ять
— Інші способи синхронізації даних
— Висновки

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

Частина 1: GIL у Python. Ключ до стабільності чи ворог продуктивності?
Частина 2: Python без блокувань. Як працюють потоки

Ми вже говорили про CPU-інтенсивні задачі та досліджували, чому потоки не можуть допомогти у питанні покращення продуктивності для задач з великими навантаженнями на процесор. Дійсно, потоки не є розв’язанням цього питання, а от процеси — те, що може допомогти.

У Python процес — це незалежна одиниця виконання програми, яка має власний простір пам’яті та ресурси, що робить їх стійкішими до проблем синхронізації порівняно з потоками, але обмін даними тут складніший.

Коли ми розмовляли про один або кілька потоків, вони всі працювали в рамках одного процесу. Говорячи про процеси, маємо на увазі паралелізм вже іншого рівня — паралелізм, пов’язаний з можливостями процесора комп’ютера.

Створення процесу

Коли у Python створюється новий процес, під капотом відбувається цілий ряд операцій. Спершу потрібно зрозуміти, що таке процес на рівні операційної системи.

Процес — це ізольований екземпляр програми, що виконується у власному адресному просторі та має власні ресурси, такі як пам’ять, файлові дескриптори та потоки управління.

У Python створення процесів відбувається через модуль multiprocessing, який використовує механізми операційної системи для ініціалізації нового процесу. Залежно від платформи, використовується різний підхід.

У Unix-системах, таких як Linux та macOS, створення процесу відбувається через системний виклик fork(), який створює копію батьківського процесу разом із його пам’яттю та середовищем. При цьому копія працює незалежно і може змінювати свій стан без впливу на батьківський процес. Однак сучасні Unix-системи реалізують механізм Copy-on-Write (абрев. COW), що означає, що пам’ять фактично не копіюється при створенні процесу, а лише позначається як спільна. Якщо дочірній процес змінює дані в пам’яті, тоді операційна система фізично копіює змінений блок, щоб уникнути конфлікту між процесами.

На Windows створення процесу працює інакше, оскільки fork() тут не підтримується. Натомість використовується метод spawn(), при якому новий процес стартується «з нуля» і не успадковує пам’ять батьківського процесу. Це означає, що при створенні процесу виконується новий екземпляр інтерпретатора Python, який завантажує весь необхідний код заново. Через це створення процесів у Windows є повільнішим порівняно з Unix-системами, адже немає оптимізації через Copy-on-Write.

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

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

Ключова особливість fork() полягає в тому, що він викликається лише один раз, але повертається двічі — один раз у батьківському процесі та один раз у дочірньому.

У батьківському процесі fork() повертає PID дочірнього процесу. Це дозволяє батьківському процесу знати, хто є його нащадком і працювати з ним, наприклад, чекати його завершення.

У дочірньому процесі fork() завжди повертає 0, оскільки він сам по собі не створює нових процесів. Це допомагає розрізняти, де знаходиться код — у батьківському чи дочірньому процесі.

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

import time
import multiprocessing

def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

def sequential_fibonacci(numbers: list[int]) -> list[int]:
    return [fib(n) for n in numbers]

def parallel_fibonacci(numbers: list[int]) -> list[int]:
    processes_num = multiprocessing.cpu_count()
    with multiprocessing.Pool(processes=processes_num) as pool:
        results = pool.map(fib, numbers)
    return results

if __name__ == '__main__':
    test_numbers = [35, 41, 40, 38]

    start_time = time.time()
    seq_result = sequential_fibonacci(test_numbers)
    seq_time = time.time() - start_time

    start_time = time.time()
    par_result = parallel_fibonacci(test_numbers)
    par_time = time.time() - start_time

    print(f'Час послідовного виконання: {seq_time:.2f} сек')
    print(f'Час паралельного виконання: {par_time:.2f} сек')
    print(f'Прискорення: {seq_time / par_time:.2f}x')

Розглянемо паралельне виконання на задачі з числами Фібоначчі. Одразу заувага — код слугує прикладом суто для теми паралельного виконання CPU-навантажених задач за допомогою процесів, тож у ньому навмисно не застосовано оптимізації підрахунку чисел Фібоначчі на кшталт кешування тощо.

Отже, програма порівнює швидкість обчислення чисел Фібоначчі рекурсивним методом у двох варіантах: послідовному та паралельному. Послідовний підхід просто обчислює значення для кожного числа у списку по черзі, має експоненційну складність O(2n). У паралельному варіанті використовується multiprocessing.Pool, щоб розподілити обчислення між усіма доступними ядрами процесора. Наприкінці програма вимірює час виконання обох підходів і виводить прискорення, отримане завдяки багатопроцесності.

Я побачила на екрані такі результати:

Час послідовного виконання: 31.24 сек
Час паралельного виконання: 16.90 сек
Прискорення: 1.85x

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

Поглянемо на завантаження процесора у момент виконання коду. На першому графіку бачимо, як виглядає навантаження до запуску коду. Загальне навантаження на ядра дуже низьке (переважно 0-2%). Лише деякі ядра мають незначні піки через фонові програми на моєму компʼютері, але всі вони знаходяться на рівні <10%. Процеси розподілені рівномірно між ядрами, немає явних перевантажень.

Тепер запустимо код та проаналізуємо результати. Спостерігається значне підвищення навантаження на всі CPU. Використання ядер досягає піків у 100%, що свідчить про ефективну паралельну обробку.

Навантаження змінюється динамічно, що може означати чергування задач між ядрами — планувальник ОС може перерозподіляти ресурси, або ж деякі процеси завершуються швидше, водночас інші залишаються активними.

Розглянемо другий графік пильніше. Згадаймо, у коді спочатку запускається послідовне виконання підрахунку чисел Фібоначчі, а після — паралельне з використанням процесів. На графіку навантаження ми можемо це побачити.

Перша частина графіка відповідає послідовному виконанню. Навантаження спостерігається на кількох ядрах, але тільки одне ядро працює на 100% у конкретний момент. Це пояснюється тим, що код не використовує паралелізм: всі обчислення йдуть один за одним в одному процесі. Планувальник ОС може переносити цей єдиний процес між ядрами, тому навантаження розподіляється нерівномірно. Отже, використовується тільки один процес, а решта ядер просто «чекають».

Друга частина графіка демонструє розподіл навантаження на CPU під час виконання підрахунку паралельно — з використанням процесів. Помітно рівномірне використання всіх ядер, оскільки процеси виконуються паралельно. Усі CPU мають піки навантаження, але жодне не працює на 100% увесь час. Є «хвилі» навантаження, що вказує на динамічний розподіл процесів.

По аналогії з ThreadPoolExecutor для потоків у випадку з процесами також існує інструмент, що автоматично управляє ресурсами — ProcessPoolExecutor. На відміну від Pool з multiprocessing, який має більш старий і менш гнучкий інтерфейс, ProcessPoolExecutor краще інтегрується із сучасним кодом.

from concurrent.futures import ProcessPoolExecutor
import math

def compute_factorial(number: int) -> int:
    return math.factorial(number)

if __name__ == '__main__':
    numbers = [100000, 200000, 300000, 400000, 500000]

    with ProcessPoolExecutor(max_workers=4) as executor:
        results = executor.map(compute_factorial, numbers)

Як обрати кількість процесів

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

Якщо задача потребує інтенсивних обчислень (англ. CPU-bound), то оптимальна кількість процесів зазвичай дорівнює або трохи менша кількості ядер процесора. Це можна визначити за допомогою multiprocessing.cpu_count(), як було показано для задачі підрахунку чисел Фібоначчі. Наприклад, якщо у вас 8-ядерний процесор, то можна використовувати 6-8 процесів, залишаючи трохи ресурсів для інших системних завдань.

Якщо використовувати всі ядра процесора без залишку, операційна система може почати гальмувати, оскільки не залишиться вільних ресурсів для фонових завдань. Це може спричинити зависання або зниження продуктивності не лише програми, а й усього комп’ютера.

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

Також у деяких випадках створення процесів може супроводжуватися контекстним перемиканням, яке займає ресурси. Якщо система перевантажена, ефективність може навіть знизитися через витрати на перемикання між процесами. Тому рекомендується залишати хоча б одне ядро вільним, наприклад, запускати cpu_count() - 1 процесів, або експериментально визначати оптимальну кількість.

Також варто враховувати обсяг доступної оперативної пам’яті, оскільки надмірна кількість процесів може призвести до значних витрат пам’яті, а, отже, зниження продуктивності.

Для експериментального підходу можна почати з кількості процесів, рівної min(cpu_count(), X), де X — це допустиме споживання пам’ятіта навантаження на систему, та поступово тестувати, змінюючи значення.

Виникає питання: чи можна, враховуючи кількість ядер та накладні витрати, що спричиняє використання процесів у коді, досхочу додавати їх кількість? Де буде та межа, що зупинить приріст продуктивності від збільшення кількості процесів?

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

Формула закону виглядає так:

Де:

S — це загальне прискорення;

P — частка коду, що НЕ можна розпаралелити;

N — кількість процесорів.

Якщо, наприклад, 90% коду може працювати паралельно, а решта 10% виконується послідовно, навіть при безмежній кількості процесів максимальне прискорення складе 10 разів.

Це має важливе значення в багатопроцесності, оскільки навіть якщо розподілити обчислення між великою кількістю процесів, залишковий послідовний код буде гальмувати загальну продуктивність. Окрім цього, памʼятаємо, кожен процес вимагає певних накладних витрат, таких як створення, управління пам’яттю і комунікація між процесами. Це означає, що збільшення кількості процесів не завжди приводить до лінійного прискорення, а іноді може навіть уповільнити виконання через витрати на синхронізацію.

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

Взаємодія та обмін даними

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

IPC (Inter-Process Communication) — це механізм, який дозволяє процесам взаємодіяти між собою, обмінюватися даними та координувати виконання.

Тут виділяють два основні підходи: обмін повідомленнями та спільну пам’ять. Обмін повідомленнями передбачає, що процеси передають дані один одному через спеціальні механізми, такі як черги, канали або сокети. Це безпечний, але трохи повільніший спосіб, оскільки дані копіюються між адресними просторами (памʼяттю) процесів. Спільна пам’ять, навпаки, дозволяє процесам безпосередньо працювати з однією і тією ж ділянкою пам’яті. Це дуже швидкий метод, оскільки не потребує копіювання даних, але він вимагає додаткових механізмів синхронізації для уникнення конфліктів між процесами (по аналогії з потоками — див. статтю частину 2).

Ще одна класифікація стосується рівня взаємодії процесів. IPC може бути локальним, коли процеси взаємодіють у межах однієї операційної системи, або мережевим, коли процеси працюють на різних пристроях і використовують для комунікації протоколи, такі як TCP/IP. Локальна взаємодія зазвичай швидша, оскільки немає мережевих затримок, тоді як мережевий IPC відкриває можливості для розподілених систем і мікросервісної архітектури.

Черги повідомлень

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

У Python черги для IPC реалізовані в модулі multiprocessing. Вони забезпечують потокобезпечний механізм передачі даних між процесами без необхідності використовувати блокування або семафори вручну. Черга створюється в основному процесі та передається як аргумент дочірнім процесам, які можуть додавати або отримувати з неї дані. При цьому використовується серіалізація через модуль pickle, що дозволяє передавати об’єкти, але додає деякі накладні витрати.

Використання multiprocessing.Queue() на рівні коду аналогічне зі вже розглянутим у попередній статті queue.Queue() для потоків. Але схожий інтерфейс не означає, що вони працюють однаково.

queue.Queue() використовується в потоках і забезпечує їхню синхронізацію за допомогою блокувань (threading.Lock). Вона дозволяє ефективно передавати дані між потоками в межах одного процесу без необхідності серіалізації, оскільки потоки мають спільний доступ до пам’яті. Це робить її швидшою та менш ресурсомісткою, але вона не підходить для паралельного виконання CPU-навантажених завдань через обмеження GIL.

multiprocessing.Queue() створена для міжпроцесного обміну даними й працює через пайпи (multiprocessing.Pipe). Кожен процес у Python має власний простір пам’яті, тож щоб розв’язати цю проблему multiprocessing.Queue() автоматично серіалізує дані перед передачею через pickle. Це дозволяє ефективно працювати з незалежними процесами, які виконуються на різних ядрах CPU, проте серіалізація додає накладні витрати, особливо при передачі великих об’єктів.

Пайпи

Пайпи в контексті міжпроцесної взаємодії (IPC) — це механізм передачі даних між процесами через спеціальні канали зв’язку. Пайп створюється у вигляді двох кінців — один процес записує дані send(), а інший читає recv(). Це забезпечує односторонній або двосторонній зв’язок між процесами. За замовчуванням пайп працює в режимі «point-to-point», тобто дані можуть передаватися лише між двома процесами. Якщо використовується двосторонній зв’язок, то обидва процеси можуть як відправляти, так і отримувати дані.

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

У Python для цього існує multiprocessing.Pipe(), який повертає два кінці пайпу conn1 і conn2. Один процес отримує conn1 і пише в нього, інший процес отримує conn2 і читає дані. Для безпечного використання можна закривати кінець пайпу, коли він більше не потрібен, щоб уникнути зависань процесів.

Тож батьківська функція створює канал за допомогою Pipe(). Після створення каналу він викликає fork() для створення дочірнього процесу. Залежно від реалізації дочірнього і батьківського процесу вони спілкуються один з одним.

Зауважимо, дані в пайпі можуть бути пошкоджені, якщо два процеси одночасно спробують читати або записувати в один і той самий кінець каналу. Але якщо процеси працюють з різними кінцями пайпу одночасно — дані будуть у безпеці.

import multiprocessing
import time
from multiprocessing.connection import Connection

def sender(conn: Connection, messages: list[str]):
    for msg in messages:
        print(f'Відправлено: {msg}')
        conn.send(msg)  # Відправляємо повідомлення через пайп
        time.sleep(1)  # Симуляція затримки
    conn.close()  # Закриваємо з'єднання після завершення передачі

def receiver(conn: Connection, num_messages: int):
    for _ in range(num_messages):
        msg = conn.recv()  # Отримуємо дані з пайпу
        print(f'Отримано: {msg}')
    conn.close()  # Закриваємо з'єднання після завершення отримання даних

if __name__ == '__main__':
    parent_conn, child_conn = multiprocessing.Pipe()
    messages = ['Привіт!', 'Просто хотіла сказати,', 'що Харків - залізобетон!', 'Дякую за увагу!']

    sender_process = multiprocessing.Process(target=sender, args=(parent_conn, messages))
    receiver_process = multiprocessing.Process(target=receiver, args=(child_conn, len(messages)))

    sender_process.start()
    receiver_process.start()

    sender_process.join()
    receiver_process.join()

Розглянемо дуже простий приклад для демонстрації роботи пайпів. Основна ідея полягає в тому, що пайп створює канал зв’язку між двома кінцями (parent_conn і child_conn), де один процес пише в один кінець, а інший читає з другого.

Функція sender() проходить по списку повідомлень, кожне з яких надсилається у пайп через conn.send(). Функція receiver() знає заздалегідь, скільки повідомлень вона повинна отримати, проходиться по ним та виводить на екран.

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

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

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

Спільна пам’ять

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

У Python модуль multiprocessing надає засоби для використання спільної пам’яті через multiprocessing.shared_memory або multiprocessing.Value і multiprocessing.Array. Вони дозволяють створювати змінні або масиви, доступні для всіх процесів, які працюють у межах однієї програми.

import multiprocessing
import time
from multiprocessing.sharedctypes import Synchronized, SynchronizedArray
from multiprocessing.synchronize import Lock

def modify_shared_memory(shared_value: Synchronized, shared_array: SynchronizedArray, lock: Lock):
    for i in range(len(shared_array)):
        with lock:  # Захист від одночасного доступу
            shared_value.value += 1
            shared_array[i] += 1
            print(f'Процес {multiprocessing.current_process().name}: shared_value={shared_value.value}, shared_array={list(shared_array)}')
        time.sleep(0.5)  # Симуляція навантаження

if __name__ == '__main__':
    lock = multiprocessing.Lock()  # Використовується для запобігання конфліктам

    shared_value = multiprocessing.Value('i', 0)  # 'i' означає ціле число, початкове значення 0
    shared_array = multiprocessing.Array('i', [1, 2, 3, 4, 5])  # Масив цілих чисел

    processes = [multiprocessing.Process(target=modify_shared_memory, args=(shared_value, shared_array, lock)) for _ in range(3)]

    for p in processes:
        p.start()

    for p in processes:
        p.join()

    print(f'Фінальне значення shared_value: {shared_value.value}')
    print(f'Фінальне значення shared_array: {list(shared_array)}')

У прикладі створюється multiprocessing.Value, що зберігає ціле число, і multiprocessing.Array, що містить масив цілих чисел. Кілька процесів змінюють спільну змінну shared_value.value та елементи масиву shared_array[i], збільшуючи їх на 1. Операції змін виконуються всередині with lock, що гарантує, що лише один процес має доступ до змінних у певний момент.

Після завершення всіх процесів у масиві кожен елемент збільшиться на 3, а shared_value отримає значення загальної кількості змін — 15.

Діаграма показує структуру пам’яті двох процесів та їхню взаємодію через спільну пам’ять (англ. shared memory).

  • Text: текстовий сегмент, містить машинний код виконуваної програми, тобто сам бінарний код. Це статична область пам’яті, зазвичай тільки для читання (щоб уникнути випадкової зміни коду). Вона однакова для всіх процесів, які виконують один і той же програмний файл.
  • Data: сегмент даних, використовується для зберігання глобальних і статичних змінних.
  • Heap: купа, використовується для динамічного виділення пам’яті, розширюється або скорочується під час виконання програми.
  • Stack: стек, використовується для локальних змінних і викликів функцій, працює за принципом LIFO (Last In, First Out).

І останнє — shared memory. Це виділений блок пам’яті, який обидва процеси можуть бачити одночасно. На діаграмі він позначений як «mapped», оскільки він відображений у віртуальний адресний простір обох процесів. Приклад даних у спільній памʼяті відповідає отриманим результатам у нашому останньому прикладі.

Тож спільна памʼять «відображається» у памʼяті кожного процеса. Це взагалі як? Відображення пам’яті (англ. memory mapping) — це техніка, при якій фізична або віртуальна пам’ять пов’язується з адресним простором процесу. Це дозволяє процесам працювати зі спільною пам’яттю, ніби це просто частина їхньої власної пам’яті.

Уявіть, що у двох людей є спільний документ у хмарі (наприклад, Google Docs). Кожен із них може бачити зміни в реальному часі, тому що працюють зі спільним ресурсом. У цьому випадку цей документ є «mapped memory» для обох людей.

Інші способи синхронізації даних

Коротенько поговоримо й про інші способи синхронізації даних між процесами.

Сокети є одним із найпоширеніших способів IPC. Якщо сказати просто, сокети — це абстракція, що забезпечує з’єднання між двома кінцевими точками. IPC через сокети зазвичай організовується у форматі клієнт-сервер. Один процес виступає сервером, створюючи сокет і чекаючи підключень. Інший процес підключається до цього сокета як клієнт. Сокети підтримують двосторонню передачу даних, що дозволяє процесам як надсилати, так і отримувати інформацію.

У контексті IPC виділяють два види сокетів:

  • TCP/IP сокети — для процесів, які можуть виконуватися як на локальному хості, так і на різних хостах;
  • Unix Domain Sockets (абрев. UDS) — для локальних процесів, що взаємодіють через файлову систему, де замість IP-адреси використовуються файлові шляхи, наприклад, /tmp/mysocket.

Ось маленький демонстраційний приклад роботи сокетів.

Сервер:

import socket

class Server:
    def __init__(self, host: str, port: int):

        self.host = host
        self.port = port

    def start(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind((self.host, self.port))

        sock.listen()
        print(f'Сервер розпочав роботу на порту {self.port}')

        while True:
            client, address = sock.accept()
            print(f'Встановлено зʼєднання з клієнтом {client} на порту {address}')
            message = 'Привід від сервера!!'
            client.send(message.encode())
            client.close()

if __name__ == '__main__':
    server = Server('127.0.0.1', 9999)
    server.start()

Клієнт:

import socket

class Client:
    BUFFER_SIZE = 1024

    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port

    def connect(self):
        client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_sock.connect((self.host, self.port))

        message = client_sock.recv(self.BUFFER_SIZE)
        client_sock.close()
        print(message.decode())

if __name__ == '__main__':
    client = Client('127.0.0.1', 9999)
    client.connect()

Я отримала такі результати зі сторони сервера:

Сервер розпочав роботу на порту 9999
Встановлено зʼєднання з клієнтом <socket.socket fd=4, family=2, type=1, proto=0, laddr=('127.0.0.1', 9999), raddr=('127.0.0.1', 50808)> на порту ('127.0.0.1', 50808)

Коли сервер слухає порт 9999, кожне клієнтське з’єднання відбувається через інший тимчасовий порт (raddr), дозволяючи серверу обслуговувати кілька клієнтів одночасно. Сокет, наприклад, fd=4, в Unix представлений як файловий дескриптор, тому серверу потрібні достатні ресурси для роботи з багатьма клієнтами одночасно.

Наступним способом синхронізації між процесами є бази даних. Для такого виду обміну даними процеси використовують загальну таблицю, куди один процес записує дані, а інший читає їх. Наприклад, у PostgreSQL можна створити таблицю для зберігання стану або повідомлень, а також використовувати механізми блокувань або стовпці зі статусами для уникнення конфліктів. Один процес записує нові дані або змінює статус запису, а інший періодично читає ці дані, наприклад, за допомогою SELECT-запитів із фільтром. Також можна налаштувати тригери в базі даних або використовувати PostgreSQL LISTEN чи NOTIFY для миттєвого сповіщення про зміни. Цей підхід дозволяє процесам обмінюватися даними навіть на різних машинах, якщо вони підключені до спільної бази.

І останній спосіб синхронізації між процесами, про котрий хотілося б сказати, це менеджер. Manager з модуля multiprocessing забезпечує високорівневий інтерфейс, який створює серверний процес, що дозволяє спільно використовувати Python-об’єкти (списки, словники, черги тощо) між різними процесами. Менеджер автоматично синхронізує доступ до цих об’єктів і забезпечує їхню безпеку. Ключова перевага Manager полягає в тому, що він дозволяє працювати зі звичними Python-об’єктами, не турбуючись про механіку блокування чи передачі даних між процесами. Всі операції над такими об’єктами відбуваються через проксі, який керується Manager.

Дуже простий приклад виглядає так:

from multiprocessing import Process, Manager

def worker(shared_list: list, value: int):
    shared_list.append(value)
    print(f'Процес додав значення {value}')

if __name__ == '__main__':
    manager = Manager()
    shared_list = manager.list()

    processes = []
    for i in range(5):
        p = Process(target=worker, args=(shared_list, i))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print(f'Результат у спільному списку: {list(shared_list)}')

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

Результат виконання коду, що я побачила на екрані:

Процес додав значення 1

Процес додав значення 0

Процес додав значення 3

Процес додав значення 2

Процес додав значення 4

Результат у спільному списку: [1, 0, 3, 2, 4]

Тобто Manager створює спільний список manager.list(), доступний усім процесам. Кожен процес додає свій елемент до цього списку, а після завершення роботи всіх процесів список містить результати від кожного з них.

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

Висновки

Отже, процеси в Python — це потужний інструмент для обходу обмежень GIL і ефективного використання багатоядерних процесорів. На відміну від потоків, кожен процес має власну пам’ять і незалежно виконує код, що робить їх ідеальним вибором для ресурсомістких обчислень або задач, які потребують ізоляції. Хоча створення та обмін даними між процесами потребує більших ресурсів, ніж у потоків, перевага в повній незалежності й масштабованості виправдовує ці витрати для багатьох сценаріїв.

На цьому цикл статей завершується. Хто ще не встиг ознайомитися з минулими частинами — ласкаво прошу.

Частина 1: GIL у Python. Ключ до стабільності чи ворог продуктивності?
Частина 2: Python без блокувань. Як працюють потоки

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

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

Усім, хто дійсно прочитав усі три статті серії та долучився до моїх роздумів про багатозадачність у Python, — щиро дякую та сподіваюсь, що матеріал був корисний та цікавий.

І памʼятайте, Python — це інструмент, а справжня магія завжди в руках розробника.
Дякую за увагу! Не прощаємось.

👍ПодобаєтьсяСподобалось28
До обраногоВ обраному19
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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

Чи від цього вже відмовились?

А нащо?
— якщо ви сайти робите (Django whatever), то мабуть використовуєте мултіпроцессінг через uvicorn
— якщо ви робите ML — усі ML ліби (torch, numpy, sentence transformers ітп) — відпускають GIL. А деякі навіть мають внутрішні thread pools (на рівні С extensions) щоб множить ваші матриці ефективно
— якщо ви намагаєтеся робити обробку даних саме на пайтон (без С backed extensions)... ну то добре але якщо це одноразовий скріпт/ноутбук (почекайте та випийте кави). Бо пайтон дуже повільний — увесь пайтон ML базується на концепції того, що Пайтон — це лише оркестратор який викликає оптимізовані С ліби

Скидаю капелюха й підтримаю таку правильну технічну тему,
лише додам що для «непосвячених» Пайтонівська (та Лінуксово/Юніксова) fork/IPCшна магія «яким дивом ми одразу викликаємо функцію нашого процесу в тому новому процесі» виглядає доволі «дико», особливо в тому контексті що віндовий CreateProcess то ніразу не fork і не створює «ідентичну копію процесу разом з усім» а «просто запускає екзешку»...,
й відповідно реалізація «тої краси» у Вінді (та в Макові, бо він юзає spawn) то імітація Лінукса з певними обмеженнями, де пайтонівський модуль вантажиться по новій і де «викликаний» target повинен бути серіалізовуємий за домомогою pickle...

Кожен метод IPC насправді заслуговує окремої статті, але, принаймні охочі тепер знають «куди розкопувати далі»...

А ще розкриття теми async/await могло б стати ідеальним завершенням серії (навіть попри жахливу здатність async інфікувати все але в умовах коли розплодити багато потоків чи процесів — занадто дорого, цей підхід з async/await може бути єдиним способом писати «людський», лінійний, послідовний код, візуально не шматуючи його на коллбеки й не бавлячись в «академічні» забавки з FSMками коли окремий «wait стейт» та івент на «кожен пчих» виклику чогось асинхронного)

Нарешті на доу, по спражньому технічні підвезли

Дійсно, потоки не є розв’язанням цього питання, а от процеси — те, що може допомогти.

Насправді на рівні ядра Лінукс та деякі інші ОС не розрізняють потоки та процеси. Для створення того та іншого використовується єдина структура даних, де відмінність тільки в тому, що у треда буде встановлений TID, та декілька інших полей, які залежать від аргументів переданих до fork/clone викликів. І CFS, і новий EEVDF, сприймають процеси та треди просто як «таски».

створення процесу відбувається через системний виклик fork(), який створює копію батьківського процесу разом із його пам’яттю та середовищем

Все ж таки цей процес дещо складніший. Та і fork в лінухі задепрекейчений і рекомендують використовувати vfork. Ну і сам процес створення процеси дещо складніший ніж те, що ви описали, а ви взагалі не написали, що відбувається після форку.

shared memory. Це виділений блок пам’яті, який обидва процеси можуть бачити одночасно.

Я б ще додав, що на відміну від Value та Array, shared_memory потрібно синхронізувати вручну, адже такий об’єкт не буде завернутий в лок для вас. Ну і за межами пайтона така штука не буде працювати, бо потрібно уже буде використовувати сісколи типу shmget(), shmat(). Як по мені, то Value та Array виглядає як жалюгідна спроба додати в пайтон атомарні операції.

fork в лінухі задепрекейчений і рекомендують використовувати vfork

Excerpt from linux recommendations on vfork(2)

Standard description
       (From  POSIX.1)  The  vfork()  function  has  the  same effect as fork(2), except that the behavior is undefined if ...

Linux description
       vfork(), just like fork(2), creates a child process of the calling process. For details ... see fork(2).

Linux notes  
       Some consider the semantics of vfork() to be an architectural blemish ...
       A call to vfork() is equivalent to calling clone(2) with flags specified as ...

HISTORY
       4.3BSD; POSIX.1-2001 (but marked OBSOLETE). POSIX.1-2008 removes the specification of vfork()

CAVEATS
       ... signal handlers can be especially problematic: if a signal handler that is invoked in the child of vfork() changes memory, those changes may result in an inconsistent process state

Так, я змішав тут все в одну кучу. Форк задепрекейчений не в самому лінуксі, а в сайтоні. Але бачив ще пости про те, що і в лінухі fork discouraged на користь vfork.

> vfork() differs from fork(2) in that the calling thread is suspended until the child terminates (either normally, by calling _exit(2), or abnormally, after delivery of a fatal signal), or it makes a call to execve(2).

Тобто “депрекейтити” нічого. vfork тільки дає маленьку поміч шедулеру, якщо дитина робить exec(), не більше того.

Але бачив ще пости

Якщо хтось писав про “депрекейтед”, він не розуміє, що пише.

BH: fork в лінухі задепрекейчений і рекомендують використовувати vfork
...
BH: ... в сайтоні. Але бачив ще пости про те, що і в лінухі fork discouraged на користь vfork

VN: vfork() differs from fork(2) in ...
VN: Якщо хтось писав про «депрекейтед», він не розуміє, що пише

скоріш усього у вебпрограмінгу просто плутаються (нп можуть плутати одночасно і os і що саме з fork/vfork може бути прямо прописано як deprecated, ніби posix obsolete/removed для того було ще недостатньо), але вірогідніше що просто не мають уяви про мануали. Тому цитувати мануали без роз’яснення що це і як їх юзати — мабуть неефективно в цьому випадку.

:))

щось у цьому є...

ага, і подальші «знаю все, [не]читав» — побачите — тільки це підтвердять

А взагалі кажучи з мануалами так трохи нечесно (не кажучи вже про вебективність Ж) — доводити/показувати щось вебпрограмерам які мабуть щей молодші ніж хтось в тих *ніксах працював — тому я краще візьму попкорн і подивлюсь)

vfork тільки дає маленьку поміч шедулеру, якщо дитина робить exec(), не більше того.

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

Не впевнений, що ви самі розумієте, про що пишете.

Нажаль там процес трішки складніший за це

Я казав про відмінність від fork. І явно вказав про наявність дочірного процесу. Ви якось «дивно» читаєте.

Не впевнений, що ви самі розумієте, про що пишете.

Відмінно розумію. Бо і сам використовував (на C), і реалізацію в ядрах дивився в деталях.

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

Я казав про відмінність від fork. І явно вказав про наявність дочірного процесу. Ви якось «дивно» читаєте.

Ні, ви зразу пишете про якусь «мінімальну допомогу шедулеру», що не правда. Бо в одному випадку буде задіяна система керування пам’яттю (це не шедулер), а в іншому виклик буде заблоковано доки дочірній процес не завершиться.

Відмінно розумію. Бо і сам використовував (на C), і реалізацію в ядрах дивився в деталях.

Ок, а я їх реалізацію построчно читав. А ще крім цього читав книжки по кернелу від Love, Kerrisk та Mauerer (цю ще в даний момент читаю).

Ні, ви зразу пишете про якусь «мінімальну допомогу шедулеру», що не правда. Бо в одному випадку буде задіяна система керування пам’яттю (це не шедулер), а в іншому виклик буде заблоковано доки дочірній процес не завершиться.

Перед цим ви писали:

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

У випадку vfork копія з copy-on-write не робиться. Якось ви плутаєтесь у показаннях:)

Ще раз: fork викликає створення copy-on-write клона (майже) всеї памʼяти процесу і створює процес із одного треду — копії того, що викликав. vfork блокує _тред_ (не процес) що його викликав і чекає або завершення, або exec().

Далі, твердження про те, що vfork() депрекейтить fork(), не має сенсу. Те копіювання досить дешеве, якщо ми не кажемо про процес товщиною з firefox, а навіть у випадку наступного exec() може бути багато що треба зробити. Підкрутити налаштування сигналів, наприклад, квоти-ліміти, поточний каталог, і все таке. Або навіть якщо exec() не виконався — зробити запис у лог про це, а не тільки _exit(127) — а це значить маніпулювання у памʼяті.
Тому має сенс казати тільки про специфічну оптимізацію, яка іноді корисна і про яку корисно думати, чи не зробити її у конкретному випадку. Чим товстіше батьківський процес — тим корисніше, тут згоден. Якщо вже написали код, і вміщується в логіку «в дочці тільки exec», то можна його використати. Тут, звісно, 100%.

Або навіть якщо exec() не виконався — зробити запис у лог про це, а не тільки _exit(127) — а це значить маніпулювання у памʼяті.

Який запис у лог?
Той лог, що локається окремим мютексом в парент процесі?...

А що як в момент форка в паренті інший срід якраз писав в саме той лог,
то дочірній «відфорканий» процес, отже отримає «все скопійоване» з тим залоканим мютексом (але вже без «того іншого» сріда який повинен би був відлокати мютекс), й «наш стрід тут» буде пробувати захопити в чайлді той мютекс для логів, який не буде ніколи відлокано?

То само, насправді й про решту закопійованих від парента штук, й що там з ними робили в паренті інші сріди, й в якому «недоробленому» стейті вони лишилися?...

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

Коротше fork та решта «історичної спадщини» від «кононічного Юліска» то є «ще ті» сюрпризи...

___________
Й та ж вінда з її нібито «позікс сумісністю» (доданою, очевидно, з маркетингових міркувань) теж підсовує свиню, всякі fopen та _wfopen ім треба обов’язково передавати флажох N... при тому сам _wfopen повертає не віндовий хендл, а FILE* й потім передавати FILE* в чайлд процес не має ніякого змісту, бо там можна зробити опен лише для хендла (наслідується ж хендл, відповідно вся фішка «наслідування» для АПІшки, що повертає FILE* — абсолютно юзлесс непотрібщина, що тільки додає граблів)...

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

Який запис у лог?
Той лог, що локається окремим мютексом в парент процесі?...

Чому? Може, просто syslog().

Коротше fork та решта «історичної спадщини» від «кононічного Юліска» то є «ще ті» сюрпризи...

O_CLOEXEC (відсутність його за замовчуванням) — зрозуміло, шкода, що не можна змінити зараз. А от fork — він дуже змістовно використовується.
А для тих, кому незручно, є posix_spawn().

Й та ж вінда з її нібито «позікс сумісністю»

Ой про це не треба, то окремий жах. Дуже окремий.

Я знаю проблеми Unix, можна ще з десяток навести. Але ж працюють і з цим...

Чому? Може, просто syslog().

це якщо знати про це й жити в Сшному світі, а не пробувати залогувати готовою модною лібою «як аже прийнято на проекті й до якох всі звикли»...
до речі той syslog він в третьому розділі man, отже це... бібліотечна функція... там точно нема мютекса всередині? чи таки є? (мені, чесно, зараз дивитися це по сорсах — влом, та й якщо зараз нема, то не факт, чи не допиляють потім... але я трохи про інше: кожна ось така «гавнюлічка», то черговий спосіб «зашпортатися» й додаткове джерело матюків та проклять щодо «недосконалого світу»)

А от fork — він дуже змістовно використовується

так, так, гнучкість і тд, але, все ж, може виявитися краще робити «це» в окремому «форксервері» ніб бавитися з усіма наслідками (що, втім, також окрема незручність і ще одне джерело проклять)

А для тих, кому незручно, є posix_spawn().

нє нє, там теж треба бавитися з posix_spawn_file_actions_addclose...
нето... для «рдного» форка closefrom є тільки в BSD (якщо я нічого не пропустив, але полюбе «недосконалий світ»)

Ой про це не треба, то окремий жах. Дуже окремий.

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

Я знаю проблеми Unix, можна ще з десяток навести. Але ж працюють і з цим...

ну от і я про те саме... куди не глянути — абсолютна печалька...
я не про міряння хто більше граблів знає та знайде,
а про те, що «в ідеальному світі так не повинно було б бути»!

syslog()

це якщо знати про це

а як можна працювати в *nix і не знати про syslog() ?

і не знати про syslog()

Так в нього можна писати за допомгою «бібліотеки»,
а можна взагалі писати логи «не туди» або «не тільки туди» за домомогою «портабл бібліотеки», і взагалі багато опцій, бо «в нас портабл аплікуха» й «все загорнуто» (і не обов’язково «ми» є авторами тих обгорток)...

та й нема й далі відповіді, а точно бібліотечна реалізація syslog(3) не юзає мютексів? Чи таки юзає? А може таки юзає?

__fsetlocking, той лок при «клонуванні всього» зберігається якщо ми будемо ломитися з дочірнього процесу чи ні? А інші задіяні в реалізацій __syslog_chk/__vsyslog_chk АПІшки мають мютекси/локи?

іншими словами «все класно» тільки на словах... реальний світ не відповідає ідеальностям

в нас портабл аплікуха

спочатку мова йшла про vfork (який вже прибрали з posix — це відносно портабл) — відносно якого хтось вирішив що якщо не
«fork в лінухі задепрекейчений» то хочби може «в лінухі fork discouraged на користь vfork».

— в лінукс щоб не знати про syslog то треба вебпрограмером бути — для яких більш ніж мулйон сюрпрайзів буде на цьому рівні (тому для них треба булоб окрему категорію заводити)

та й нема й далі відповіді, а точно ..

це питання (пояснювати) де що і як — до того хто пропонував то юзати — а не про знати щодо існування syslog.

— якщо взагалі відносно vfork — то достатньо прочитати bugs/caveats в мануалі, а відносно reasoning чому і для чого залишили цей obsolete інтерфейс в параграфі «there are various reasons why Linux and other systems have retained vfork(): ...»

реальний світ не відповідає ідеальностям

і це має заперечувати існування мануалів?

— в лінукс щоб не знати про syslog то треба вебпрограмером бути — для яких більш ніж мулйон сюрпрайзів буде на цьому рівні (тому для них треба булоб окрему категорію заводити на сайтах)

а що вебпрограмери не запускають процесів (зокрема й з штатним, звичайним, звичним, номальним miltiprocessing.щостотам), й «в цьому» не буває задіяним fork/vfork?

Чому вони повинні ходити по тих всіх граблять з лєвих непайтонівських «сюрпрайзів» просто юзаючи штатну АПІшку як пише мануал? Лєві граблі замість того, щоб просто вкодовувати те, що їм треба згідно ТЗ!

а не про знати щодо існування syslog

ні, це питання якраз про «знати», про хто й в якій аплікусі й шо саме пише «згідно ТЗ» в якісь файли...

і тепер виходить «нє ти тою своєю лібою в лог більше не пиши, тепер в Лінухах пиши тільки через syslog, але я насправді не знаю чи в ньому нема тої ж проблеми» (ми ж почали з способів створення підпроцесів та наслідків, от я й кажу fork не дружить срідами та з мютексами, це факт... і особисто я це розглядаю як дизайнерський про*об отих «легендарних батьків засновників платформи», що тепер нам «дістався в спадок»))

і це має заперечувати існування мануалів?

От наразі я не маю готовенького мануала, що відповідає на мої питання щодо писання логів в сабпроцесі))

Мануал пише
«The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects»

і що?

де тут про те, чи syslog гарантовано не поюзає мютекса, який внаслідував свій залоканий стейт від парента, й ніколи не розлокається?
якщо нема такої готовенької відповіді, то syslog проблему не вирішує...

я не хочу перелопчувати globc, musl, ще щось, щоб то з’ясувати))

в більшості випадків для «простого смертного» простіше «забити» на цей «корнер кейс» як «занадто рідкісний», придумати воркераунд, запитати AI та взяти перший попавший (неправильний) солюшен, або й просто ігнорувати/приховати/не визнавати проблему :)
(звісно, за винятком того кейсу, якщо той «простий смертний» і є той «цап відбувайло» кому треба інвестігувати саме таку проблему в особо обдарованому кейсі)...

й це все «придумати воркераунд» ще навіть не доходячи до «замінити нашу кастомну лібу на syslog, й що ще не факт, чи то поможе»...

і потенційно «не поможе» не лише через то, що умовні glibc/musl можуть «десь по дорозі» поюзати мютекс,
а ще й тому, що навіть поюзавши SysLogHandler, не факт, що нема мютексів в самих лібах Пайтона по дорозі від logger.info до того SysLogHandler... (ця штука не покрита мануалом, то що, тепер подивися сорси Пайтна 3.10, 3.11, 3.12 щоб то з’ясувати? та ну його в баню!)

Нагадую для Пайтона з Лінуховим fork-ом
для
multiprocessing.Process(target=чототам...

оте «чототам», воно згідно мануалу для fork бачить «відкопійовану пам’ять» та всі «states of mutexes, condition variables, and other pthreads objects» як їх полишив його пайтонівський парент, а не «повторно завантажений для імітації форка» модуль як у вінді.

Відповідно якщо в пайтонівському чайлді ми ломимося в той мютекс який вже трапився в пайтонівському паренті бути залоканим іншим срідом в момент fork у «нашому сріді», то в чайлді ми вже зависли «навіки», бо розлокувати його вже ніхто не буде!
Це — дефект бай дізайн операції fork, про який нічого не сказано в мануалах вебпрограмера про multiprocessing.Process!

І так, тоді set_start_method(’spawn’) рішає проблему швидко, бо без вроблення додаткового ефорта, то «а чорт його знає, що там насправді поламалося й чого в дочірньому процесі воно висне» при юзанні канонічного fork))

Але до чого це я? Та якраз до того, що тих «вебпрограмерів» взагалі не повинно колихати який там fork чи spawn чи forkserver та решта «мулйон сюрпрайзів буде на цьому рівні», бо ці всі проблеми в «ідеальному світі» вже давно мали б бути вирішені так, щоб не треба було нікого кликати розгрібати ті тонни їхньго «гавнокоду», що вони там в себе накодили на лєвому проекті, для того, щоб таки знйти спражній руткоз воркераунд, який ховається в розумінні тих Cшних АПІшок, в які «вебпрограмери» взагалі не мали б заглиблюватися :)

а що вебпрограмери ...

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

Решта — «як у вінді» — зовсім не цікаво коли говорять про юніксові інтерфейси (і С зовсім не Пайтон).

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

їхні уявлення згідно їхніх мануалів!
а в їхніх мануалах нічого не пише про «states of mutexes, condition variables, and other pthreads objects» та які то буде мати наслідки якщо заломитися в такий мютекс з чайлда, що виявився унаслідуваний залокним від парента :)

в них згідно мануалів «все класно», і юзають вони «як книжка пише» поки вони на ті «тонкощі» не напоролися))

Та блін, це ж Пайтон, «кросплатформенна штука», яку «всюди можна юзати однаково», зокрема й multiprocessing.Process (поки не наткнешся на ті всі тонкощі!)

Решта — «як у вінді» — зовсім не цікаво коли говорять про юніксові інтерфейси.

та ні, якраз цікаво, бо його можна заставити бути як у вінді за допомогою set_start_method(’spawn’)
))

Тоді
1. Важкорепродюсаємі граблі з форком та мютексом під Лінухами для «обдарованого кейсу» зникають, й тепер нічого не висне!
2. Код віддебаганий тою тімою, що працює на Лінухах, так де несумісність з spawn, він впаде ще на етапі розробки до того, як його спробують запустити на Вінді і одразу повилазить про те, що вони пхають в сабпроцес а воно «не піклиться»))

Проблему вирішено (навіть дві), і це прекрасно :)

Проблему вирішено (навіть дві)

пропоную на тому і завершити [не]дочекавшись поки... ШІ прийде і все поправить, включно і лінукси і вінди і сі і пайтони)

Запросто. Це не сучасно-модно-молодіжний стиль:)

Модно це log4j/log4cpp/etc. з конфігурацією апендера для ElasticSearch.

І ніт, це не вебпрограмінг як ви пишете поруч — це коли все в контейнерах.

так так і threading::ScopedLock lock поганяє threading::ScopedLock lock-ом... й так і кортить після форка поюзти щось готовеньке, що «і так видно» з нашого скоупа з «копії», а те «щось», воно (в загальному випадку) є «в якому завгодно стейті», зокрема й всередині операції, яка відбувалася в паренті в іншому сріді, й була «недороблена» поки ми робили fork, й тепер ми вже з копії хочемо до тої ж «недоробленої» штуки заломитися, щоб зробити «ще щось корисне» між frok та exec, або й після невдалого exec щось ще дологувати не в окремий лог, а в той, що «і так видно»...

тільки не лише з log4cpp, а й з канонічним та трушним syslog в загальному випадку теж нема певності чи не буде граблів саме з варіаннтом коли парент туди пише з багатьох срідів, а ми робимо fork...

тому опенлог для чайлда треба робити по новій, але це ж напевно попередній лог в тому чайлді треба закрити, нє? чайлд же скопіювався «з усім», в тому числі й з попереднім логом що був відкритим openlog ще в паренті, нє? а АПІшка закриття вона ж лізе в той мютекс, чи ні?

_____________
syslog, взагалі, точно не катить, бо
«A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called.»

і далі є список що то за async-signal-safe, які АПІшки дозволені

й там нема ніякого syslog, ні АПІшок його закривання/відкривання для того лога...))
тобто не можна юзати не лише «модні подєлки», а й канонічний syslog після fork... і навіть якщо з «няшної Сшки» згідно спеки!
Як до того всього повинен дійти «простий вебдевелопер»?

______________________
й такі ситуації, насправді, ніразу не весело, бо природні очікування для «простого смертного» що хоче просто та зручно поюзати
multiprocessing.Process(target=чототам...
вони зовсім не такі, без сподівання таких граблів для звичних операцій...

людина ж просто пише
def чототам():
і отут сподівається, ніби можна поюзати все оте,
що тут же в своєму ж модулі,
й що видно вище в цьому ж самому файлі!...

але не можна, якщо парент малтісрідовий та юзається fork,
бо воно «внутрях» все одно все покладається на ту ж Сшку, той же Posix та всі приколи про трушний fork вони аплікабл навіть якщо ти стильний, модний, молодьожний тру вебдевелопер!

Як до того всього повинен дійти ... ?

глянути в мануал що — syslog MT-safe, але не AS-safe

log4... — по ідеї щось малоб бути в його доках який тип safety ви отримаєте з ним — але хз що там — не в курсі

глянути в мануал що — syslog MT-safe, але не AS-safe

Ні, ні, нема такого в пайтонівських доках!
І в доках до syslog теж того нема

Це про мютекси й трабли знання яке «треба знати»
з чого я й почав «Який запис у лог?
Той лог, що локається окремим мютексом в парент процесі?...»
а далі вся наша дискусія довкола того, що syslog «не катить»,
й він таки не катить й не «бо напевно там можуть теж бути мютекси» а «точно, бо він не ліститься в списку дозволених АПІшок»))

log4... — по ідеї щось малоб бути в його доках який тип safety ви отримаєте з ним — але хз що там — не в курсі

В ідеальному світі «мало б бути», але чи є?
З точки зору «здорового глузду» ніякий логер для малтісрідінгової аплікухи не катить після fork («не катить», якщо ми його не створимо по новій, а поюзаємо з парента), бо там неодмінно будуть якісь локи...

syslog MT-safe, не AS-safe

в доках до syslog теж того нема

/MT-Safe
www.gnu.org/...​/syslog_003b-vsyslog.html
manpages.debian.org/...​ages-dev/syslog.3.en.html

або швидше в cli набрати: man syslog

в інших os/libc (чи наскільки compat на майбутнє) — не шукав

нема такого в пайтонівських доках

нічим не допоможу, і невпевнений чи в пайтон з форками взагалі хоч щось гарантовано, і як системні інтерфейси заюзані

в інтернет /MT-Safe

і що?
«MT-Safe or Thread-Safe functions are safe to call in the presence of other threads. MT, in MT-Safe, stands for Multi Thread.»
взагалі не в тему
нас повинно зацікавити те, що вона «AS-Unsafe»... «AS-Safe, stands for Asynchronous Signal» (й згідно спеки, 100% не катить, бо «the child process may only execute async-signal-safe operations until such time as one of the exec functions is called»)

www.gnu.org/...​/syslog_003b-vsyslog.html
manpages.debian.org/...​ages-dev/syslog.3.en.html

де там про fork?

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

syslog не катить

нічим не допоможу, і невпевнений чи там взагалі щось форкати можна, і як системні інтерфейси заюзані

Так воно ж форкає згідно доків

Та й ми ж тут коментуємо, взагалі то, пост про Пайтон та його
multiprocessing.Process(target=чототам...
(і multiprocessing.Lock це «не той» Lock, і не про те))

===================
про що ми сперечаємося?
чи можна юзати syslog в чайлді? ні не можна, див вище.
чи існують граблі з «відкопійованими з парента недорбленими стейтами» якщо багатосрідова аплікуха робить форк? так, існують!

Власне й в тому суть (знай би, де впадеш, то соломки б постелив :))

Зокрема й тут: юзаєш собі спокійно Пайтон згідно доків, «нікого не чіпаєш», а тут «така от проблемка», бо там на Лінухах під капотом fork з його сюрпрайзами!

Хочеш то пофікити — юзай set_start_method(’spawn’)
(або зовсім не юзай малтісрідінг в паренті, що не завжди можливо, адже його можуть юзати й ліби конкреного проекту, і то так просто не викинеш)

проблему вирішено :)

про що

про те що повз системного syslog в ніксах пройти неможливо, та існування якого не мало б (від слова зовсім) викликати питання «знати про це».

А якщо з самого-самого початку то

fork в лінухі задепрекейчений і рекомендують використовувати vfork

що природньо викликало питання відносно юнікс мануалів.

Все решта — що де і як використовувати (включно з vfork python-multithread-fork syslog чи ще щось — це шукайте десь зверху — хто що де пропонував і дискутуйте що там spawn вирішено)

p.s. це я як побачив «про що», інші по іншому, і т.д.

про те що повз системного syslog в ніксах пройти неможливо, та існування якого не мало б (від слова зовсім) викликати питання «знати про це».

Якого «системного», якщо корінь розмови — Пайтон, й те, на які АПІшки він опирається (або не опирається) в своєму малтіпроцесінгу))

І що ми тепер знаємо? Що юзати syslog не можна після fork бо хоч він й MT-Safe але AS-Unsafe))

що природньо викликало питання відносно юнікс мануалів.

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

Якщо весь мій «всесвіт» крутиться довкола умовного PyCharm, то «всьо решта» воно «в ідеальному світі» взагалі не повинно мене зачіпати... А на практиці «є нюанси», про які доводить «знати» не лише мені а й іншим доказувати «юзай set_start_method(’spawn’) бо ...»

І взагалі fork() is evil; vfork() is goodness; afork() would be better; clone() is stupid

І навіть M$ про те саме
))

Все решта — що де і як використовувати (включно з vfork python-multithread-fork syslog чи ще щось — це шукайте десь зверху — хто що де пропонував і дискутуйте що там spawn вирішено)

нічого незрозуміло, але дуже цікаво :)

p.s. це я як побачив «про що», інші по іншому, і т.д.

а мій інтерес цілком практичний — проблема існує, рішення знайдено, й навіть аргументів чому те рішення саме таке, а не інакше додатково прибуло :)

fork в лінухі задепрекейчений і рекомендують використовувати vfork

І взагалі fork() is evil; vfork() is goodness

назвіть це superloop містера NobodyXu і додайте в с̶к̶р̶и̶ж̶а̶л̶і̶ gists вебпрограмування

назвіть це superloop містера NobodyXu

ем, та ж навіть M$ про то само...
там ж на тому ж gist й лінки про репости
взагалі не розумію, що там неправда?

те, що цю частиниу не вписали в офіційних манах?...
так ті мануали також пише буквально «невідомо хто», але навіть згідно тих мануалів юзати syslog не можна після fork бо попри те, що syslog MT-Safe він все одно AS-Unsafe

додайте в с̶к̶р̶и̶ж̶а̶л̶і̶ gists вебпрограмування

ем, не розумію, а в чому проблема з вебпрограмуванням?
чому це вебпрограмери не мають права на свій прекрасний світ без траблів з зависанням логів після fork? ))
Та і для няшних Сшників та Сшниць про з@п@дло з нерозлоканими мютексами коли відбувається fork теж знати корисно :)

рекомендують використовувати vfork

vfork() is goodness

superloop містера NobodyXu

та ж навіть MS ...

штовхайте далі, vfork goodness десь застрягла

штовхайте далі, vfork goodness десь застрягла

«смєшалість в кучу коні, люді...»

vfork (та posix_spawn() куди ж без неї))

))

штовхайте далі, vfork goodness десь застрягла

смєшалісь

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

тепер піднімаєтесь догори

та «там догори» питань більше нема
fork — сумулька (нехай й іншими словами))
syslog після форка — сумулька :)

і так «можна працювати в *nix і не знати про syslog()», запросто

і люди цілком собі працюють в своїх пайчармах, і ніякими syslog та системними джорналами не користуються, бо їм то наф*г непотрібно))
й в «ідеальному світі» *саме так би й мало б бути* :)

коли все в контейнерах

жахіття, коли все

Він задепрекейтен лише якщо є якісь потоки окрім мейн. Бо це може спричиняти неприємні баги. Якщо форк виконувати до того як є потоки усе норм. Тобто, рекомендація наступна:
1. Стартуєте апп
2. Завантажуєте у памʼять усі необхідні статичні дані в основному потоці
3. Робите форк якщо хочете у мультіпроцес
4. Завершуєте ініціалізацію апи (тепер у незалежних процесах). Саме тут повинні запуститися усі допоміжні потоки накшталт експорту телеметрії
5. Інформуєте основний процес, що ваші «сабпроцес» вже готові приймати ревести

Все вірно, аплодую, саме так!

Але зараз хтось прийде й почне виясняти, що саме там мав а увазі автор процитованого посту і тому подібне...

Та байдуже на ті всі з’ясування, продуктом розмови повинна бути порада як діяти в конкретній ситуації, й ось цей допис вище: чадовий підсумок!

що саме там ...

там і виясняти нічого не треба:

fork в лінухі задепрекейчений і рекомендують використовувати vfork

що тут можна двозначно зрозуміти — то загадка ... То або коректно, або ні)

Я так і знав))

fork в лінухі задепрекейчений і рекомендують використовувати vfork

що тут можна двозначно зрозуміти — то загадка ... То або коректно, або ні)

байдуже конкретний вислів, трабли з форком існують, переступаємо через «пуризм», ігноруємо такі деталі...

fork в лінухі НЕ задепрекейчений, але все одно форк — г@авно (і на те є аргументи)
І так, для деяких кейсів vfork — краще... (а для деяких навіть fork прокатить, просто з міркувань простоти та ліні, вище маємо чудовий рецепт))

корисний аутпут є, скільки ми цікавих нюансів висвітлили...
й нагенерили україномовного контенту по темі, що, на моє сподівання, заіндексується гуглом і вестиме сюди, а не на «форум рускіх праграмістаф»

хіба ж це не прекрасно?

fork в лінухі задепрекейчений і рекомендують використовувати vfork

fork в лінухі НЕ задепрекейчений, але все одно ...

цього достатньо щоб не плутатись, і спочатку дивитись в мануал — коли що і як використовувати — і це вже буде «прекрасно»

Та ніхто (по більшості) не буде дивитися, скопіпейстять з стековерфлова з чату GPT без ніякого розуміння...

(мануал треба читати не одну сторіночку і дуже вдумчиво, щоб зрозуміти наслідки та зробити висновки... це «так просто не працює»)

треба читати не одну сторіночку.. зрозуміти.. зробити.. — це «так просто не працює»

ідея вебпрограмування як — «а нафіга ту документацію читати, воно і так все працює»)

ідея вебпрограмування як — «а нафіга ту документацію читати, воно і так все працює»)

Саме так в «ідеальному світі» й мало б бути. Тобто саме без необхідності читати мануали Сшних АПІшкок, і саме без необхідності знати (та розуміти) ті всі подробиці!

Стандартної документації, що йде «в наборі» з Пайтоном «повинно бути досить»... PyCharm ІДЕшки повинно бути досить!

Не повинно існувати такого «таємного знання для посвячених», що стає «додатковим читанням», тобто в «ідеальному світі» саме й треба досягти того, щоб тому вебпрограмеру взагалі не треба було заглядати в Сшну частину...

Й бажання вебпрограмера прямувати до такого ідеалу — цілком природнє :)

«А нафіга ту документацію читати, воно і так все працює»

Й бажання вебпрограмера прямувати до такого ідеалу — цілком природнє

Цілком природно, як і природнє бажання не займатися вебпрограмуванням взагалі, а нп краще надиктовувати щось аі-асістенту і звільнений час витрачати на щось більш креативне. А поки того «ідеалу» нема, прийдеться хоч зрідка читати доки.

А поки того «ідеалу» нема, прийдеться хоч зрідка читати доки.

або шукати відповідь в гуглі, й... знайти одразу готову відповідь (наприклад тут), замість проходження «шляху гуру» :)

А ще є така надія, що навіть GPT це прочитає й колись додасть у свою готову відповідь))

або шукати відповідь в гуглі, й... знайти одразу ...

і знайти одразу NobodyXu.

Якщо стд мануал не дається, то навіть і біг Гугл з його беззаперечною корисністю і усією його magic — здається не завжди допомагає. Хоча ще залишається надія на ШІ — що в майбутньому можливо як краще з доками працювати буде, так і краще за гугл роз’яснювати то буде)

й знайти одразу NobodyXu.

Ху із зіс? за яким з лінків неправильно?

Як мені здається обс*р@ння форка чудово веде пошук «якраз в правильному напрямку» :)

Й через певну кількість кроків прийде до set_start_method(’spawn’)

і це прекрасно :)

якщо тролінг з «vfork is goodness» від NobodyXu веде

якраз в правильному напрямку

і це прекрасно для когось, хай так і буде — то його «прекрасне» без док)

і це прекрасно для когось, хай так і буде — то його «прекрасне» без док)

NobodyXu Nico Williams там зібрав чудовий набір лінків зокрема й посилання на доки))

Що саме з сказаного Nico Williams суперечить приведеним докам?

і щось типу «vfork is good, fork is bad» піде на 15е коло, з яких на першому вже все є, а ті хто не встиг — завжди може продовжити хоч спочатку хоч зсередини)

Відносно «vfork is goodness» — штовхайте далі «прекрасне» щоб то в лінукс доки внесли, бо інакше сприйматимуть саме так як воно є — тобто так як записано в документації, а не в «vfork is goodness».

p.s. general statement «fork is evil, vfork is goodness» зі сторони сприймається як тонкий тролінг, НобадіХу по назві доволі таки вписується до такого типу кардинальних стверджень які чомусь не додали в доки ЛінуксБаді)

Штовхайте далі «прекрасне» щоб то в лінукс доки внесли, бо інакше сприйматимуть саме так як воно є — тобто так як записано в документації, а не в «vfork is goodness»)

а як записано?
хіба ж там не пише «A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called.»

А штовхати треба не в Лінукс (бо ж воно там є), а в Пайтон, щоб зробили
set_start_method(’spawn’)
завжди по дефолту)) й тоді ще й на всіх платформах воно робитиме однаково!

Всьо решта цікавого про (v)fork добродій Nico Williams чудово «навів», не бачу щоб його висновки суперечили докам :)

Взагалі не розумію цієї манічки, а чому це рекомендація повинна бути (або не бути) саме в офіційному описі «як воно працює»?

Он в ECMA-262 ніде ж так й не пише, що «eval нот рекомендед», але ж ми й без того знаємо, що «eval нот рекомендед», правда?
(спробувати вивчити ЖС за ECMA-262 — то був би «ще той» мазохізм))

____
(ну і syslog, звісно, не катить, бо він відсутній в списку The following table defines a set of functions and function-like macros that shall be async-signal-safe. Therefore, applications can call them, without restriction, from signal-catching functions.)

А штовхати треба не в Лінукс (бо ж воно там є), а в Пайтон, щоб зробили ...

якщо в лінукс вже все є, проштовхуйте в пайтон що там і як по дефолту має бути з вашої точки зору, posix_spawn(3) rationale по використанню в доках також є

хіба ж там не пише «A process shall be created with a ...

fork(2) notes по використанню в description — це не bugs/caveats/removed/deprecated/evil

не бачу щоб його висновки суперечили докам

секції caveats/bugs в vfork(2), і також рекомендації де і як використовувати, — не зовсім збігаються з кардинальними твердженнями «vfork is goodness» (не в залежності від того наскільки bad чи good в цілому fork/exec концепт, і наскільки то застаріло чи ні)

p.s. наступну ітерацію forkness обговорення, пліс, вже без мене

якщо в лінукс вже все є, проштовхуйте в пайтон що там і як по дефолту має бути з вашої точки зору, posix_spawn(3) rationale по використанню в доках також є

так процес походу вже йде «куди треба»: «Note The default start method will change away from fork in Python 3.14. Code that requires fork should explicitly specify that via get_context() or set_start_method().
...
Changed in version 3.12: If Python is able to detect that your process has multiple threads, the os.fork() function that this start method calls internally will raise a DeprecationWarning. Use a different start method. See the os.fork() documentation for further explanation.»

і навіть «Changed in version 3.12: If Python is able to detect that your process has multiple threads, os.fork() now raises a DeprecationWarning.

We chose to surface this as a warning, when detectable, to better inform developers of a design problem that the POSIX platform specifically notes as not supported. Even in code that appears to work, it has never been safe to mix threading with os.fork() on POSIX platforms. The CPython runtime itself has always made API calls that are not safe for use in the child process when threads existed in the parent (such as malloc and free).

Users of macOS or users of libc or malloc implementations other than those typically found in glibc to date are among those already more likely to experience deadlocks running such code.

See this discussion on fork being incompatible with threads for technical details of why we’re surfacing this longstanding platform compatibility problem to developers.»

коротше fork — г@вно! Шкода тільки, що це зайняло стільки років щоб до того нарешті доперти. І ото, що «доперання» зайняло поза десяток років, то також й бага «канонічної» документації, де наслідки певних дій ніразу зходу не очевидні, тобто «в лінукс вже все є» — неправда, бо за поза 10років ціла команда мейнтейнерів Пайтона так і не змогли того побачити...

fork(2) notes по використанню в description — це не bugs/caveats/removed/deprecated/evil

Це достатньо великі граблі для того, щоб вважати запуск fork малтісрідінговою аплікухою, яка юзає не async-signal-safe АПІшки після fork та до exec — багою!...

Й навіть авторам Пайтона (нехіле таке ком’юніті) знадобилися роки ходіння по «таємничих граблях» щоб до того нарешті дійти...

...звісно, що в «ідеальному світі» вони повинні було б доперти до того одразу з появою модуля multiprocessing, але по факту ситуація яскраво ілюструє як люди, і навіть мейнтейнери таких масштабних проектів працюють в реалі...

коли там з’явився 2.7 Пайтон з форком та малтіпроцесінгом?
років 15 тому... й це люди поза десяток років ходили по таємничих важкорепродюсаємих граблях, мучалися з таємничими багами щоб нарешті доперти, «ура, ну то зробимо DeprecationWarning»!

Щось нете напевно, з доками, що при її використанні стільки граблів, правда ж?

Стів Роберт Мартін плаче кривавими сльозами, бо ж «юзедж АПІшки повинен було зрозумілий з її назви, коментарі коде-смел бла-бла-бла» (найбільше лицемірство, деякі ті самі люди, котрі юзають оті АПІшки, де знайти таку злосну багу з наявних описів АПІшки практично неможливо протягом 10років, то, дуже часто, ті ж самі люди, які в своєму коді часом не пишуть жодного коментаря й вважають що «це і так понятно» при тих всі умовностях та нюансах, коли їхні «самозрозумілі» подєлки при використанні згідно їхнього «селфдескріптів» насправді мають ще більше глюків, ніж навіть той fork!)

секції caveats/bugs в vfork(2), і також рекомендації де і як використовувати, — не зовсім збігаються з кардинальними твердженнями «vfork is goodness» (не в залежності від того наскільки bad чи good в цілому fork/exec концепт, і наскільки то застаріло чи ні)

це називається «гучний заголовок», та й тема допису Nico Williams «трохи не про те», але «теж цікаво», бо ще один камінчик до теми, що fork г@вно з усіх боків, куди не глянь, і за тестами продуктивності, і навіть через те, що O_CLOEXEC/SOCK_CLOEXEC для відкриття файлів/сокетів не подефолту й всі непотрібні дескриптори дочірньому процесові також в спадок!))

p.s. наступну ітерацію forkness обговорення, пліс, вже без мене

Та прошу пана, взагалі не знаю, навіщо ото сперечатися «хто що сказав», тим більше навіть я вже стаю 100% переконаним, що треба діяти так ніби

fork в лінухі задепрекейчений і рекомендують використовувати vfork

100% істине

хоч і

notes по використанню в description — це не

та

не зовсім збігаються з кардинальними твердженнями

але все одно — fork г@вно))

fork в лінухі задепрекейчений і рекомендують використовувати vfork

Він задепрекейтен лише якщо є ...

Початкова фраза була відносно «fork() vs vfork()» - саме як процитовано вище:
тобто — deprecated «він» в цьому контексті читається як fork, і відповідно використовуйте vfork.
Тобто в цих коментах дві різні суті: (1) fork-vs-vfork, (2) використання fork взагалі.

Дякую за працю, було цікаво читати!

Дякую за схвальний відгук)

Було б цікаво побачити порівняння швидкості виконання, коли дані передаються між кількома процесами через **спільну пам’ять (shared memory)** та **канали (pipes)** 🙂

Тому що, коли я востаннє з цим стикався, швидкість виконання коду через **спільну пам’ять** була **катастрофічно нижчою**, ніж через **канали**... 😅

Тому що, коли я востаннє з цим стикався, швидкість виконання коду через **спільну пам’ять** була **катастрофічно нижчою**, ніж через **канали**... 😅

Це на одній системі? Вкрай дивно. Мабуть, пороблено було щось в логиці...

Важливо розуміти, що multiprocessing.cpu_count() повертає кількість ядер процесора машини, на якій виконується код.

Якщо програма запущена в оточенні з урізаною кількістю ресурсів (наприклад, kubernetes pod), то ця функція може повернути не той результат, який ви очікуєте.

Тобто якщо на машині доступно 4 CPU, але ви запускаєте pod з 1 CPU, то multiprocessing.cpu_count() все-одно поверне 4

Підписатись на коментарі