优秀的编程知识分享平台

网站首页 > 技术文章 正文

c++20 语法与性能介绍 part 2(c++语法题)

nanyue 2024-08-10 18:35:14 技术文章 15 ℃

1.8 函数 - 1

std::function

std::function模板,在<functional>中定义,是一个多态函数包装器,可以用来创建一个类型,可以指向任何可调用的东西,如函数、函数对象或lambda表达式。std::function的实例可以用作函数指针,也可以用作函数实现回调的参数,并且可以存储、复制、移动,当然也可以执行。函数模板的模板参数看起来与大多数模板参数略有不同。其语法如下:

std::function<R(ArgTypes...)>

R是该函数的返回类型,而ArgTyppes是该函数的一个以逗号分隔的参数类型列表。示例:

void func(int num, string_view str)
{
     cout << format("func({}, {})", num, str) << endl;
}
int main()
{
    function<void(int, string_view)> f1 { func };
     f1(1, "test");
}

function可用于函数可以使用的任何场合,比如回调,特别是需要将回调存储为某个类的数据成员的时候。不过,可以使用C++20缩写的函数模板语法,比较简短快捷,如下实现:

void findMatches(span<const int> values1, span<const int> values2,
 auto matcher, auto handler)
{ /* ... */ }

Binders

Binder可用于将可调用对象的参数绑定到某些值。为此,您可以使用在<functional>中定义的std::bind(),它允许我们以一种灵活的方式绑定可调用的参数。可以将参数绑定到固定的值,甚至可以以不同的顺序重新排列参数。最好用一个例子来解释。假设有一个叫做func()的函数,它接受两个参数:

void func(int num, string_view str)
{
     cout << format("func({}, {})", num, str) << endl;
}
string myString { "abc" };
auto f1 { bind(func, placeholders::_1, myString) };
f1(16);

bind()也可以用来重新排列参数,如下面的代码所示。_2指定了当调用func()时,f2()的第一个参数需要去的位置。换句话说,f2()的第一个参数将成为func()的第二个参数,而f2()的第二个参数将成为func()的第一个参数:

auto f2 { bind(func, placeholders::_2, placeholders::_1) };
f2("Test", 32);
// output is:
// func(32, Test)

<functional>定义了std::ref()和cref()助手函数模板。它们可以分别用于将引用绑定到非常量和将引用绑定到常量。例如:

void increment(int& value) { ++value; }
int index { 0 };
increment(index);
// bind
auto incr { bind(increment, ref(index)) };
incr();

如果将绑定参数与重载的函数结合起来,就会有一个小问题。假设您有以下两个overloaded()函数。一个接受一个整数,另一个接受一个浮点数:

void overloaded(int num) {}
void overloaded(float f) {}

auto f3 { bind(overloaded, placeholders::_1) }; // ERROR
auto f4 { bind((void(*)(float))overloaded, placeholders::_1) }; // OK

另外,bind还可以用于将某个对象的方法作为回调参数:

class Handler
{
   public:
   void handleMatch(size_t position, int value1, int value2)
   {
       cout << format("Match found at position {} ({}, {})",
       position, value1, value2) << endl;
   }
};

Handler handler;
findMatches(values1, values2, intEqual, bind(&Handler::handleMatch, &handler,
 placeholders::_1, placeholders::_2, placeholders::_3));

LAMBDA EXPRESSIONS

必须创建一个函数或仿函数类,给它一个不与其他名称发生冲突的名称,然后使用这个名称,这对于一个简单的概念来说是相当大的开销。在这些情况下,使用由lambda表达式表示的所谓匿名(未命名)函数是一个很大的方便。Lambda表达式允许您内联编写匿名函数。它们的语法更简单,并且可以使您的代码更紧凑,更容易阅读。Lambda表达式对于定义内联传递给其他函数的小回调很有用,而不必定义在重载函数调用函数中实现的完整函数对象。这样,所有的逻辑都保持在一个地方,而且更容易理解和维护。Lambda表达式可以接受参数、返回值、模板化、通过值或引用从其封闭范围中访问变量等等。它有很大的灵活性,但是首先,让我们看一下lambda表达式的语法。

[ captures ] ( params ) specs requires??(optional) { body }	(1)	
[ captures ] attr ( params ) specs requires??(optional) { body }	(1)	(since C++23)
[ captures ] { body }	(2)	
[ captures ] attr specs { body }	(2)	(since C++23)
[ captures ] < tparams > requires??(optional) ( params ) specs requires??(optional) { body }	(3)	(since C++20)
[ captures ] < tparams > requires??(optional) attr ( params ) specs requires??(optional) { body }	(3)	(since C++23)
[ captures ] < tparams > requires??(optional) { body }	(4)	(since C++20)
[ captures ] < tparams > requires??(optional) attr specs { body }	(4)	(since C++23)

不建议使用捕获默认值,即使捕获默认值只捕获在lambda表达式主体中真正使用的那些变量。通过使用=捕获默认值,您可能会意外地导致一个昂贵的副本。通过使用&捕获默认值,您可能会意外地修改封闭范围中的变量。建议您明确地指定要捕获的变量以及如何捕获。

全局变量总是通过引用捕获,即使被要求按值捕获!例如,在下面的代码片段中,捕获默认值用于按值捕获所有内容。然而,全局变量全局,并在执行lambda后更改其值。

 int global { 42 };
 int main()
 {
   auto lambda { [=] { global = 2; } };
   lambda();
   // global now has the value 2!
 }

总结,一个lambda表达式的完整语法如下:

[capture_block] <template_params> (parameters) mutable constexpr
 noexcept_specifier attributes
 -> return_type requires {body}

