2.2 Потоки

в 15:12, , рубрики: c++, game development, SFML, Локализация продуктов, параллельное программирование, потоки, Программирование, С++, урок

2.2 Потоки - 1

От переводчика: данная статья является третьей в цикле переводов официального руководства по библиотеке SFML. Прошлую статью можно найти тут. Данный цикл статей ставит своей целью предоставить людям, не знающим язык оригинала, возможность ознакомится с этой библиотекой. SFML — это простая и кроссплатформенная мультимедиа библиотека. SFML обеспечивает простой интерфейс для разработки игр и прочих мультимедийных приложений. Оригинальную статью можно найти тут. Начнем.

Оглавление:

0.1 Вступление

1. Приступая к работе

  1. SFML и Visual Studio
  2. SFML и Code::Blocks (MinGW)
  3. SFML и Linux
  4. SFML и Xcode (Mac OS X)
  5. Компиляция SFML с помощью CMake

2. Модуль System

  1. Обработка времени
  2. Потоки
  3. Работа с пользовательскими потоки данных

3. Модуль Window

  1. Открытие и управление окнами
  2. Обработка событий
  3. Работа с клавиатурой, мышью и джойстиками
  4. Использование OpenGL

4. Модуль Graphics

  1. Рисование 2D объектов
  2. Спрайты и текстуры
  3. Текст и шрифты
  4. Формы
  5. Проектирование ваших собственных объектов с помощью массивов вершин
  6. Позиция, вращение, масштаб: преобразование объектов
  7. Добавление специальных эффектов с шейдерами
  8. Контроль 2D камеры и вида

5. Модуль Audio

  1. Проигрывание звуков и музыки
  2. Запись аудио
  3. Пользовательские потоки аудио
  4. Спатиализация: звуки в 3D

6. Модуль Network

  1. Коммуникация с использованием сокетов
  2. Использование и расширение пакетов
  3. Веб-запросы с помощью HTTP
  4. Передача файлов с помощью FTP

Что такое поток?

Большая часть из вас уже знает, что такое поток, однако объясним, что это такое, для новичков в данной теме.

Поток — это по сути последовательность инструкций, которые выполняются параллельно с другими потоками. Каждая программа создает по меньшей мере один поток: основной, который запускает функцию main(). Программа, использующая только главный поток, является однопоточной; если добавить один или более потоков, она станет многопоточной.

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

Потоки SFML или std::thread?

В своей последней версии (2011), стандартная библиотека C++ предоставляет набор классов для работы с потоками. Во время написания SFML, стандарт C++11 еще не был написан и не было никакого стандартного способа создания потоков. Когда SFML 2.0 был выпущен, было много компиляторов, которые не поддерживали этот новый стандарт.

Если вы работаете с компилятором, который поддерживает новый стандарт и содержит заголовочный файл , забудьте о классах потоков SFML и используйте стандартные классы C++ вместо них. Но, если вы работаете с компилятором, не поддерживающим данный стандарт, или планируете распространять ваш код и хотите добиться полной портируемости, потоковые классы SFML являются хорошим выбором

Создание потоков с помощью SFML

Хватит разглагольствований, давайте посмотрим на код. Класс, дающий возможность создавать потоки с помощью SFML, называется sf::Thread, и вот как это (создание потока) выглядит в действии:

#include <SFML/System.hpp>
#include <iostream>

void func()
{
    // эта функция запускается когда вызывается thread.launch()

    for (int i = 0; i < 10; ++i)
        std::cout << "I'm thread number one" << std::endl;
}

int main()
{
    // создание потока с функцией func в качестве точки входа
    sf::Thread thread(&func);

    // запуск потока
    thread.launch();

    // главные поток продолжает быть запущенным...

    for (int i = 0; i < 10; ++i)
        std::cout << "I'm the main thread" << std::endl;

    return 0;
}

В этом коде функции main и func выполняются параллельно после вызова thread.launch(). Результатом этого является то, что текст, выводимый обеими функциями, смешивается в консоли.

2.2 Потоки - 2

Точка входа в поток, т.е. функция, которая будет выполняться, когда поток запускается, должна быть передана конструктору sf::Thread. sf::Thread пытается быть гибким и принимать различные точки входа: non-member функции или методы классов, с или без аргументов, функторы и так далее. Приведенный выше пример показывает, как использовать функцию-член, вот несколько других примеров.

  • non-member функция с одним аргументом:
    void func(int x)
    {
    }
    
    sf::Thread thread(&func, 5);
    
  • метод класса:
    class MyClass
    {
    public:
    
        void func()
        {
        }
    };
    
    MyClass object;
    sf::Thread thread(&MyClass::func, &object);
    
  • функтор (функциональный объект):
    struct MyFunctor
    {
        void operator()()
        {
        }
    };
    
    sf::Thread thread(MyFunctor());
    

Последний пример, который использует функторы, является наиболее мощным, поскольку он может принимать любые типы функторов и поэтому делает класс sf::Thread совместимым со многими типами функций, которые напрямую не поддерживаются. Эта функция особенно интересна с лямбда-функциями C++11 или std::bind.

// с лямбда-функцией

sf::Thread thread([](){
    std::cout << "I am in thread!" << std::endl;
});

// с std::bind
void func(std::string, int, double)
{
}

sf::Thread thread(std::bind(&func, "hello", 24, 0.5));

Если вы хотите использовать sf::Thread внутри класса, не забудьте, что он не имеет стандартного конструктора. Поэтому, вы должны инициализировать его в конструкторе вашего класса в списке инициализации:

class ClassWithThread
{
public:

    ClassWithThread()
    : m_thread(&ClassWithThread::f, this)
    {
    }

private:

