Передача аргументов с использованием шаблона Variadic
Шаблоны Variadic позволяют определять объекты-функции, которые принимают различное количество объектов. Это очень удобно в приложениях с параллелизмом, поскольку мы можем использовать их для передачи нескольких аргументов в поток.
В части этой серии статей о параллелизме мы видели, что один из способов передать аргументы функции потока — это упаковать их в класс с помощью оператора вызова функции. Несмотря на то, что это работало хорошо, было бы очень обременительно писать специальный класс каждый раз, когда нам нужно передать данные в поток. Мы также можем использовать Lambda, которая захватывает аргументы, а затем вызывает функцию. Но есть более простой способ: конструктор потока можно вызвать с помощью функции и всех ее аргументов. Это возможно, потому что конструктор потока представляет собой вариативный шаблон, который принимает несколько аргументов.
До C++11 классы и функции могли принимать только фиксированное количество аргументов, которые нужно было указывать при первом объявлении. С вариативными шаблонами можно включать любое количество аргументов любого типа.
#include <iostream> #include <thread> #include <string> void printID(int id) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::cout << "ID = " << id << std::endl; } void printIDAndName(int id, std::string name) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "ID = " << id << ", name = " << name << std::endl; } int main() { int id = 0; // Define an integer variable // starting threads using variadic templates std::thread t1(printID, id); std::thread t2(printIDAndName, ++id, "MyString"); std::thread t3(printIDAndName, ++id); // this procudes a compiler error // wait for threads before returning t1.join(); t2.join(); //t3.join(); return 0; }
Как видно из приведенного выше примера кода, первый объект потока создается путем передачи ему функции printID
и целочисленного аргумента. Затем создается второй объект потока с функцией printIDAndName
, для которой требуется целое число и строковый параметр. Если бы потоку был предоставлен только один аргумент при вызове printIDAndName
, возникла бы ошибка компилятора (см. std::thread t3
в примере) — это та же проверка типов, которую мы получили бы при непосредственном вызове функции.
Есть еще одно различие между прямым вызовом функции и передачей ее потоку: в первом случае аргументы могут передаваться по значению, по ссылке или с использованием семантики перемещения — в зависимости от сигнатуры функции. При вызове функции с использованием вариативного шаблона аргументы по умолчанию либо перемещаются, либо копируются — в зависимости от того, являются ли они rvalue или lvalue. Однако есть способы, которые позволяют нам перезаписать это поведение. Например, если вы хотите переместить lvalue, мы можем вызвать std::move
. В следующем примере запускаются два потока, каждый из которых имеет свою строку в качестве параметра. С t1
строка name1 копируется по значению, что позволяет нам печатать name1 даже после вызова join
. Вторая строка name2
передается функции потока с использованием семантики перемещения, что означает, что она больше недоступна после вызова join
в t2
.
#include <iostream> #include <thread> #include <string> void printName(std::string name, int waitTime) { std::this_thread::sleep_for(std::chrono::milliseconds(waitTime)); std::cout << "Name (from Thread) = " << name << std::endl; } int main() { std::string name1 = "MyThread1"; std::string name2 = "MyThread2"; // starting threads using value-copy and move semantics std::thread t1(printName, name1, 50); std::thread t2(printName, std::move(name2), 100); // wait for threads before returning t1.join(); t2.join(); // print name from main std::cout << "Name (from Main) = " << name1 << std::endl; std::cout << "Name (from Main) = " << name2 << std::endl; return 0; }
В следующем примере сигнатура функции потока изменяется так, чтобы вместо нее использовалась неконстантная ссылка на строку.
#include <iostream> #include <thread> #include <string> void printName(std::string &name, int waitTime) { std::this_thread::sleep_for(std::chrono::milliseconds(waitTime)); name += " (from Thread)"; std::cout << name << std::endl; } int main() { std::string name("MyThread"); // starting thread std::thread t(printName, std::ref(name), 50); // wait for thread before returning t.join(); // print name from main name += " (from Main)"; std::cout << name << std::endl; return 0; }
При передаче имени строковой переменной в функцию потока нам нужно явно пометить его как ссылку, чтобы компилятор воспринял его как таковое. Это можно сделать с помощью функции std::ref
. В выводе консоли становится ясно, что строка была успешно изменена в функции потока перед передачей в main
.
Несмотря на то, что код работает, теперь мы делимся изменяемыми данными между потоками — это то, что мы обсудим в последующих разделах этого курса как основной источник ошибок параллелизма.
Запуск потоков с функциями-членами
В предыдущих разделах вы видели, как запускать потоки с функциями и функциональными объектами, с дополнительными аргументами и без них. Кроме того, теперь вы знаете, как передавать аргументы функции потока по ссылке. Но что, если мы хотим запустить функцию-член, отличную от оператора вызова функции, например функцию-член существующего объекта? К счастью, библиотека C++ может справиться с этим вариантом использования: для вызова функций-членов функция std::thread
требует дополнительного аргумента для объекта, для которого вызывается функция-член.
#include <iostream> #include <thread> class Vehicle { public: Vehicle() : _id(0) {} void addID(int id) { _id = id; } void printID() { std::cout << "Vehicle ID=" << _id << std::endl; } private: int _id; }; int main() { // create thread Vehicle v1, v2; std::thread t1 = std::thread(&Vehicle::addID, v1, 1); // call member function on object v std::thread t2 = std::thread(&Vehicle::addID, &v2, 2); // call member function on object v // wait for thread to finish t1.join(); t2.join(); // print Vehicle id v1.printID(); v2.printID(); return 0; }
В приведенном выше примере объект Vehicle v1 передается в функцию потока по значению, таким образом создается копия, которая не влияет на «оригинал», живущий в основном потоке. Таким образом, изменения в его переменной-члене _id
не будут отображаться при печати с вызовом printID()
позже в main. Вместо этого второй Vehicle
объект v2
передается по ссылке. Поэтому изменения в его переменной _id
также будут видны в потоке main
.
В предыдущем примере мы должны гарантировать, что существование v2
переживет завершение потока t2
, иначе будет попытка доступа к недействительному адресу памяти. Альтернативой является использование объекта, размещенного в куче, и указателя с подсчетом ссылок, такого как std::shared_ptr<Vehicle>
, чтобы гарантировать, что объект живет до тех пор, пока поток завершает свою работу. В следующем примере показано, как это можно реализовать:
#include <iostream> #include <thread> class Vehicle { public: Vehicle() : _id(0) {} void addID(int id) { _id = id; } void printID() { std::cout << "Vehicle ID=" << _id << std::endl; } private: int _id; }; int main() { // create thread std::shared_ptr<Vehicle> v(new Vehicle); std::thread t = std::thread(&Vehicle::addID, v, 1); // call member function on object v // wait for thread to finish t.join(); // print Vehicle id v->printID(); return 0; }