除了捕获块和主体之外,所有内容都是可选的:

?Capture block:这也被称为lambda导引器,它可以指定如何捕获和提供封闭范围中的变量。

?Template parameters(C++20):这允许您编写模板化的lambda表达式,本章后面将进行讨论。

?Parameters:这是一个关于lambda表达式的参数列表。只有当不需要任何参数且不指定变量、约束符、说明符、属性、返回类型或必需子句时,才可以省略此列表。该参数列表类似于正常函数的参数列表。

?mutable:这将lambda表达式标记为可变的。

?constexpr:这将表达式标记为constexpr,因此可以在编译时进行计算。即使省略,如果一个表达式满足对constexpr函数的限制,那么它的表达式也是constexpr的。

?noexcept specifier:这可以用于指定noexcept子句,类似于正常函数的noexcept子句。

?Attributes:可用于指定lambda表达式指定属性。

?Return type:这是返回值的类型。如果省略这个,编译器根据与函数返回类型推导相同的规则推导返回类型。

?Requires clause(C++20):这将将模板类型约束添加到lambda闭包的函数调用操作符中。

Generic Lambda Expressions

可以对lambda表达式的参数使用自动类型推导,而不是显式地为它们指定具体的类型。要为参数指定自动类型推导,只将类型指定为auto。类型演绎规则与模板参数演绎规则相同。

// Define a generic lambda expression to find equal values.
auto areEqual { [](const auto& value1, const auto& value2) {
 return value1 == value2; } };
// Use the generic lambda expression in a call to findMatches().
vector values1 { 2, 5, 6, 9, 10, 1, 1 };
vector values2 { 4, 4, 2, 9, 0, 3, 1 };
findMatches(values1, values2, areEqual, printMatch);

这个泛型lambda表达式的编译器生成的仿函数,大致如下:

class CompilerGeneratedName
{
 public:
   template <typename T1, typename T2>
   auto operator()(const T1& value1, const T2& value2) const
   { return value1 == value2; }
};

Lambda Capture Expressions

Lambda捕获表达式允许您使用任何类型的表达式初始化捕获变量。它可以用于在lambda表达式中引入没有从封闭范围中捕获的变量。例如,下面的代码创建了一个lambda表达式。在这个lambda表达式中有两个可用的变量:一个是myCapture,使用lambda捕获表达式初始化为字符串:Pi;还有一个为pi,值捕获到闭包中。注意,使用捕获初始化器初始化的非引用捕获变量,如myCapture,都是复制构造的,这意味着const限定符被剥离。

double pi { 3.1415 };
auto myLambda { [myCapture = "Pi: ", pi]{ cout << myCapture << pi; } };

一个lambda捕获变量可以用任何类型的表达式进行初始化,因此也可以用std::move()进行初始化。这对于不能复制、只能移动的对象很重要,比如unique_ptr。默认情况下,按值捕获使用复制语义,因此不可能在lambda表达式中按值捕获unique_ptr。使用lambda捕获表达式,可以通过移动来捕获它,如本例中所示:

auto myPtr { make_unique<double>(3.1415) };
auto myLambda { [p = move(myPtr)]{ cout << *p; } };

Templated Lambda Expressions

C++20支持模板化的lambda表达式。这允许您更容易地访问泛型lambda表达式的参数的类型信息。例如,假设您有一个lambda表达式,它需要将一个vector作为参数传递。但是,vector中的元素类型可以是任何东西;因此,它是一个使用auto作为其参数的通用的lambda表达式。lambda表达式的主体想要找出vector中的元素的类型。在C++20之前,这只能通过使用decltype()和std::decay_t来实现,这是一种所谓的type trait。要知道decay_t从一个类型中删除了任何常量和引用修饰。下面是泛型的lambda表达式:

auto lambda { [](const auto& values) {
     using V = decay_t<decltype(values)>; // The real type of the vector.
     using T = typename V::value_type; // The type of the elements of the vector.
     T someValue { };
     T::some_static_function();
	} 
};

使用decltype()和decay_t是相当复杂的。一个模板化的lambda表达式使这一点变得更加容易。下面的lambda表达式强制其参数为一个vector,但仍然为vector的元素类型使用一个模板类型参数:

[] <typename T> (const vector<T>& values) {
 T someValue { };
 T::some_static_function();
}

还可以通过添加requires子句来对模板类型施加约束。下面是一个例子:

[] <typename T> (const T& value1, const T& value2) requires integral<T> {/* ... */}

Lambda Expressions as Return Type

通过使用std::function,可以从函数返回lambda表达式。请看看以下定义:

function<int(void)> multiplyBy2Lambda(int x)
{
 return [x]{ return 2 * x; };
}

可以使用auto关键字(函数返回类型推导)来使这更容易:

auto multiplyBy2Lambda(int x)
{
 return [x]{ return 2 * x; };
}

C++20允许在所谓的未评估的上下文中使用lambda表达式。例如,传递给decltype()的参数只在编译时使用,而从不计算。因此,以下内容在C++17或更高版本中无效,但自C++20之后有效:

using LambdaType = decltype([](int a, int b) { return a + b; });

Default Construction, Copying, and Assigning

从C++20开始,无状态的lambda表达式可以被默认构造、复制和赋值。下面是一个简单的例子:

auto lambda { [](int a, int b) { return a + b; } }; // A stateless lambda.
decltype(lambda) lambda2; // Default construction.
auto copy { lambda }; // Copy construction.
copy = lambda2; // Copy assignment.

using LambdaType = decltype([](int a, int b) { return a + b; }); // Unevaluated.
LambdaType getLambda()
{
 return LambdaType{}; // Default construction.
}