    void f()
    {
        ...
    }

    sf::Thread m_thread;
};

Если вам действительно нужно создать экземпляр sf::Thread после инициализации объекта, вы можете создать его в куче.

Запуск потока

После того, как вы создали экземпляр sf::Thread, вы должны запустить его с помощью запуска функции.

sf::Thread thread(&func);
thread.launch();

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

Остановка потоков

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

sf::Thread thread(&func);

// запуск потока
thread.launch();

...

// выполнение блокируется до тех пор, пока поток не завершится
thread.wait();

Функция ожидания также неявно вызывается деструктором sf::Thread, так что поток не может оставаться запущенным (и бесконтрольным) после того, как его родительский экземпляр sf::Thread уничтожается. Помните это, когда вы управляете вашими потоками (смотрите прошлую секцию статьи).

Приостановка потока

В SFML нет функции, которая бы предоставляла способ приостановки потока; единственный способ приостановки потока — сделать это из кода самого потока. Другими словами, вы можете только приостановить текущий поток. Что бы это сделать, вы можете вызвать функцию sf::sleep:

void func()
{
    ...
    sf::sleep(sf::milliseconds(10));
    ...
}

sf::sleep имеет один аргумент — время приостановки. Это время может быть выражено в любой единице, как было показано в статье про обработку времени.

Обратите внимание, что вы можете приостановить любой поток с помощью данной функции, даже главный поток.

sf::sleep является наиболее эффективным способом приостановить поток: на протяжении приостановки потока, он (поток) практически не потребляет ресурсы процессора. Приостановка, основанная на активном ожидании, вроде пустого цикла while, потребляет 100% ресурсов центрального процессора и делает… ничего. Однако имейте в виду, что продолжительность приостановки является просто подсказкой; реальная продолжительность приостановки (больше или меньше указанного вами времени) зависит от ОС. Так что не полагайтесь на эту функцию при очень точном отсчете времени.

Защита разделяемых данных

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

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

Наиболее распространенным (и используемым) примитивом является мьютекс. Мьютекс расшифровывается как «Взаимное исключение». Это гарантия, что только один поток может выполнять код. Посмотрим, как мьютексы работают, на примере ниже:

#include <SFML/System.hpp>
#include <iostream>

sf::Mutex mutex;

void func()
{
    mutex.lock();

    for (int i = 0; i < 10; ++i)
        std::cout << "I'm thread number one" << std::endl;

    mutex.unlock();
}

int main()
{
    sf::Thread thread(&func);
    thread.launch();

    mutex.lock();

    for (int i = 0; i < 10; ++i)
        std::cout << "I'm the main thread" << std::endl;

    mutex.unlock();

    return 0;
}

Этот код использует общий ресурс (std::cout), и, как мы видим, это приводит к нежелательным результатам. Вывод потоков смешался в консоли. Чтобы убедиться в том, что вывод правильно напечатается, вместо того, чтобы быть беспорядочно смешанным, мы защищаем соответствующие области кода мьютексом.

Первый поток, который достигает вызова mutex.lock(), блокирует мьютекс и получает доступ к коду, который печатает текст. Когда другие потоки достигают вызова mutex.lock(), мьютекс уже заблокирован, и другие потоки приостанавливают свое выполнение (это похоже на вызов sf::sleep, спящий поток не потребляет время центрального процессора). Когда первый поток разблокирует мьютекс, второй поток продолжает свое выполнение, блокирует мьютекс и печатает текст. Это приводит к тому, что текст в консоли печатается последовательно и не смешивается.

2.2 Потоки - 3

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

Защита мьютексов

Не волнуйтесь: мьютексы уже потокобезопасны, нет необходимости их защищать. Но они не безопасны в плане исключений. Что происходит, если исключение выбрасывается, когда мьютекс заблокирован? Он никогда не может быть разблокирован и будет оставаться заблокированным вечно. Все потоки, пытающиеся разблокировать заблокированный мьютекс, будут заблокированы навсегда. В некоторых случаях, ваше приложение будет «заморожено».

Чтобы быть уверенным, что мьютекс всегда разблокирован в среде, в которой он (мьютекс) может выбросить исключение, SFML предоставляет RAII класс, позволяющий обернуть мьютекс в класс sf::Lock. Блокировка происходит в конструкторе, разблокировка происходит в деструкторе. Просто и эффективно.

sf::Mutex mutex;

void func()
{
    sf::Lock lock(mutex); // mutex.lock()

    functionThatMightThrowAnException(); // mutex.unlock(), если функция выбросит исключение

} // mutex.unlock()

Помните, что sf::Lock может также быть использован в функциях, которые имеют множество возвращаемых значений.

sf::Mutex mutex;

bool func()
{
    sf::Lock lock(mutex); // mutex.lock()

    if (!image1.loadFromFile("..."))
        return false; // mutex.unlock()

    if (!image2.loadFromFile("..."))
        return false; // mutex.unlock()

    if (!image3.loadFromFile("..."))
        return false; // mutex.unlock()

    return true;
} // mutex.unlock()

Распространенные заблуждения

Вещь, часто упускаемая из виду: поток не может существовать без соответствующего экземпляра sf::Thread. Следующий код можно часто увидеть на форумах:

void startThread()
{
    sf::Thread thread(&funcToRunInThread);
    thread.launch();
}

int main()
{
    startThread();
    // ...
    return 0;
}

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

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

Так что не забывайте: вы должны управлять экземплярами sf::Thread, чтобы они жили так долго, как требуется функции потока.

Автор: HighMem

Источник

Поделиться новостью

* - обязательные к заполнению поля