如何更好地使用C++线程
C + 11中引入了多线程支持。在C ++ 11之前,我们必须在C中使用POSIX线程或p线程库。尽管该库完成了工作,但缺少任何提供功能集的标准语言,都会导致严重的可移植性问题。 C ++ 11消除了所有这些,并为我们提供了std :: thread。线程类和相关函数在线程头文件中定义。
std :: thread是表示C ++中单个线程的线程类。要启动线程,我们只需要创建一个新的线程对象并将要调用的执行代码(即可调用对象)传递到该对象的构造函数中即可。创建对象后,将启动一个新线程,该线程将执行callable中指定的代码。
可调用对象可以是三个
函数指针
功能对象
Lambda表达式
定义callable之后,将其传递给构造函数。
线程池
在C ++中,线程池基本上是一个池,当我们要一起处理多个任务(同时运行多个线程)时,池中使用的线程数是固定的。当没有任务时,该线程在线程池中处于空闲状态,当任务到达时,该线程将被发送到线程池并分配给该线程。待处理的任务将保留在队列中,等待线程释放。在C ++中,没有用于线程池的特定库,但是它在API中提供了各种方法,程序员可以根据需要使用和创建一个方法。
等待线程完成
线程启动后,我们可能需要等待线程完成才能采取一些措施。例如,如果我们将初始化应用程序GUI的任务分配给线程,则需要等待线程完成以确保GUI正确加载。
要等待线程,请使用std :: thread :: join()函数。该函数使当前线程等待,直到由* this标识的线程完成执行。
例如,要阻塞主线程直到线程t1完成,我们将执行
线程睡眠
至少在指定的sleep_duration内阻止当前线程的执行。
由于调度或资源争用延迟,此功能可能会阻塞比sleep_duration更长的时间。
该标准建议使用稳定的时钟来测量持续时间。如果实现使用系统时钟代替,则等待时间也可能对时钟调整敏感。
1 #include <iostream>
2 #include <chrono>
3 #include <thread>
4
5 using namespace std;
6
7 int main()
8 {
9 cout << "Start..." << endl;
10
11 auto start = chrono::high_resolution_clock::now();
12 chrono::milliseconds ms(2000);
13
14 this_thread::sleep_for(ms);
15
16 auto end = chrono::high_resolution_clock::now();
17 chrono::duration<double, milli> elapsed = end - start;
18
19 cout << "elapsed: " << elapsed.count() << " ms" << endl;;
20
21 cout << "Start..." << endl;
22
23 chrono::nanoseconds ns(1000000000);
24 start = chrono::high_resolution_clock::now();
25 this_thread::sleep_for(ns);
26 end = chrono::high_resolution_clock::now();
27 elapsed = end - start;
28 cout << "elapsed: " << elapsed.count() << " ms" << endl;
29 }
Start...
elapsed: 2000.46 ms
Start...
elapsed: 1000.17 ms
线程同步
我将讨论如何等待带有条件变量,future,锁存和障碍的事件,以及如何使用它们来简化操作的同步。
RAII
资源获取即是初始化,或RAII(Resource Acquisition Is Initialization),是一种C ++编程技术,它绑定了必须在使用之前获取的资源的生命周期和一个对象生命周期,这些资源包括分配的堆内存,执行线程,打开的套接字,打开的文件,锁定的互斥锁,磁盘空间,数据库连接,即资源有限存在的所有对象。
RAII保证资源可用于可能访问该对象的任何函数,资源可用性是类不变的,从而消除了冗余的运行时测试。它还可以确保在其控制对象的生存期结束时,以获取的相反顺序释放所有资源。同样,如果资源获取失败(构造函数异常退出),则每个完全构造的成员和基对象获取的所有资源将以初始化的相反顺序释放。这利用了核心语言功能(对象生存期,范围退出,初始化顺序和堆栈展开)来消除资源泄漏,并确保异常安全。这种技术的另一个名称是范围绑定资源管理SBRM(Scope-Bound Resource Management)。
RAII可以总结如下:
- 将每个资源封装到一个类中
- 构造函数获取资源并建立所有类不变式,或者如果无法做到则抛出异常,
- 析构函数释放资源,并且从不抛出异常;
- 始终通过RAII类的实例来使用资源,
- 具有自动存储期限,或自身的临时寿命,或者
- 生命周期受自动,或临时对象的生命周期限制
通过移动语义,可以安全地在对象之间,不同范围之间,以及线程内外,转移资源所有权, 而且同时保持资源的安全性。
带有open()/ close(),lock()/ unlock()或init()/ copyFrom()/ destroy()成员函数的类是非RAII类的典型示例:
锁
在最后一个示例中,我需要同步对g_exceptions向量的访问,以确保一次仅一个线程可以推送一个新元素。为此,我使用了互斥锁和互斥锁。互斥锁是核心同步原语,在C ++ 11中,它在标头中具有四种形式。
互斥锁:提供核心功能lock()和unlock()以及无阻塞try_lock()方法,如果互斥锁不可用,则返回该方法。
- recursive_mutex:允许从同一线程多次获取互斥锁。
- timed_mutex:类似于互斥锁,但是它带有另外两个方法try_lock_for()和try_lock_until(),它们尝试在一段时间内或直到到达某个时刻之前获取互斥锁。
- recursive_timed_mutex:是timed_mutex和recusive_mutex的组合。
这是一个使用std :: mutex的示例(注意,前面提到的get_id()和sleep_for()帮助函数的使用)。
下面展示一些 内联代码片
。
1 #include <iostream>
2 #include <thread>
3 #include <mutex>
4 #include <chrono>
5
6 using namespace std;
7
8 mutex g_lock;
9
10 void func(int ms_value)
11 {
12 g_lock.lock();
13
14 cout << "entered thread " << this_thread::get_id() << endl;
15 chrono::milliseconds ms(ms_value);
16
17 auto start = chrono::high_resolution_clock::now();
18 this_thread::sleep_for(ms);
19 auto end = chrono::high_resolution_clock::now();
20 chrono::duration<double, milli> elapsed = end - start;
21
22 cout << "leaving thread " << this_thread::get_id() << endl;
23 cout << "elapsed: " << elapsed.count() << endl;
24 g_lock.unlock();
25 }
26
27 int main()
28 {
29 srand((unsigned int)time(0));
30
31 thread t1(func, 100);
32 thread t2(func, 50);
33 thread t3(func, 80);
34
35 t1.join();
36 t2.join();
37 t3.join();
38
39 return 0;
40 }
lock()和unlock()方法应该简单明了。第一个锁定互斥锁,如果该互斥锁不可用则阻塞,而第二个则解锁该互斥锁。
下一个示例显示了一个简单的线程安全容器(内部使用std :: vector)。该容器具有添加单个元素的add()和添加多个元素的addrange()之类的方法,并且在内部仅调用add()。
注意:但是,正如下面的注释所指出的,由于多种原因(包括使用va_args),这不是线程安全的容器。另外,dump()方法不应属于容器,在实际的实现中,它将是一个辅助功能(独立)。本示例的目的仅是讲授一些关于互斥的概念,而不是制作完整的,无错误的,线程安全的容器。
明确的锁定和解锁会导致问题,例如忘记解锁或获取锁的顺序不正确,这会生成死锁。该标准提供了几个类和函数来帮助解决此问题。包装器类允许以RAII样式一致地使用互斥锁,并在块范围内自动锁定和解锁。这些包装器是:
- lock_guard:在构造对象时,它将尝试获取互斥锁的所有权(通过调用lock()),而在对象被破坏时,它将自动释放互斥锁(通过调用unlock())。这是不可复制的类。
- unique_lock:是一种通用的互斥包装器,与lock_quard不同,它还提供了对延迟锁定,时间锁定,递归锁定,锁所有权的转移和条件变量的使用的支持。这也是不可复制的类,但是它是可移动的。
recursive_mutex
recursive_mutex是一个同步原语,可用于保护共享数据免受多个线程同时访问。
recursive_mutex提供了排他的,递归的所有权语义:
- 从成功调用lock或try_lock时开始,调用线程拥有recursive_mutex。在此期间,该线程可能会额外调用lock或try_lock。当该线程发出数量相等的解锁请求时,该线程释放recursive_mutex所有权。
- 当线程拥有recursive_mutex时,如果其他线程试图声明对recursive_mutex的所有权,则它们将被阻塞(用于调用锁),或接收到错误的返回值(对于try_lock)。
- 未指定recursive_mutex可能被锁定的最大次数,但是达到该次数后,对lock的调用将引发std :: system_error,而对try_lock的调用将返回false。
如果在某个线程仍然拥有recursive_mutex的情况下,将其销毁,则该程序的行为是不确定的。
lock_guard类
类lock_guard是一个互斥包装器,它提供了一种便捷的RAII风格的机制,
- 在作用域的持续时间内,拥有一个互斥体。
- 在对象被释放时,自动释放互斥锁。
1 #include <iostream>
2 #include <mutex>
3 #include <thread>
4 #include <stdexcept>
5
6 using namespace std;
7
8
9 mutex m2;
10
11 int func_with_exception(int val)
12 {
13 cout << this_thread::get_id() << ": func_with_exception" << endl;
14
15 if (val == 0)
16 throw invalid_argument( "received negative value" );
17
18 return 1;
19 }
20
21 bool everything_ok(int val)
22 {
23 cout << this_thread::get_id() << ": everything_ok" << endl;
24
25 if (val == 1)
26 return false;
27 return true;
28 }
29
30 void func_2(int val)
31 {
32 std::lock_guard<std::mutex> lk(m2); // RAII class: mutex acquisition is initialization
33 func_with_exception(val); // if f() throws an exception, the mutex is released
34 if(!everything_ok(val)) return; // early return, the mutex is released
35
36 cout << this_thread::get_id() << ": func_2 done" << endl;
37 }
38
39 void thread_func(int val)
40 {
41 try {
42 func_2(val);
43 }
44 catch (const std::invalid_argument& e)
45 {
46 cout << this_thread::get_id() << ": except" << endl;
47 }
48 }
49
50
51 int main()
52 {
53 thread t1(thread_func, 0);
54 thread t2(thread_func, 1);
55 thread t3(thread_func, 2);
56
57 t1.join();
58 t2.join();
59 t3.join();
60 }
输出结果
140587041294080: func_with_exception
140587041294080: except
140587032839936: func_with_exception
140587032839936: everything_ok
140587024385792: func_with_exception
140587024385792: everything_ok
140587024385792: func_2 done
创建lock_guard对象时,它将尝试获取它所拥有的互斥锁的所有权;当控制离开其创建lock_guard对象的作用域时,将释放lock_guard,并释放互斥量。
lock_guard类不可复制。
线程函数
函数指针
函数对象
Lambda表达式
1 #include <iostream>
2 #include <thread>
3 #include <vector>
4 #include <numeric>
5
6 using namespace std;
7
8 int main ()
9 {
10 vector<int> numbers(20);
11
12 thread iotaThread([&numbers](int startArg) {
13 iota(numbers.begin(), numbers.end(), startArg);
14 cout << "from: " << this_thread::get_id() << " thread id" << endl;
15 }, 10);
16
17 iotaThread.join();
18 cout << "numbers in main (id " << this_thread::get_id() << "):" << endl;
19 for (auto& num : numbers)
20 cout << num << ", ";
21 cout << endl;
22 }
23
Lambda,std::future与std :: async
模板函数std :: async
模板函数async异步运行函数f(可能在单独的线程中,该线程可能是线程池的一部分),并返回std :: future,该变量最终将保存该函数调用的结果。
1)std :: async的调用策略为std :: launch :: async,或std::launch::deferred。换句话说,当查询生成的std :: future的返回值时,函数 f 可能在另一个线程中执行,或者它可能同步运行。
2)根据特定的运行策略,带有参数args调用函数f:
- 如果设置了异步标志((policy & std :: launch :: async)!= 0),那么,async将在新的执行线程上执行可调用对象f,就像由std:: thread(std :: forward (f),std :: forward (args)…)生成一个线程一样。除非函数f返回值或引发异常,否则,将其存储在共享状态中,可通过std :: future访问,async将其返回给调用函数。
- 如果设置了延迟标志(即(policy&std :: launch :: deferred)!= 0),则异步将f和args转换为std :: thread构造函数,但不会产生新的执行线程。取而代之的是,执行惰性求值:对异步返回给调用者的std :: future上的非定时等待函数的第一次调用将导致f的副本与args的副本一起被调用(作为右值)。 。(也作为rvalues传递)在当前线程(不必是最初称为std :: async的线程)中。结果或异常处于与将来关联的共享状态,然后才准备就绪。对同一std :: future的所有其他访问将立即返回结果。
- 如果在策略中同时设置了std :: launch :: async和std :: launch :: deferred标志,则执行异步执行还是懒惰求值取决于实现。
- 如果std :: launch :: async或std :: launch :: deferred均未设置,或者在policy中未设置任何实现定义的策略标志,则该行为未定义。
在任何情况下,对std :: async的调用都会与对f的调用(如std :: memory_order中定义)同步-并且在使共享状态准备就绪之前,对f的完成进行排序。如果选择了异步策略,则关联的线程完成将与正在等待共享状态的第一个函数的成功返回,或与释放共享状态的最后一个函数的返回(以先到者为准)进行同步。如果std :: decay :: type或std :: decay :: type中的每个类型都无法从其对应的参数构造出来,则程序格式错误。
通过std :: async,使用多线程。我们将该功能与C ++ 11中的线程一起使用。这是一个高级API,可让您延迟或完全异步地设置和调用计算。
异步调用:
1 #include <iostream>
2 #include <thread>
3 #include <vector>
4 #include <future>
5 #include <numeric>
6
7 using namespace std;
8
9 int main()
10 {
11 vector<int> numbers(20);
12
13 future<void> iotaFuture = async(launch::async,
14 [&numbers]() {
15 iota(numbers.begin(), numbers.end(), 100);
16 cout << "calling from: " << this_thread::get_id() << " thread id" << endl;
17 });
18
19 iotaFuture.get(); // make sure we get the results...
20 cout << "numbers in main (id " << this_thread::get_id() << "):" << endl;
21
22 for (auto& num : numbers)
23 std::cout << num << ", ";
24 cout << endl;
25 }
26