INVOKERS

std::invoke(),在<functional>中定义,可用于用一组参数调用任何可调用的对象。下面的示例使用了三次invoke():一次调用普通函数,一次调用lambda表达式,一次调用字符串实例上的成员函数:

void printMessage(string_view message) { cout << message << endl; }
int main()
{
   invoke(printMessage, "Hello invoke.");
   invoke([](const auto& msg) { cout << msg << endl; }, "Hello invoke.");
   string msg { "Hello invoke." };
   cout << invoke(&string::size, msg) << endl;
}

就其本身而言,invoke()并不是那么有用,因为您还可以直接调用该函数或lambda表达式。但是,在编写需要调用某些任意可调用对象的通用模板代码时,它非常有用。

1.9 variant, any,tuple

std::variant定义在<variant>中,可以给它赋值一个在给定类型集之一的某个值。在定义变体时,必须指定它可能包含的类型。例如,下面的代码定义了一个可以一次包含一个整数、一个字符串或一个浮点值的变体:

variant<int, string, float> v;
v = 12;
v = 12.5f;
v = "An std::string"s;

一个variant的模板类型参数必须是唯一的;像variant<int,int>的声明是错误的。您可以使用index()方法获取当前存储在变体中的值类型的基于零的索引,您可以使用std::holds_alternative()函数模板来找出变体当前是否包含某种类型的值。使用std::get<index>()或get<T>()从一个变体中检索值,其中索引是您要检索的类型的基于零的索引,而T是您要检索的类型。如果您使用的是类型或与变体中当前值不匹配的索引,这些函数会抛出一个bad_valan_varandaccess异常:

cout << "Type index: " << v.index() << endl;
cout << "Contains an int: " << holds_alternative<int>(v) << endl;
// output:
//Type index: 1
//Contains an int: 0

cout << get<string>(v) << endl;
try {
 cout << get<0>(v) << endl;
} catch (const bad_variant_access& ex) {
 cout << "Exception: " << ex.what() << endl;
}
// output
//An std::string
//Exception: bad variant access

要避免异常,请使用std::get_if<index>()get_if<T>()助手函数。这些函数接受一个指向变体的指针,并返回一个指向请求值的指针,或在错误时为nullptr:

string* theString { get_if<string>(&v) };
int* theInt { get_if<int>(&v) };
cout << "Retrieved string: " << (theString ? *theString : "null") << endl;
cout << "Retrieved int: " << (theInt ? *theInt : 0) << endl;
// output
//Retrieved string: An std::string
//Retrieved int: 0

有一个std::visit()助手函数,您可以使用它将所谓的访问者模式应用到一个变体。访问者必须是可调用的,它可以接受可能存储在变体中的任何类型。假设您有以下的访问者类

class MyVisitor
{
 public:
 void operator()(int i) { cout << "int " << i << endl; }
 void operator()(const string& s) { cout << "string " << s << endl; }
 void operator()(float f) { cout << "float " << f << endl; }
};

visit(MyVisitor{}, v);

any

std::any定义在<any>中,是一个可以包含任何类型的单个值的类。您可以使用任意构造函数或std::make_any()助手函数创建一个实例。一旦构建了它,您可以询问任何实例是否包含值以及包含的值的类型。要访问所包含的值,您需要使用any_cast(),它会在失败的情况下抛出bad_any_cast类型的异常。下面是一个示例:

any empty;
any anInt { 3 };
any aString { "An std::string."s };
cout << "empty.has_value = " << empty.has_value() << endl;
cout << "anInt.has_value = " << anInt.has_value() << endl << endl;
cout << "anInt wrapped type = " << anInt.type().name() << endl;
cout << "aString wrapped type = " << aString.type().name() << endl << endl;
int theInt { any_cast<int>(anInt) };
cout << theInt << endl;
try {
 int test { any_cast<int>(aString) };
 cout << test << endl;
} catch (const bad_any_cast& ex) {
 cout << "Exception: " << ex.what() << endl;
}
any something { 3 }; // Now it contains an integer.
something = "An std::string"s; // Now the same instance contains a string.

输出内容如下:

empty.has_value = 0
anInt.has_value = 1
anInt wrapped type = int
aString wrapped type = class std::basic_string<char,struct std::char_
traits<char>,class std::allocator<char> >
3
Exception: Bad any_cast

TUPLES

std::tuple在<tuple>中定义,是对pair的推广。它允许您存储任意数量的值,每个值都有自己的特定类型。与pair一样,元组具有固定的大小和固定的值类型,这是在编译时确定的。可以使用元组构造函数创建元组,同时指定模板类型和实际值。例如,下面的代码创建了一个元组,其中第一个元素是一个整数,第二个元素是一个字符串,最后一个元素是一个布尔值:

using MyTuple = tuple<int, string, bool>;
MyTuple t1 { 16, "Test", true };
cout << format("t1 = ({}, {}, {})", get<0>(t1), get<1>(t1), get<2>(t1)) << endl;
// Outputs: t1 = (16, Test, 1)

可以使用std::tuple_element类模板在编译时根据元素的索引获取元素的类型。tuple_element要求tuple指定元组的类型(在本例中是MyTuple),而不是像t1这样的实际元组实例。还可以根据std::get<T>()的类型从元组中检索元素,其中T是您要检索的元素类型,而不是索引。如果元组有几个具有请求类型的元素,编译器会生成错误。可以使用std::tuple_size类模板来查询元组的大小。与tuple_元素一样,tuple_size要求您指定元组的类型,而不是一个实际的元组:

