Многопоточность

Создание потоков

Последнее обновление: 06.02.2024

Многопоточность является важнейших аспектов в современном программировании. Ключевым понятием при работе с многоопоточностью является поток. Поток предствляет некоторую часть кода программы. При выполнении программы каждому потоку выделяется определенный квант времени. И при помощи многопоточности мы можем выделить в приложении несколько потоков, которые будут выполнять различные задачи одновременно. Если у нас, допустим, графическое приложение, которое посылает запрос к какому-нибудь серверу или считывает и обрабатывает огромный файл, то без многопоточности у нас бы блокировался графический интерфейс на время выполнения задачи. А благодаря потокам мы можем выделить отправку запроса или любую другую задачу, которая может долго обрабатываться, в отдельный поток. Поэтому, к примеру, клиент-серверные приложения (и не только они) практически не мыслимы без многопоточности.

В языке Си поток представляет объект типа pthread_t, который определен в заголовочном файле pthread.h. А для создания потока применяется функция pthread_create(), которая имеет следующий прототип:

int pthread_create(
  pthread_t *thread,
  const pthread_attr_t *attr,
  void *(*routine) (void *),
  void *arg);

Эта функция принимает следующие параметры:

  • *thread: указатель на объект pthread_t, который инициализируется.

  • *attr: указатель на набор атрибутов потока в виде объекта pthread_attr_t. Если атрибуты не нужны, можно передать NULL

  • *routine: указатель на функцию, которая запускается в потоке. Подобная функция должна принимать в качестве параметра указатель void*, вместо которого можно передать указатель на любой объект. Однако допускается только один аргумент. Соответственно если надо передать несколько значений, то можно оформить их в массив или структуру.

    Возвращаемое значение функции также должно представлять указатель void*. Соответственно также можно возвратить указатель на любой объект. Данное возвращаемое значение будет выступать в качестве результата потока. Опять же здесь мы можем возвратить значение NULL, если оно нам не нужно.

  • *arg: указатель void*, через который передается значение в запускаемую функцию.

Например, определим следующую программу:

#include <pthread.h>
#include <stdio.h>

void* some_work(void* arg) 
{
    for(int i = 0; i < 5; ++i) 
    {
        puts(arg);
    }
    return NULL;
}
int main(void) 
{ 
    pthread_t thread1, thread2;
    // создаем потоки
    pthread_create(&thread1, NULL, some_work, "Hello World" );
    pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" );

    printf("End...\n");
    return 0;
}

В функции main сначала объявляем две переменных, которые будут представлять потоки:

pthread_t thread1, thread2;

Затем инициализируем их:

pthread_create(&thread1, NULL, some_work, "Hello World" );
pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" );

Инициализация обоих потоков аналогична. В качестве первого параметра передаем в функцию адрес переменной. Атрибуты потока нам сейчас не важны, поэтому вторым аргументом передаем значение NULL. В качестве выполняемой функции передаем функцию some_work, которая определена выше. Четвертое значение - аргумент для функции - в нашем случае это две разных строки.

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

void* some_work(void* arg) 
{
    for(int i = 0; i < 5; ++i) 
    {
        puts(arg);
    }
    return NULL;
}

Результат функции здесь нам не важен, поэтому возвращаем NULL

Скомпилируем и запустим программу:

eugene@Eugene:~/Documents/metanit$ gcc -Wall -pedantic -std=c2x main.c -o main
eugene@Eugene:~/Documents/metanit$ ./main
End...
eugene@Eugene:~/Documents/metanit$ 

На примере моего вывода можно увидеть, что потоки не отработали до конца - ни один из них не вывел переданную в него строку. В реальности вывод в данном случае недетерминирован. Например, он мог бы быть следующим:

eugene@Eugene:~/Documents/metanit$ ./main
End...
Hello World
Hello World
Hello World
Hello World
Hello World
eugene@Eugene:~/Documents/metanit$ 

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

Функция pthread_exit

Функция pthread_exit() позволяет подождать, пока не завершаться все потоки, и после их завершения завершает вызывающий поток (в нашем случае главный поток программы, в котором запущена функция main) и позволяет выполниться всем другим потокам. Эта функция принимает один параметр - возвращаемый результат. Если он не важен, то можно передать NULL. Далее мы посмотрим, где это значение может пригодиться.

Итак, изменим код программы, применив функцию pthread_exit():

#include <pthread.h>
#include <stdio.h>

void* some_work(void* arg) 
{
    for(int i = 0; i < 5; ++i) 
    {
        puts(arg);
    }
    return NULL;
}
int main(void) 
{ 
    pthread_t thread1, thread2;
    // создаем потоки
    pthread_create(&thread1, NULL, some_work, "Hello World" );
    pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" );

    pthread_exit(NULL);

    printf("End...\n");
    return 0;
}

Теперь оба потока будут выполняться полностью:

eugene@Eugene:~/Documents/metanit$ ./main
Hello World
Hello World
Hello World
Hello World
Hello World
Hello METANIT.COM
Hello METANIT.COM
Hello METANIT.COM
Hello METANIT.COM
Hello METANIT.COM
eugene@Eugene:~/Documents/metanit$ 

Однако, поскольку функция pthread_exit() завершает вызывающий поток (поток функции main), то консоль НЕ выведет строку "End...".

Функция sleep

Чтобы сделать более показательной одновременную работу потоков, применим функцию sleep(). Она определена в заголовочном файле "unistd.h" и позволяет приостановить текущий поток на определенное количество секунд. Так, изменим код программы следующим образом:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* some_work(void* arg) 
{
    for(int i = 0; i < 5; ++i) 
    {
        puts(arg);
        sleep(1);   // задержка в 1 секунду
    }
    return NULL;
}
int main(void) 
{ 
    pthread_t thread1, thread2;
    pthread_create(&thread1, NULL, some_work, "Hello World" );
    pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" );

    pthread_exit(NULL);
    printf("End...\n");
    return 0;
}

В данном случае применяем задержку в 1 секунду, благодаря чему потоки могут выводить строку на консоль почти по перемено. Например:

eugene@Eugene:~/Documents/metanit$ ./main
Hello World
Hello METANIT.COM
Hello METANIT.COM
Hello World
Hello World
Hello METANIT.COM
Hello World
Hello METANIT.COM
Hello World
Hello METANIT.COM
eugene@Eugene:~/Documents/metanit$ 

Точный вывод, какой поток начнет первым выведлет строку на консоль опять же неопределен.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850