Какой вид многозадачности (потоки или процессы) потребляет больше системных ресурсов?

Ответ

Многопроцессность (multiprocessing) потребляет значительно больше системных ресурсов, чем многопоточность (threading).

Это обусловлено фундаментальными различиями в их архитектуре:

  • Процессы (Multiprocessing):

    • Каждый процесс имеет собственное, полностью независимое адресное пространство памяти и свои ресурсы операционной системы (файловые дескрипторы, таблицы страниц и т.д.).
    • Создание нового процесса требует выделения значительного объема памяти и ресурсов ОС, что является дорогостоящей операцией.
    • Переключение контекста между процессами (context switching) также относительно затратно, так как ОС должна сохранять и восстанавливать состояние всего адресного пространства.
    • Обмен данными между процессами (IPC - Inter-Process Communication) требует специальных механизмов (пайпы, очереди, общая память), что добавляет накладные расходы.
  • Потоки (Threading):

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

Особенности Python: В Python, из-за наличия Global Interpreter Lock (GIL), потоки не могут выполнять код Python параллельно на нескольких ядрах CPU. GIL позволяет выполнять только один поток Python за раз. Поэтому:

  • Для CPU-bound задач (интенсивные вычисления) multiprocessing является предпочтительным, так как позволяет использовать несколько ядер CPU для реального параллелизма, несмотря на большие накладные расходы на ресурсы.
  • Для I/O-bound задач (ожидание ввода/вывода, сетевые запросы) threading подходит лучше, так как потоки могут переключаться во время ожидания I/O, эффективно используя время, и имеют меньшие накладные расходы.

Примеры использования:

# CPU-bound задача: используем multiprocessing для реального параллелизма
from multiprocessing import Pool
import os

def calculate_square(number):
    # Имитация интенсивной CPU-операции
    return number * number

if __name__ == '__main__':
    print(f"Running CPU-bound task with {os.cpu_count()} processes...")
    with Pool(processes=os.cpu_count()) as pool:
        results = pool.map(calculate_square, range(100000))
    # print(results[:10]) # Вывод первых 10 результатов

# I/O-bound задача: используем threading для эффективного ожидания I/O
import threading
import time

def fetch_data(url):
    # Имитация сетевого запроса или чтения файла
    print(f"[Thread {threading.current_thread().name}] Fetching data from {url}...")
    time.sleep(1) # Имитация задержки I/O
    print(f"[Thread {threading.current_thread().name}] Finished fetching from {url}.")
    return f"Data from {url}"

if __name__ == '__main__':
    urls = [f"http://example.com/{i}" for i in range(5)]
    threads = []
    print("nRunning I/O-bound task with threads...")
    for i, url in enumerate(urls):
        thread = threading.Thread(target=fetch_data, args=(url,), name=f"Worker-{i}")
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()
    print("All I/O-bound tasks completed.")

Ответ 18+ 🔞

Давай разберёмся, что к чему, а то тут у некоторых в голове каша, как будто они с похмелья. Многопроцессность и многопоточность — это две большие разницы, как говорят в Одессе, и ресурсов они жрут по-разному, просто пиздец.

Смотри, в чём соль:

  • Многопроцессность (multiprocessing): Это когда ты заводишь кучу отдельных, ебаных, независимых вселенных. Каждый процесс — это свой собственный мир. У него своя память, свои файлы, свои таблицы, всё своё, блядь. Завести новый процесс — это как построить новый дом с нуля: кирпичи, фундамент, крыша — овердохуища ресурсов и времени. А переключаться между ними — это как переезжать из одной квартиры в другую: всю мебель таскать, вещи собирать — затратно, ёпта. И если им надо пообщаться, они не просто так крикнут через стенку, им нужны специальные трубы (пайпы) или общая комната (shared memory), что тоже не бесплатно.

  • Многопоточность (threading): А это уже не отдельные вселенные, а комнаты в одной квартире. Все потоки живут в одном процессе, делят одну память, один холодильник и один туалет. Создать новый поток — это как поставить раскладушку в углу: быстро и дешево. Переключиться между ними — просто шагнуть из кухни в комнату. Общаются они через общую память, что быстро, но, блядь, нужно дверь в туалет закрывать (мьютексы), а то получится состояние гонки, и все обоссутся.

А теперь про Питон, этот ёперный театр с GIL! В Питоне есть такая штука — Global Interpreter Lock (GIL). Это такой здоровенный замок на всём интерпретаторе. Из-за него в один момент времени только один поток может выполнять питонячий код. Получается, что на нескольких ядрах CPU потоки по-настоящему параллельно не побегут. Поэтому:

  • Если у тебя задача CPU-bound (типа перемножить гигантские матрицы или хеши посчитать) — тут multiprocessing твой друг. Он, хоть и прожорливый, но запустит процессы на разных ядрах, и они будут пахать по-честному, параллельно. Ресурсов сожрёт много, зато дело сделает.
  • Если же задача I/O-bound (скачать сто картинок из интернета, почитать кучу файлов) — тут бери threading. Пока один поток ждёт ответа от сервера, GIL отпустит замок, и другой поток сможет работать. Переключения быстрые, ресурсов мало, и всё будет летать.

Ну и примеры, чтобы совсем всё стало ясно, как божий день:

# CPU-bound задача: тут без мультипроцессинга — никуда, хоть тресни.
from multiprocessing import Pool
import os

def calculate_square(number):
    # Представь, что тут не просто умножение, а какой-нибудь адский расчёт
    return number * number

if __name__ == '__main__':
    print(f"Гоняем CPU-bound задачу на {os.cpu_count()} процессах...")
    with Pool(processes=os.cpu_count()) as pool:
        results = pool.map(calculate_square, range(100000))
    # print(results[:10])

# I/O-bound задача: а тут треды рулят, им лишь бы подождать да передать эстафету.
import threading
import time

def fetch_data(url):
    # Тут типа ждём ответа от сервака, а на самом деле просто спим
    print(f"[Поток {threading.current_thread().name}] Лезем за данными на {url}...")
    time.sleep(1) # Имитируем, будто интернет через модем Dial-up
    print(f"[Поток {threading.current_thread().name}] С горем пополам скачали {url}.")
    return f"Данные с {url}"

if __name__ == '__main__':
    urls = [f"http://example.com/{i}" for i in range(5)]
    threads = []
    print("nГоняем I/O-bound задачу на тредах...")
    for i, url in enumerate(urls):
        thread = threading.Thread(target=fetch_data, args=(url,), name=f"Раб-{i}")
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()
    print("Все I/O-задачи сделаны, можно и отдохнуть.")

Короче, выбирай инструмент по задаче, а не потому что "в интернете написали". Иначе будет как с тем Герасимом: возьмёшь не то и в итоге всё потопишь, а потом совесть заебает.