cout << "Type of element with index 2 = "
 << typeid(tuple_element<2, MyTuple>::type).name() << endl;
// Outputs: Type of element with index 2 = bool

cout << "String = " << get<string>(t1) << endl;
// Outputs: String = Test

cout << "Tuple Size = " << tuple_size<MyTuple>::value << endl;
// Outputs: Tuple Size = 3

使用类模板参数推导(CTAD),可以在构造元组时省略模板类型参数,并让编译器根据传递给构造函数的参数类型自动推导这些参数。例如,下面定义了相同的t1元组,由一个整数、一个字符串和一个布尔值组成。注意,您现在必须指定“Test”s,以确保它是一个std::string

tuple t1 { 16, "Test"s, true };

由于类型的自动扣除,因此不能使用&来指定引用。如果您想使用类模板参数推论来生成包含引用到非常量或引用到常量的元组,则需要分别使用ref()或cref(),它们都在<functional>中定义。例如,下面的语句导致一个元组<int,double&, const double&, string&>:

double d { 3.14 };
string str1 { "Test" };
tuple t2 { 16, ref(d), cref(d), ref(str1) };

如果没有类模板参数推导,您可以使用std::make_tuple()函数模板来创建一个元组。由于它是一个函数模板,因此它支持函数模板参数推导,因此也允许您仅通过指定实际值来创建一个元组。这些类型是在编译时自动推导出来的。下面是一个示例:

auto t2 { make_tuple(16, ref(d), cref(d), ref(str1)) };

Decompose Tuples

有两种方法可以将一个元组分解为它的各个元素:结构化绑定(available since C++17)和std::tie()

// structured bindings
tuple t1 { 16, "Test"s, true };
auto [i, str, b] { t1 };
cout << format("Decomposed: i = {}, str = \"{}\", b = {}", i, str, b) << endl;
// decompose a tuple into references
auto& [i2, str2, b2] { t1 };
i2 *= 2;
str2 = "Hello World";
b2 = !b2;

// tie
tuple t1 { 16, "Test"s, true };
int i { 0 };
string str;
bool b { false };
cout << format("Before: i = {}, str = \"{}\", b = {}", i, str, b) << endl;
tie(i, str, b) = t1;
cout << format("After: i = {}, str = \"{}\", b = {}", i, str, b) << endl;
// output
Before: i = 0, str = "", b = false
After: i = 16, str = "Test", b = true

使用tie(),您可以忽略某些不希望被分解的元素。您使用特殊的std::ignore,而不是使用变量分解元素。例如,可以通过将前面示例中的tie()语句替换为以下内容来忽略t1元组的字句串元素:

tie(i, ignore, b) = t1;

您可以使用std::tuple_cat()将两个元组连接成一个元组。在下面的例子中,t3的类型是元组<int, string, bool, double, string>

tuple t1 { 16, "Test"s, true };
tuple t2 { 3.14, "string 2"s };
auto t3 { tuple_cat(t1, t2) };

元组支持所有的比较运算符。为了使比较运算符能够正常工作,存储在元组中的元素类型也应该支持它们。下面是一个示例:

tuple t1 { 123, "def"s };
tuple t2 { 123, "abc"s };
if (t1 < t2) {
 cout << "t1 < t2" << endl;
} else {
 cout << "t1 >= t2" << endl;
}
// output
t1 >= t2

可以使用std::make_from_tuple<T>来构造一个T类型的对象,该函数的参数是一个tuple对象。make_from_tuple会将给定元组的元素作为参数传递给T的构造函数。

class Foo
{
 public:
 		Foo(string str, int i) : m_str { move(str) }, m_int { i } {}
 private:
     string m_str;
     int m_int;
};
// 如下面的代码段,myTuple用来构建Foo类的对象
tuple myTuple { "Hello world.", 42 };
auto foo { make_from_tuple<Foo>(myTuple) };

从技术上讲,make_from_tuple()的参数不一定是元组,但它必须支持std::get<>()和tuple_size。数组和数对都满足这些要求。这个函数在日常使用中并不实用,但在使用模板和模板元编程编写通用代码时,它具有用场。

apply

std::apply()调用给定的可调用元素(函数、lambda表达式、函数对象等等),将给定元组的元素作为参数传递。下面是一个示例:

int add(int a, int b) { return a + b; }
...
cout << apply(add, tuple { 39, 3 }) << endl;

与make_from_tuple()一样,这个函数在使用模板和模板元编程编写通用代码时也比在日常使用时更有用。

1.10 thread

在<thread>中定义的C++线程库使启动新线程变得很容易。您可以通过几种方式指定需要在新线程中执行的内容。您可以让新线程执行一个全局函数、函数对象的操作符()、一个lambda表达式,甚至是某个类的实例的成员函数。

// 全局函数
void counter(int id, int numIterations)
{
 for (int i { 0 }; i < numIterations; ++i) {
 cout << "Counter " << id << " has value " << i << endl;
 }
}
thread t1 { counter, 1, 6 };
t1.join();

// 函数对象
class Counter
{
 public:
 Counter(int id, int numIterations)
 : m_id { id }, m_numIterations { numIterations } { }
 void operator()() const
 {
 for (int i { 0 }; i < m_numIterations; ++i) {
 cout << "Counter " << m_id << " has value " << i << endl;
 }
 }
 private:
 int m_id;
 int m_numIterations;
};
// Using uniform initialization syntax.
thread t1 { Counter{ 1, 20 } };
// Using named variable.
Counter c { 2, 12 };
thread t2 { c };
// Wait for threads to finish.
t1.join();
t2.join();

