Featured image of post Часть III. Управление памятью и многопоточность

Часть III. Управление памятью и многопоточность

Основы управления памятью и многопоточности в системном программировании, с примерами на языке C. Оптимизация ресурсов и синхронизация потоков с использованием pthreads

Часть III. Управление памятью и многопоточность


3.1 Управление памятью в системном программировании

3.1.1 Введение в управление памятью

В системном программировании управление памятью — это процесс выделения, управления и освобождения памяти для программ. Память — это критический ресурс, которым управляет операционная система, предоставляя приложениям доступ к необходимым блокам памяти.

Основные задачи управления памятью:

  • Выделение памяти: Программы запрашивают блоки памяти для хранения данных.
  • Освобождение памяти: Программы освобождают память, когда она больше не требуется, чтобы предотвратить утечки памяти.
  • Защита памяти: Каждому процессу выделяется изолированная область памяти, чтобы предотвратить вмешательство других процессов.

3.1.2 Адресное пространство процесса

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

Адресное пространство процесса включает в себя:

  • Текстовый сегмент: Содержит код программы.
  • Данные: Статические и глобальные переменные.
  • Куча: Динамическая память, которая выделяется во время выполнения программы.
  • Стек: Хранит локальные переменные и адреса возврата для функций.

3.1.3 Выделение и освобождение динамической памяти

В языках системного программирования, таких как C, программист отвечает за управление динамической памятью с использованием функций malloc() и free().

  • malloc(): Функция, которая выделяет блок памяти указанного размера и возвращает указатель на начало этого блока. Если памяти недостаточно, функция возвращает NULL.

    Пример:

    1
    2
    3
    4
    
    int *arr = (int *)malloc(10 * sizeof(int));
    if (arr == NULL) {
        // Ошибка: недостаточно памяти
    }
    
  • free(): Функция, которая освобождает ранее выделенный блок памяти, делая его доступным для других частей программы.

    Пример:

    1
    
    free(arr);
    

3.1.4 Утечки памяти и их предотвращение

Утечка памяти происходит, когда программа выделяет память, но не освобождает ее после завершения работы. Это может привести к значительному снижению производительности или даже к завершению программы из-за недостатка памяти.

Предотвращение утечек памяти:

  • Всегда освобождайте динамически выделенную память с помощью free().
  • Используйте инструменты для поиска утечек памяти, такие как Valgrind и AddressSanitizer.

Пример программы с утечкой памяти:

1
2
3
4
int *leak() {
    int *arr = (int *)malloc(100 * sizeof(int));
    return arr;  // Память не будет освобождена
}

Исправленная версия:

1
2
3
void free_memory(int *arr) {
    free(arr);  // Память освобождается
}

3.1.5 Пример использования динамической памяти

Пример программы на C, которая динамически выделяет память для массива чисел, заполняет его случайными числами, сортирует и выводит результат:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int main() {
    int n = 10;
    int *arr = (int *)malloc(n * sizeof(int));

    if (arr == NULL) {
        perror("Ошибка выделения памяти");
        return 1;
    }

    srand(time(0));
    for (int i = 0; i < n; i++) {
        arr[i] = rand() % 100;
    }

    qsort(arr, n, sizeof(int), compare);

    printf("Отсортированный массив:\n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);
    return 0;
}

Объяснение программы:

  • Мы динамически выделяем память для массива из 10 целых чисел с помощью malloc().
  • Затем заполняем массив случайными числами и сортируем его с помощью функции qsort().
  • После вывода отсортированного массива освобождаем память с помощью free().

3.2 Многопоточность в системном программировании

3.2.1 Основы многопоточности

Поток (thread) — это наименьшая единица выполнения программы. Многопоточность позволяет программам выполнять несколько задач параллельно, что особенно важно для повышения производительности на многоядерных процессорах.

Операционная система предоставляет механизм управления потоками, который позволяет программам создавать, синхронизировать и завершать потоки. В системном программировании часто используются потоки для параллельного выполнения вычислительных задач, работы с вводом-выводом и взаимодействия с аппаратным обеспечением.

Преимущества многопоточности:

  • Повышение производительности за счет параллельного выполнения.
  • Эффективное использование ресурсов многоядерных процессоров.
  • Возможность асинхронной обработки задач.

3.2.2 Создание и управление потоками с использованием POSIX Threads (pthreads)

В системах на основе UNIX потоки создаются и управляются с использованием библиотеки POSIX Threads (pthreads). Эта библиотека предоставляет функции для создания, синхронизации и завершения потоков.

Основные функции pthreads:

  • pthread_create(): Создание нового потока.
  • pthread_join(): Ожидание завершения потока.
  • pthread_mutex_lock() и pthread_mutex_unlock(): Управление мьютексами для синхронизации потоков.

Пример программы с потоками на C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <pthread.h>

void *thread_function(void *arg) {
    printf("Поток: %ld\n", (long)arg);
    return NULL;
}

int main() {
    pthread_t threads[5];

    for (long i = 0; i < 5; i++) {
        pthread_create(&threads[i], NULL, thread_function, (void *)i);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

Объяснение программы:

  • В этой программе создается 5 потоков, каждый из которых выполняет функцию thread_function(), которая просто выводит сообщение с номером потока.
  • Функция pthread_create() создает новый поток, а pthread_join() позволяет главному потоку ожидать завершения каждого созданного потока.

3.2.3 Синхронизация потоков с использованием мьютексов

Когда несколько потоков одновременно обращаются к одним и тем же данным, могут возникнуть гонки данных. Для предотвращения этого используются механизмы синхронизации, такие как мьютексы.

Мьютекс (mutual exclusion) — это объект синхронизации, который позволяет только одному потоку одновременно получать доступ к критической секции программы (например, к общим данным).

Основные функции работы с мьютексами:

  • pthread_mutex_lock(): Блокирует мьютекс, предоставляя доступ к критической секции только одному потоку.
  • pthread_mutex_unlock(): Освобождает мьютекс, позволяя другим потокам войти в критическую секцию.

Пример программы с синхронизацией потоков:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <pthread.h>

int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *increment(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t threads[5];

    for (int i = 0; i < 5; i++) {
        pthread_create(&threads[i], NULL, increment, NULL);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("Итоговое значение счетчика: %d\n", counter);
    return 0;
}

Объяснение программы:

  • Пять потоков увеличивают общий счетчик одновременно. Чтобы избежать гонки данных, каждый поток блокирует доступ к переменной counter с помощью мьютекса перед увеличением значения.
  • После увеличения переменной поток освобождает мьютекс, позволяя другим потокам получить доступ к счетчику.

Практическое задание

Задание 1:

  • Напишите программу на C, которая динамически выделяет память для большого массива, заполняет его случайными числами, сортирует и выводит результат. Обязательно освободите память после завершения программы.

Задание 2:

  • Напишите программу, которая создает несколько потоков для параллельного увеличения общего счетчика. Используйте мьютексы для предотвращения гонок данных и выведите итоговое значение счетчика.

Заключение к главе 3

В этой главе мы изучили важные аспекты управления памятью и многопоточности в системном программировании. Эти знания необходимы для создания высокопроизводительных системных программ, которые эффективно используют ресурсы процессора и памяти. В следующем разделе мы углубимся в более продвинутые темы, такие как параллелизм и работа с виртуальными машинами.

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy