Управление потоками

Автор работы: Пользователь скрыл имя, 17 Июня 2013 в 11:58, реферат

Описание работы

Допустим, вы решили написать параллельную программу, а конкретно - использовать несколько потоков. Как в этом случае нужно поступить? Как запустить потоки, как узнать что поток завершился, и как отследить его выполнение? Средства, появившиеся в стандартной библиотеке с выходом нового стандарта С++11, позволяют относительно просто решить эти задачи. Для решения сложных задач библиотека позволяет построить то, что нужно из простейших кирпичиков.

Файлы: 1 файл

Управление потоками.docx

— 46.24 Кб (Скачать файл)

Управление потоками

 

Допустим, вы решили написать параллельную программу, а конкретно - использовать несколько потоков. Как  в этом случае нужно поступить? Как  запустить потоки, как узнать что поток завершился, и как отследить его выполнение? Средства, появившиеся в стандартной библиотеке с выходом нового стандарта С++11, позволяют относительно просто решить эти задачи. Для решения сложных задач библиотека позволяет построить то, что нужно из простейших кирпичиков. 

 

Базовые операции управления потоками 

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

 

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

     В простейшем случае для запуска потока следует сконструировать объект std::thread, который определяет, какая задача будет исполняться в потоке. Обычно задача представляет собой простую функцию без параметров, возвращающую void. Эта функция работает в своём потоке, пока не вернёт управление, и в этот момент поток завершается. С другой стороны в роли задачи может выступить объект-функция, который принимает дополнительные параметры и выполняет ряд независимых операций, информацию о которых получает во время работы от той или иной системы передачи сообщений. И останавливается такой поток, когда получит соответствующий сигнал. Вне зависимости от того, что поток будет делать и откуда он запускается, сам запуск потока в стандартном С++ всегда сводится к конструированию объекта std::thread:  

    

     void do_some_work(); 

    std::thread my_thread(do_some_work); 

    Класс std::thread работает с любым типом, допускающим вызов, поэтому конструктору std::thread можно передать экземпляр класса, в котором определён оператор вызова: 

    class background_task 

    { 

     public: 

         void operator()() const 

         { 

              do_something(); 

              do_something_else();          

}     

}; 

     

    background_task f; 

    std::thread my_thread(f);

 

 

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

    Синтаксически передача конструктору временного объекта вместо именованной переменной выглядит так же, как объявление функции, и именно так компилятор и интерпретирует эту конструкцию. Например, в коде 

    std::thread my_thread(background_task());

объявлена функция my_thread, принимающая один параметр и возвращающая std::thread. Никакой новый поток здесь не запускается. Решить эту проблему можно тремя способами: сделать как в примере выше; добавить пару скобок или воспользоваться новым универсальным синтаксисом инициализации: 

     1) std::thread my_thread((background_task()))

     2) std::thread my_thread{background_task()}; 

     В случае (1) наличие дополнительных скобок не даёт компилятору интерпретировать конструкцию как объявление функции, так что объявляется переменная my_thread типа std::thread. В случае (2) новый синтаксис так же приводит к объявлению переменной. 

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

    

          std::thread my_thread([]( 

         do_something(); 

         do_something_else(); 

    ));

 

 

      После запуска потока необходимо явно решить, ждать его завершения или нет. Если это решение не будет принято к моменту уничтожения объекта std::thread, то программа завершится (деструктор std::thread вызовет функцию std::terminate()). Поэтому необходимо гарантировать, что поток корректно присоединён или отсоединён. Решение необходимо принять именно до уничтожения объекта std::thread, к самому потоку оно не имеет отношения. Поток вполне может завершиться задолго до того, как программа присоединится к нему или отсоединит его.  А отсоединённый поток может продолжать работу и после уничтожения объекта std::thread. 

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

     Например, такая проблема возникает, если функция потока хранит указатели или ссылки на локальные переменные, и поток ещё не завершился, когда произошёл выход из области видимости, где эти переменные определены. 

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

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

    

Ожидание завершения потока 

    Чтобы дождаться завершения потока, следует вызвать функцию join() ассоциированного объекта std::thread. Вызывая эту функцию, мы гарантируем, что поток завершится раньше выхода из функции, то есть раньше, чем будут уничтожены локальные переменные. В данном случае это значит, что запускать функцию в отдельном потоке не имело смысла, так как первый поток в это время ничего не делает, но в реальной программе исходный поток мог бы либо сам делать что-то полезное, либо запустить несколько потоков параллельно, а потом дождаться их всех. 

     Функция join() даёт очень простую и прямолинейную альтернативу - либо мы ждём завершения потока, либо нет. Если необходим более точный контроль над ожиданием потока, например если необходимо проверить, завершился ли поток, или ждать только ограниченное время, то следует прибегнуть к другим методам, таким, как условные переменные и будущие результаты. Кроме того, при вызове join() очищается вся ассоциированная с потоком память, так что объект std::thread более не связан с завершившимся потоком - он вообще не связан ни с каким потоком. Это значит, что для каждого потока вызвать функцию join() можно только один раз.

 