// lambda
int main()
{
 int id { 1 };
 int numIterations { 5 };
 thread t1 { [id, numIterations] {
 for (int i { 0 }; i < numIterations; ++i) {
 cout << "Counter " << id << " has value " << i << endl;
 }
 } };
 t1.join();
}

// 成员函数
class Request
{
 public:
 Request(int id) : m_id { id } { }
 void process() { cout << "Processing request " << m_id << endl; }
 private:
 int m_id;
};
int main()
{
 Request req { 100 };
 thread t { &Request::process, &req };
 t.join();
}

ATOMIC OPERATIONS LIBRARY

原子类型允许原子访问,这意味着允许并发读写而不需要额外的同步。如果没有原子操作,递增变量就不是线程安全的,因为编译器首先将值从内存加载到寄存器中,然后将其递增,然后将结果存储回内存中。在此增量操作期间,另一个线程可能会接触到相同的内存,这是一个数据竞争。这些原子类型是在<atomic>中定义的。C++标准为所有基本体类型定义了已命名的积分原子类型。


Mutex Classes

互斥体代表互斥。使用互斥锁的基本机制如下:

?一个希望使用与其他线程共享的(读/写)内存的线程试图锁定一个互斥锁对象。如果另一个线程当前持有此锁定,则希望获得访问块的新线程将直到锁定被释放或超时间隔到期。

?一旦线程获得锁定,就可以自由使用共享内存。当然,这假设所有想要正确使用共享数据的线程都获得互斥锁上的锁。

?在线程完成对共享内存的读写后,它会释放锁,让其他线程有机会获得对共享内存的锁。如果两个或两个以上的线程正在等待锁,则不能保证哪个线程将被授予锁,从而被允许继续进行。

C++标准库提供了非定时和定时互斥类型,具有递归和非递归的风格。在讨论所有这些选项之前,让我们先看看一个称为自旋锁的概念。

atomic_flag spinlock = ATOMIC_FLAG_INIT; // Uniform initialization is not allowed.
static const size_t NumberOfThreads { 50 };
static const size_t LoopsPerThread { 100 };
void dowork(size_t threadNumber, vector<size_t>& data)
{
   for (size_t i { 0 }; i < LoopsPerThread; ++i) {
     while (spinlock.test_and_set()) { } // Spins until lock is acquired.
     // Save to handle shared data...
     data.push_back(threadNumber);
     spinlock.clear(); // Releases the acquired lock.
   }
}
int main()
{
   vector<size_t> data;
   vector<thread> threads;
   for (size_t i { 0 }; i < NumberOfThreads; ++i) {
	   threads.push_back(thread { dowork, i, ref(data) });
   }
   for (auto& t : threads) {
  	 t.join();
   }
   cout << format("data contains {} elements, expected {}.\n", data.size(),
   NumberOfThreads * LoopsPerThread);
}

注意这个例子,由于旋锁使用繁忙的等待循环,只有当您知道线程只会锁定旋锁时,它们才应该是一个选项。此外,lock,还有unique_lock, share_lock, lock_guard, scoped_lock等,例子:

mutex mut1;
mutex mut2;
void process()
{
 unique_lock lock1 { mut1, defer_lock };
 unique_lock lock2 { mut2, defer_lock };
 lock(lock1, lock2);
 // Locks acquired.
} // Locks automatically released.

mutex mut1;
mutex mut2;
void process()
{
 scoped_lock locks { mut1, mut2 };
 // Locks acquired.
} // Locks automatically released.

可以使用std::call_once()和std::once_flag,以确保某个函数或方法被精确地调用一次,无论有多少线程尝试调用calle_flonce()。只有一个call_once()调用实际调用给定的函数或方法。如果给定的函数不抛出任何异常,则此调用被称为有效call_once()调用。如果给定的函数确实抛出了一个异常,则会将该异常传播回调用者,并选择另一个调用者来执行该函数。对特定的once_flag实例的有效调用在对同一once_flag上的所有其他call_once()调用之前结束。其他线程在相同的once_flag块上调用call_once(),直到有效调用完成。

示例代码:

once_flag g_onceFlag;
void initializeSharedResources()
{
   // ... Initialize shared resources to be used by multiple threads.
   cout << "Shared resources initialized." << endl;
}
void processingFunction()
{
   // Make sure the shared resources are initialized.
   call_once(g_onceFlag, initializeSharedResources);
   // ... Do some work, including using the shared resources
   cout << "Processing" << endl;
}
int main()
{
   // Launch 3 threads.
   vector<thread> threads { 3 };
   for (auto& t : threads) {
   		t = thread { processingFunction };
   }
   // Join on all threads
   for (auto& t : threads) {
   		t.join();
   }
}
// output
Shared resources initialized.
Processing
Processing
Processing

同步流C++20引入了std::basic_osyncstream,预定义的类型别名osyncstreamwosyncstream流,都在<同步流>中定义。这些类名中的O代表输出。这些类保证了在同步流被销毁的那一刻,通过它们完成的所有输出都会出现在最终的输出流中。它保证了输出不能与来自其他线程的其他输出相互交错。

class Counter
{
 public:
   Counter(int id, int numIterations)
   : m_id { id }, m_numIterations { numIterations } { }
   void operator()() const
   {
     	for (int i { 0 }; i < m_numIterations; ++i) {
       	osyncstream { cout } << "Counter "  << m_id << " has value " << i << endl;
	  	}
	 }
 private:
   int m_id;
   int m_numIterations;
};

// 或者这样实现()
void operator()() const
{
   for (int i { 0 }; i < m_numIterations; ++i) {
     osyncstream syncedCout { cout };
     syncedCout << "Counter " << m_id << " has value " << i << endl;
   }
}

