0%

C++11多线程编程介绍

多线程编程是任何编程语言的一个重要方面,本文对C++11中的多线程编程库以及一些使用方法进行详细介绍。

1. 与 C++11 多线程相关的头文件

C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是:

<atomic>, <thread>, <mutex>, <condition_variable>, <future>

  • <atomic>:该头文主要声明了两个类, std::atomicstd::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。

  • <thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。

  • <mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。

  • <condition_variable>:该头文件主要声明了与条件变量相关的类,包括 std::condition_variablestd::condition_variable_any

  • <future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::futurestd::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

注意,C++11中没有关于信号量的头文件及相关变量的定义。

2. thread 的”Hello world”

下面是一个最简单的使用 std::thread 类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread> // std::thread

void thread_task() {
std::cout << "hello thread" << std::endl;
}


int main(int argc, const char *argv[])
{
std::thread t(thread_task);
t.join();

return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */

Makefile:(需要连接pthread, GCC4.6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
all:Thread

CC=g++
CPPFLAGS=-Wall -std=c++11 -ggdb
LDFLAGS=-pthread

Thread:Thread.o
$(CC) $(LDFLAGS) -o $@ $^

Thread.o:Thread.cc
$(CC) $(CPPFLAGS) -o $@ -c $^

.PHONY:
clean

clean:
rm Thread.o Thread

3. thread 详解

std::thread在头文件<thread>中声明,因此使用 std::thread 时需要包含<thread>头文件。

操作 <thread>函数
default(1) thread() noexcept
initialization (2) template <class Fn, class... Args>, explicit thread (Fn&& fn, Args&&... args)
copy [deleted] (3) thread (const thread&) = delete
move (4) thread (thread&& x) noexcept

(1). 默认构造函数,创建一个空的 thread 执行对象。

(2). 初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable新产生的线程会调用 fn 函数,该函数的参数由 args 给出

(3). 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造

(4). move 构造函数,move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。

注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached。

*主线程中执行joinable对象的join(),相当于在主线程中添加这些thread,也就是在这些添加的thread执行完毕之后才会继续执行主线程,类似嵌入代码。
join进入主线程的thread并不是从头执行,而是继续执行到完毕,或者说主线程等待其执行到完毕。相当于异步执行变为了同步执行。

线程的move操作

move: thread& operator= (thread&& rhs) noexcept;

  • thread类中的operator=是move操作,不是copy操作

  • 将rhs这个thread对象的状态赋值到*this中

    thread object whose state is moved to *this.

  • move 赋值操作,如果当前对象不可 joinable,需要传递一个右值引用(rhs)给 move 赋值操作;

  • 如果当前对象可被 joinable,则 terminate() 被调用。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>

#include <chrono> // std::chrono::seconds
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::sleep_for

// thread运行函数
void thread_task(int n) {
std::this_thread::sleep_for(std::chrono::seconds(n));
std::cout << "hello thread "
<< std::this_thread::get_id()
<< " paused " << n << " seconds" << std::endl;
}


int main(int argc, const char *argv[])
{
std::thread threads[5];
std::cout << "Spawning 5 threads...\n";

// 创建5个线程
for (int i = 0; i < 5; i++) {
// move-assign threads
threads[i] = std::thread(thread_task, i + 1);
}

std::cout << "Done spawning threads! Now wait for them to join\n";

// 5个线程依次join()
for (auto& t: threads) {
t.join();
}

std::cout << "All threads joined.\n";

return 0;
}

还有一些其余的常规函数,可以查阅手册
std::thread

4. mutex 详解

Mutex 又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在<mutex>头文件中,所以如果你需要使用std::mutex,就必须包含<mutex>头文件。

Linux的pthread下有pthread_mutex

mutex 相关头文件介绍

Mutex 系列类(四种)

  • std::mutex,最基本的 Mutex 类。
  • std::recursive_mutex,递归 Mutex 类。
  • std::time_mutex,定时 Mutex 类。
  • std::recursive_timed_mutex,定时递归 Mutex 类。

Lock 类(两种)

  • std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
  • std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

其他类型

  • std::once_flag
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

函数

  • std::try_lock,尝试同时对多个互斥量上锁。
  • std::lock,可以同时对多个互斥量上锁。
  • std::call_once,如果多个线程需要同时调用某个函数,call_once可以保证多个线程对该函数只调用一次。

mutex用法介绍

下面以std::mutex为例介绍 C++11 中的互斥量用法。

std::mutex是C++11 中最基本的互斥量,std::mutex对象提供了独占所有权的特性——即不支持递归地对std::mutex对象上锁,而std::recursive_lock则可以递归地对互斥量对象上锁。

std::mutex 成员函数

  • 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

  • lock()调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

    • (1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
    • (2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    • (3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(), 解锁,释放对互斥量的所有权。

  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,

    • (1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
    • (2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
    • (3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

mutex例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>       // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex

volatile int counter(0); // non-atomic counter
std::mutex mtx; // locks access to counter

void attempt_10k_increases() {
for (int i=0; i<10000; ++i) {
if (mtx.try_lock()) { // only increase if currently not locked:
++counter;
mtx.unlock();
}
}
}

int main (int argc, const char* argv[]) {
std::thread threads[10];
for (int i=0; i<10; ++i)
threads[i] = std::thread(attempt_10k_increases);

for (auto& th : threads) th.join();
std::cout << counter << " successful increases of the counter.\n";

return 0;
}

std::recursive_mutex 介绍

  • std::recursive_mutexstd::mutex一样,也是一种可以被上锁的对象,但是和std::mutex不同的是,std::recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权

  • std::recursive_mutex释放互斥量时需要调用与该锁层次深度相同次数的unlock(),可理解为lock()次数和unlock()次数相同,除此之外,std::recursive_mutex的特性和std::mutex大致相同。

std::time_mutex 介绍

std::time_mutexstd::mutex多了两个成员函数,try_lock_for()try_lock_until()

  • try_lock_for函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutextry_lock()不同,try_lock如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

    • 在指定时间内阻塞,知道获得锁。超时返回false
  • try_lock_until函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

time_mutext举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>       // std::cout
#include <chrono> // std::chrono::milliseconds
#include <thread> // std::thread
#include <mutex> // std::timed_mutex

std::timed_mutex mtx;

void fireworks() {
// waiting to get a lock: each thread prints "-" every 200ms:
while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
std::cout << "-";
}
// got a lock! - wait for 1s, then this thread prints "*"
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "*\n";
mtx.unlock();
}

int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(fireworks);

for (auto& th : threads) th.join();

return 0;
}

std::recursive_timed_mutex 介绍

std:recursive_mutexstd::mutex的关系一样,std::recursive_timed_mutex的特性也可以从std::timed_mutex推导出来。

lock_guard 用法介绍

与 Mutex RAII 相关,方便线程对互斥量上锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>       // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_error

std::mutex mtx;

void print_even (int x) {
if (x%2==0) std::cout << x << " is even\n";
else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
try {
// using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
std::lock_guard<std::mutex> lck (mtx);
print_even(id);
}
catch (std::logic_error&) {
std::cout << "[exception caught]\n";
}
}

int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_thread_id,i+1);

for (auto& th : threads) th.join();

return 0;
}

unique_lock 用法介绍

与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>       // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock

std::mutex mtx; // mutex for critical section

void print_block (int n, char c) {
// critical section (exclusive access to std::cout signaled by lifetime of lck):
std::unique_lock<std::mutex> lck (mtx);
for (int i=0; i<n; ++i) {
std::cout << c;
}
std::cout << '\n';
}

int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');

th1.join();
th2.join();

return 0;
}