Ожидание в  случае исключения  

     Если вы хотите отсоединить поток, то обычно достаточно вызвать detach() сразу после его запуска, так что здесь проблемы не возникает. Но если вы собираетесь дождаться завершения потока, то надо тщательно выбирать место, куда поместить вызов join(). Важно, чтобы из-за исключения, произошедшего между запуском потока и вызовом join(), не оказалось, что обращение к join() вообще окажется пропущенным. 

     Чтобы приложение не завершилось аварийно при возникновении исключения, необходимо решить, что делать в этом случае. Если вы собирались вызвать join() при нормальном выполнении программы, то следует вызвать её и в случае исключения, чтобы избежать проблем, связанных с временем жизни. 

     

    struct func; 

     

    void f() 

    { 

         int some_local_state = 0; 

         func my_func(some_local_state); 

         std::thread t(my_func); 

         try 

         { 

              do_something_in_current_thread(); 

         } 

         catch(...) 

         { 

              t.join(); 

              throw; 

         } 

         t.join();     

}

 

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

     Один из способов решить эту задачу - воспользоваться стандартной идиомой захват ресурса есть инициализация (RAII) и написать класс, который вызывает join() в деструкторе, например такой: 

     

    class thread_guard 

    { 

         std::thread& t; 

    public: 

         explicit thread_guard(std::thread& t_) : t(t_) {} 

         ~thread_guard() 

         { 

              if (t.joinable()) 

                   t.join();          

         thread_guard(thread_guard const&)=delete;  

         thread_guard& operator=(thread_guard const&)=delete;        

}; 

     

    struct func; 

    void f() 

    { 

         int some_local_state;          

std::thread t(func(some_local_state)); 

         thread_guard g(t); 

         do_something_in_current_thread(); 

    }

 

Когда текущий поток доходит  до конца f, локальные объекты уничтожаются в порядке, обратном тому, в котором  были сконструированы. Следовательно, сначала уничтожается g, и в его  деструкторе происходит присоединение  к потоку. В любом случае. 

     Копирующий конструктор и оператор присваивания помечены =delete, чтобы компилятор не сгенерировал их автоматически: копирование или присваивание такого объекта таит в себе опасность, поскольку время жизни копии может оказаться дольше, чем время жизни присоединяемого потока. Но раз эти функции объявлены как "удалённые", то любая попытка скопировать объект типа thread_guard приведёт к ошибке компиляции. 

    

Запуск потоков  в фоновом режиме 

     Вызов detach() объекта std::thread оставляет поток работать в фоновом режиме, без прямых способов коммуникации с ним. Теперь ждать завершения потока не получится  - после того как поток отсоединён, уже невозможно получить ссылающийся на него std::thread, для которого можно было бы вызвать join(). Отсоединённые потоки действительно работают в фоне благодаря библиотеки времени выполнения С++. 

     Отсоединённые потоки часто называют потоками-демонами по аналогии с процессами-демонами в UNIX.  

     Разумеется, чтобы отсоединить поток от объекта std::thread, поток должен существовать: нельзя вызвать detach() для std::thread, с которым не связан никакой поток. 

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

     Набросок кода, реализующего данный подход:

 

     void edit_document(string const& filename) 

    { 

         open_document_and_display_gui(filename);  

         while (!done_editing()) 

         { 

              usr_command cmd = get_usr_input(); 

              if (cmd.type == open_new_document) 

              { 

                   string const new_name = get_filename_from_usr();  

                   std::thread t(edit_document, new_name); // 1 

                   t.detach(); // 2  

              } 

              else 

                   process_usr_input(cmd);          

}     

}

 

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

 

 

 

Передача аргументов функции потока  

     По существу передача аргументов вызываемому объекту или функции сводится просто к передаче дополнительных аргументов конструктору std::thread, но важно помнить, что по умолчанию эти аргументы копируются в память объекта, где они доступны вновь созданному потоку, причём так происходит так происходит даже в том случае, когда функция ожидает на месте параметра ссылку.  

     Пример: 

    

     void f(int i, string const& s); 

    std::thread t(f, 3, "hello"); 

     

Здесь создаётся новый  поток, ассоциированный с t, в котором  вызывается функция f(3, "hello"). Функция  принимает в качестве второго  параметра  объект типа string, но мы передаём строковый литерал char const*, который преобразуется к типу string уже в контексте нового потока. Это особенно важно, когда переданный аргумент является указателем на автоматическую переменную, как в примере: 

Информация о работе Управление потоками