使用Mutex

class Counter
{
 public:
   Counter(int id, int numIterations)
   : m_id { id }, m_numIterations { numIterations } { }
   void operator()() const
   {
     for (int i { 0 }; i < m_numIterations; ++i) {
       lock_guard lock { ms_mutex };
       cout << "Counter " << m_id << " has value " << i << endl;
   	}
   }
 private:
   int m_id;
   int m_numIterations;
   inline static mutex ms_mutex;
};

使用Timed Locks

class Counter
{
 public:
 Counter(int id, int numIterations)
   : m_id { id }, m_numIterations { numIterations } { }
   void operator()() const
   {
     for (int i { 0 }; i < m_numIterations; ++i) {
       unique_lock lock { ms_timedMutex, 200ms };
       if (lock) {
       		cout << "Counter " << m_id << " has value " << i << endl;
         } else {
       // Lock not acquired in 200ms, skip output.
       }
     }
   }
   private:
 int m_id;
 int m_numIterations;
 inline static timed_mutex ms_timedMutex;
};

双重检查锁定

void initializeSharedResources()
{
   // ... Initialize shared resources to be used by multiple threads.
   cout << "Shared resources initialized." << endl;
}

atomic<bool> g_initialized { false };
mutex g_mutex;

void processingFunction()
{
   if (!g_initialized) {
   unique_lock lock { g_mutex };
   if (!g_initialized) {
     initializeSharedResources();
     g_initialized = true;
     }
   }
   cout << "OK" << endl;
}
int main()
{
 vector<thread> threads;
 for (int i { 0 }; i < 5; ++i) {
 	threads.push_back(thread { processingFunction });
 }
 for (auto& t : threads) {
 	t.join();
 }
}

CONDITION VARIABLES

条件变量允许一个线程阻塞,直到另一个线程设置了某个条件,或者直到系统时间达到指定的时间。这些变量允许显式的线程间通信。如果您熟悉使用Win32 API的多线程编程,那么您可以在Windows中比较条件变量和事件对象。有两种条件变量可用,它们都在<condition_variable>中定义:

?std::condition_variable:一个条件变量,只能在unique_lock<mutex>等待,根据C++标准,它允许在某些平台上达到最大的效率。

?std::condition_variable_any:一个条件变量,可以等待任何类型的对象,包括自定义锁类型。

条件变量支持以下方法:

?notify_one();此方法唤醒等待此条件变量的一个线程。这类似于Windows中的自动重置事件。

?notify_all();此方法唤醒在此条件变量上等待的所有线程。

?wait(unique_lock<mutex>& lk);调用wait()的线程应该已经获得了一个在lk上的锁。调用wait()的效果是,它原子地调用lk.unlock(),然后阻塞线程,等待通知。当线程被notify_one()或另一个线程中的notify_all()调用解除阻塞时,函数再次调用lk.lock(),可能会阻塞直到获得锁定,然后返回。

?wait_for(unique_lock<mutex>& lk,const chrono::duration<Rep, Period>&

rel_time);此方法类似于wait(),除了线程被notify_one()、notify_all()或给定超时过期时解锁。

?wait_until(unique_lock<mutex>& lk, const chrono::time_point<Clock, Duration>& abs_time);此方法类似于wait(),除了线程通过notify_one()调用、notify_all()调用或当系统时间过去时来解锁。

还有一些版本的wait(), wait_for(), 以及wait_until()接受额外的谓词参数。例如,接受一个额外谓词的wait()的版本等同于以下内容:

while (!predicate())
   wait(lk);

condition_variable_any类支持与condition_variable相同的方法,除了它接受任何类型的锁类,而不只接受unique_lock<mutex>。所使用的锁类应该有一个lock()unlock()方法。

class SomeSafeQueue
{
private :
  		queue<string> m_queue;
  		mutex m_mutex;
  		condition_variable m_condVar;
public:
		void Enqueue(string && entry)
  	{
      	// Lock mutex and add entry to the queue.
        unique_lock lock { m_mutex };
        m_queue.emplace(forward(entry));
        // Notify condition variable to wake up thread.
        m_condVar.notify_all();
  	}
  
		void LoopInThread()
		{
      	unique_lock lock { m_mutex };
        while (true) {
           // Wait for a notification.
           m_condVar.wait(lock, [this]{ return !m_queue.empty(); });
           // Condition variable is notified, so something is in the queue.
           // Process queue item...
        }
    }
};

LATCHES

锁存器(闩锁)是一个一次性的线程协调点。在一个锁存点处的许多线程块。一旦给定数量的线程到达锁存点,所有线程都被阻塞,并允许继续执行。基本上,它是一个计数器,每个线程到达锁点倒计时。一旦计数器达到0,锁存器就无限期地保持在信号状态,所有阻塞线程都被未阻塞,并且随后到达锁存器点的任何线程都立即被允许继续。

latch startLatch { 1 };
vector<jthread> threads;
for (int i { 0 }; i < 10; ++i) {
 threads.push_back(jthread { [&startLatch] {
 // Do some initialization... (CPU bound)
 // Wait until the latch counter reaches zero.
 startLatch.wait();
 // Process data...
 } });
}
// Load data... (I/O bound)
// Once all data has been loaded, decrement the latch counter
// which then reaches zero and unblocks all waiting threads.
startLatch.count_down();

BARRIERS

屏障(障碍)是由一系列相位组成的可重用的线程协调机制。在屏障点上有许多线程块。当给定数量的线程到达屏障时,执行一个阶段完成回调,所有阻塞线程被解除阻塞,线程计数器被重置,下一个阶段开始。在每个阶段中,可以调整下一阶段的预期线程数量。障碍可以执行循环之间的同步。

例如,假设有多个线程并发运行,并在循环中执行一些计算。进一步假设,一旦这些计算完成,您需要在线程开始其循环的新迭代之前对结果做一些操作。在这种情况下,障碍是完美的。所有的线程都在障碍物上阻塞。当它们都到达时,您的阶段完成回调将处理线程的结果,然后解除阻塞所有线程以开始它们的下一次迭代。

void completionFunction() noexcept { /* ... */ }
int main()
{
   const size_t numberOfThreads { 4 };
   barrier barrierPoint { numberOfThreads, completionFunction };
   vector<jthread> threads;
   for (int i { 0 }; i < numberOfThreads; ++i) {
       threads.push_back(jthread { [&barrierPoint] (stop_token token) {
       		while (!token.stop_requested()) {
             // ... Do some calculations ...
             // Synchronize with other threads.
             barrierPoint.arrive_and_wait();
             }
       } });
	}
}

SEMAPHORES

信号量是轻量级的同步原语,可以用作其他同步机制的构建块,如互变量、闩锁和障碍。基本上,一个信号量是由一个表示多个插槽的计数器组成的。该计数器在构造函数中被初始化。如果您获得了一个插槽,计数器会减少,而释放一个插槽会增加计数器。在<semaphore>中定义了两个信号量类:std::counting_semaphorebinary_semaphore。前者模拟了一个非负的资源计数。后者只有一个插槽;因此,这个插槽要么是自由的,要么是不自由的,非常适合作为互斥锁的构建块。

计数信号量允许您精确地控制您希望允许并发运行多少个线程。例如,以下代码片段最多允许四个线程并行运行(虽然创建了10个线程,但只能同时运行四个):

counting_semaphore semaphore { 4 };
vector<jthread> threads;
for (int i { 0 }; i < 10; ++i) {
   threads.push_back(jthread { [&semaphore] {
       semaphore.acquire();
       // ... Slot acquired ... (at most 4 threads concurrently)
       semaphore.release();
	 } });
}

std::promise and std::future

future可以用来更容易地从一个线程中获取结果,并将异常从一个线程传输到另一个线程,然后它就可以以自己想要的方式处理异常。当然,为了防止它们离开线程,总是尝试尽可能多地处理实际线程中的异常仍然是一个很好的做法。

promise是指线程存储其结果的东西。未来用于访问存储在promise中的结果。也就是说,promise是结果的输入端,未来是输出端。一旦在同一个线程或另一个线程中运行的函数计算出了它想要返回的值,它就可以将这个值放在一个promise中。然后可以通过future检索这个值。您可以将此机制看作是一个可获得结果的线程间通信通道。

void doWork(promise<int> thePromise)
{
   // ... Do some work ...
   // And ultimately store the result in the promise.
   thePromise.set_value(42);
}
int main()
{
   // Create a promise to pass to the thread.
   promise<int> myPromise;
   // Get the future of the promise.
   auto theFuture { myPromise.get_future() };
   // Create a thread and move the promise into it.
   thread theThread { doWork, move(myPromise) };
   // Do some more work...
   // Get the result.
   int result { theFuture.get() };
   cout << "Result: " << result << endl;
   // Make sure to join the thread.
   theThread.join();
}

这段代码只是为了演示的目的。它在一个新线程中开始计算,然后调用get(),这将阻塞直到结果计算出来。这听起来像是一个昂贵的函数调用。在实际应用程序中,您可以通过定期检查是否有可用的结果(如使用wait_for())或使用条件变量等同步机制来使用futures。当结果还不可用时,您可以同时做其他事情,而不是阻塞。

std::packaged_task

std::packaged_task让我们更容易的使用promises,如前一节所述。下面的代码演示了这一点。它创建了一个打包的任务来执行calculateSum()。通过调用get_future(),从packaged_task中检索future。启动一个线程,并将packaged_task移动到其中。无法复制packaged_task!在线程启动后,在检索到的future上调用get()来获得结果。这将阻塞,直到结果可用。

int calculateSum(int a, int b) { return a + b; }
int main()
{
   // Create a packaged task to run calculateSum.
   packaged_task<int(int, int)> task { calculateSum };
   // Get the future for the result of the packaged task.
   auto theFuture { task.get_future() };
   // Create a thread, move the packaged task into it, and
   // execute the packaged task with the given arguments.
   thread theThread { move(task), 39, 3 };
   // Do some more work...
 
   // Get the result.
   int result { theFuture.get() };
   cout << result << endl;
   // Make sure to join the thread.
   theThread.join();
}

std::async

如果您想让C++运行时更多地控制是否创建一个线程来计算某个东西,您可以使用std::async()。它接受一个要执行的函数,并返回一个可以用于检索结果的future。有两种方法,async()可以运行一个函数:

?通过运行一个函数在一个单独的线程异步执行。

?运行一个函数调用,在调用线程上同步调用,当你在返回的future上调用get()的时候(同步)。

如过在调用async时没有其他参数,运行时自动选择两个方法,这取决于这些因素,如CPU核心的数量,和在您的系统已经并发执行的task数量。你可以通过指定一个策略参数影响运行时的行为:

?launch::async:强制运行时执行函数异步在不同的线程

?launch::deferred:强制运行时同步调用函数,当get()被调用时。

?launch::async | launch::deferred:让运行时选择(=默认行为)下面的例子演示了使用async()

int calculate() { return 123; }
int main()
{
   auto myFuture { async(calculate) };
   //auto myFuture { async(launch::async, calculate) };
   //auto myFuture { async(launch::deferred, calculate) };
   // Do some more work...
   // Get the result.
   int result { myFuture.get() };
   cout << result << endl;
}

在析构函数中对async()块的调用返回的future,直到其结果可用。这意味着,如果您调用async()而没有捕获返回的future,那么async()调用就有效地变成了一个阻塞调用!例如,下面的行,将同步调用calculate()

async(calculate);

这个语句的情况是async()创建并返回一个future。这个future并没有被捕捉到,所以它是一个匿名的future。因为它是一个临时的future,所以它的析构函数在这个语句的末尾被调用,这个析构函数将阻塞,直到结果可用。

Exception Handling

使用futures的一个很大优点是它们可以在线程之间传输异常。在future上调用get()要么返回计算结果,要么重新抛出存储在链接到future的promise中的任何异常。当您使用packaged_taskasync()时,从启动的函数抛出的任何异常都将自动存储在promise中。如果您直接使用std::promise作为promise,您可以调用set_exception()在其中存储异常。下面是一个使用async()的例子:

int calculate()
{
 	throw runtime_error { "Exception thrown from calculate()." };
}
int main()
{
   // Use the launch::async policy to force asynchronous execution.
   auto myFuture { async(launch::async, calculate) };
   // Do some more work...
   // Get the result.
   try {
     int result { myFuture.get() };
     cout << result << endl;
   } catch (const exception& ex) {
	   cout << "Caught exception: " << ex.what() << endl;
   }
}

std::shared_future

std::future<T>只需要T是可移动构造的。当您在future<T>上调用get()时,结果将被移出future并返回给您。这意味着您只能在future<T>上调用get()一次。

如果您希望能够多次调用get(),甚至从多个线程,那么您需要使用std::shared_future<T>,在这种情况下,T需要是可复制构造的。可以通过使用std::future::share()或通过向shared_future传递future相应的构造函数来创建shared_future。请注意,future是不可复制的,所以您必须将它移动到shared_futurey构造函数中。

promise<void> thread1Started, thread2Started;

promise<int> signalPromise;
auto signalFuture { signalPromise.get_future().share() };
//shared_future<int> signalFuture { signalPromise.get_future() };

auto function1 { [&thread1Started, signalFuture] {
   thread1Started.set_value();
   // Wait until parameter is set.
   int parameter { signalFuture.get() };
   // ...
} };

auto function2 { [&thread2Started, signalFuture] {
   thread2Started.set_value();
   // Wait until parameter is set.
   int parameter { signalFuture.get() };
   // ...
} };

// Run both lambda expressions asynchronously.
// Remember to capture the future returned by async()!
auto result1 { async(launch::async, function1) };
auto result2 { async(launch::async, function2) };

// Wait until both threads have started.
thread1Started.get_future().wait();
thread2Started.get_future().wait();

// Both threads are now waiting for the parameter.
// Set the parameter to wake up both of them.
signalPromise.set_value(42);

COROUTINES

协程是一个函数,可以在执行期间暂停,并在稍后的时间点恢复。任何具有以下内容之一的函数都是协程:

?co_await:在等待计算完成时暂停协例程的执行。当计算完成后,将继续执行。

?co_return:从相关程序返回(在相关程序中不允许仅返回)。在此之后不能恢复协程序。

?co_yield:将相关程序的值返回给调用者,并挂起相关程序,随后再次调用相关程序,在挂起的位置继续执行。

一般来说,有两种类型的协程:全堆栈和无堆栈。堆栈的协程可以从嵌套调用内部的任何地方挂起。另一方面,一个无堆栈的协程只能从顶部的堆栈框架上悬挂。当暂停无堆栈协程序时,只保存函数主体中具有自动存储持续时间的变量和临时变量;不保存调用堆栈。因此,对于无堆栈协程的内存使用是最小的,允许数百万甚至数十亿个协例程并发运行。C++20只支持无堆栈的变体。

协程可用于使用同步编程方式实现异步操作。用例包括以下内容:

?生成器

?异步I/O

?惰性计算

?事件驱动的应用程序

例如vc++2019支持的std::experimental::generator

experimental::generator<int> getSequenceGenerator( int startValue, int numberOfValues)
{
   for (int i { startValue }; i < startValue + numberOfValues; ++i) {
       // Print the local time to standard out, see Chapter 22.
       time_t tt { system_clock::to_time_t(system_clock::now()) };
       tm t;
       localtime_s(&t, &tt);
       cout << put_time(&t, "%H:%M:%S") << ": ";
       // Yield a value to the caller, and suspend the coroutine.
       co_yield i;
   }
}
int main()
{
   auto gen { getSequenceGenerator(10, 5) };
   for (const auto& value : gen) {
       cout << value << " (Press enter for next value)";
       cin.ignore();
   }
}

When you run the application, you’ll get the following output:
16:35:42: 10 (Press enter for next value)
Pressing Enter adds another line:
16:35:42: 10 (Press enter for next value)
16:36:03: 11 (Press enter for next value)
Pressing Enter again adds yet another line:
16:35:42: 10 (Press enter for next value)
16:36:03: 11 (Press enter for next value)
16:36:21: 12 (Press enter for next value)

协程调用流程如图

协程与子例程/函数对比

C++标准不关心协程如何实现,而只陈述其行为。因此,协程的实现完全依赖于编译器和库的编写者。要了解这一点,您可以检查std::coroutine_handle::resume()的库实现,它使用GCC编译器提供的__builtin_coro_resume。证明库和编译器是紧密耦合的。

我们可以控制不同线程中的协程序的执行,例如以下代码:

Tags:

最近发表
标签列表