C++ 学习笔记7

cpp_7

线程

线程是进程中的一个实体,是CPU调度的基本单位。一个进程中可以包含多个线程,每个线程都有自己的执行路径和执行状态。线程共享进程的资源,如内存空间、文件描述符等,但每个线程有自己的栈空间和寄存器上下文。

线程可以并发执行,多个线程可以同时运行在多个CPU上或者在同一个CPU上通过时间片轮转的方式交替执行。线程之间可以共享数据,可以通过共享内存或消息传递等方式进行通信。

线程的优势是能够提高程序的并发性和响应速度,可以同时执行多个任务,提高程序的性能。然而,线程之间的共享资源也会带来一些问题,如竞争条件和死锁等,需要通过同步机制来解决。

线程是在同一时间并行执行代码片段。

C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。

头文件:#include <thread>

线程类:std::thread

创建线程/构造函数

用于创建线程对象。

  1. thread() noexcept; 默认构造函数。构造一个线程对象,不执行任何任务(不会创建/启动子线程)。

  2. 有参构造函数

    1
    2
    template< class Function, class... Args >
    explicit thread(Function&& fx, Args&&... args);

    创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。

    任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数
    使用类的非静态成员函数作为参数时,在此之前需要创建类对象,确保类对象的声明周期比线程长,因为类的普通成员函数可能访问类对象的成员变量,如果对象销毁了,内存可能发生泄漏。格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Test
    {
    public:
    void func(string str, int num){}
    };
    int main
    {
    Test t;
    thread t1(&Test::func, &t, "str", 5);
    }
  3. thread(const thread& ) = delete;

    拷贝构造函数被删除,不允许线程对象之间的拷贝。

  4. thread(thread&& other ) noexcept;
    有移动构造函数,将线程other的资源所有权转移给新创建的线程对象。

  5. 赋值函数

    1
    2
    thread& operator= (thread&& other) noexcept;
    thread& operator= (const other&) = delete;

    线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。

注意

  • 先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。

  • 线程的任务函数返回后,子线程将终止。

  • 如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。

代码示例:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <iostream>
#include <thread> // 线程类头文件。
#include <windows.h> // Sleep()函数需要这个头文件。
using namespace std;

// 普通函数。
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}

// 仿函数。
class mythread1
{
public:
void operator()(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
};

// 类中有静态成员函数。
class mythread2
{
public:
static void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
};

// 类中有普通成员函数。
class mythread3
{
public:
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
};

int main()
{
// 用普通函数创建线程。
//thread t1(func, 3, "我是一只傻傻鸟。");
//thread t2(func, 8, "我有一只小小鸟。");

// 用lambda函数创建线程。
auto f = [](int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
};
//thread t3(f, 3, "我是一只傻傻鸟。");

// 用仿函数创建线程。
//thread t4(mythread1(), 3, "我是一只傻傻鸟。");

// 用类的静态成员函数创建线程。
//thread t5(mythread2::func, 3, "我是一只傻傻鸟。");

// 用类的普通成员函数创建线程。
mythread3 myth; // 必须先创建类的对象,必须保证对象的生命周期比子线程要长。
thread t6(&mythread3::func, &myth, 3, "我是一只傻傻鸟。"); // 第二个参数必须填对象的this指针,否则会拷贝对象。

cout << "任务开始。\n";
for (int ii = 0; ii < 10; ii++) {
cout << "执行任务中......\n";
Sleep(1000); // 假设执行任务需要时间。
}
cout << "任务完成。\n";

//t1.join(); // 回收线程t1的资源。
//t2.join(); // 回收线程t2的资源。
//t3.join(); // 回收线程t3的资源。
//t4.join(); // 回收线程t4的资源。
//t5.join(); // 回收线程t5的资源。
t6.join(); // 回收线程t6的资源。
}

线程资源的回收

虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。

回收子线程的资源有两种方法:

  1. 在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。

  2. 在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。
    分离后,需要主程序留有时间执行子线程。

用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。

示例:

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>
#include <thread> // 线程类头文件。
// linux 使用 unistd.h 的 sleep()
#include <windows.h> // Sleep()函数需要这个头文件。
using namespace std;

// 普通函数。
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}

int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");

t1.detach(); t2.detach(); // 分离子线程。

//cout << "任务开始。\n";
//for (int ii = 0; ii < 12; ii++) {
// cout << "执行任务中......\n";
// Sleep(1000); // 假设执行任务需要时间。
//}
//cout << "任务完成。\n";

//t1.join(); // 回收线程t1的资源。
//t2.join(); // 回收线程t2的资源。
Sleep(12000);
}

this_thread 的全局函数

C++11提供了命名空间this_thread来表示当前线程,该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。

都在 std 命名空间下。即:std::this_thread::get_id() 等

  1. get_id()

    该函数用于获取线程ID,thread类也有同名的成员函数(t1.get_id()也可以)。

    1
    2
    3
    thread::id get_id() noexcept;
    // 使用如下,获取当前线程id
    cout << this_thread::get_id() << endl;
  2. sleep_for()
    类似 sleep,但使用的是 chrono 库的 duration 作为参数。

    该函数让线程休眠一段时间。

    1
    2
    3
    4
    template <class Rep, class Period>
    void sleep_for (const chrono::duration<Rep,Period>& rel_time);
    // 使用如下
    sleep_for(std::chrono::second(1)); // 同 sleep(1);
  3. sleep_until()

    该函数让线程休眠至指定时间点。(可实现定时任务)
    需要将字符串时间转换为时间点格式。

    1
    2
    template <class Clock, class Duration>
    void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
  4. yield()

    该函数让线程主动让出自己已经抢到的CPU时间片。
    yield() 的作用是让当前线程放弃执行,将执行机会让给其他线程,但不会使线程进入阻塞状态。具体来说,yield() 的作用是告诉操作系统当前线程愿意放弃其执行权,但并不能保证操作系统会立即调度其他线程。

    1
    void yield() noexcept;
  5. thread类其它的成员函数

    不在 this_thread 命名空间下,而是成员函数。

    1
    2
    void swap(std::thread& other);    // 交换两个线程对象。
    static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。

示例:

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
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;

// 普通函数。
void func(int bh, const string& str) {
cout << "子线程:" << this_thread::get_id() << endl;

for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}

int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");

cout << "主线程:" << this_thread::get_id() << endl;
cout << "线程t1:" << t1.get_id() << endl;
cout << "线程t2:" << t2.get_id() << endl;

t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}

call_once 函数

在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次。

在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。

头文件:#include <mutex>

函数原型:

1
2
template< class callable, class... Args >
void call_once( std::once_flag& flag, Function&& fx, Args&&... args );

第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。

第二个参数是需要执行的函数fx。

后面的可变参数是传递给函数fx的参数。

使用方法:

  1. 创建一个类型为 once_flag 的全局变量 onceflag(随便什么名字)。
  2. 在被调用的线程函数中使用 call_once 函数,并传入对应参数。

示例:

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
#include <iostream>
#include <thread> // 线程类头文件。
#include <mutex> // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;

once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
cout << "once_func() bh= " << bh << ", str=" << str << endl;
}

// 普通函数。
void func(int bh, const string& str) {
call_once(onceflag,once_func,0, "各位观众,我要开始表白了。");

for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}

int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");

t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}

native_handle函数

用于返回 linux 中原生的线程 id,以便可以使用线程库 pthread.h 中的函数操作线程。

C++11定义了线程标准,不同的平台和编译器在实现的时候,本质上都是对操作系统的线程库进行封装,会损失一部分功能。

为了弥补C++11线程库的不足,thread类提供了native_handle()成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。

示例:

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>
#include <thread>
#include <pthread.h> // Linux的pthread线程库头文件。
using namespace std;

void func() // 线程任务函数。
{
for (int ii=1;ii<=10;ii++)
{
cout << "ii=" << ii << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}

int main()
{
thread tt(func); // 创建线程。

this_thread::sleep_for(chrono::seconds(5)); // 休眠5秒。

pthread_t thid= tt.native_handle(); // 获取Linux操作系统原生的线程句柄。

pthread_cancel(thid); // 取消线程。

tt.join(); // 等待线程退出。
}

线程安全

前面在多个线程使用 cout 输出内容时,会出现乱序,是由于 cout 是全局对象,因此导致冲突。

在单核cpu中,不会有冲突。

背景:

如:合租时,要使用卫生间。

  • 同一进程中的多个线程共享该进程中的全部的系统资源。

  • 多个线程访问同一共享资源的时候会产生冲突。

  • 顺序性、可见性、原子性

    1. 顺序性
      CPU 会为了提高程序的执行效率,对代码进行优化,按更高效的方式进行执行。但执行的结果与代码顺序执行一致。

    2. 可见性

      线程操作共享变量时,会将该变量从内存加载到CPU缓存中,修改该变量后,CPU会立即更新缓存,但不一定会立即将它写回内存。这时候,如果其它线程访问该变量,从内存中读到的是旧数据,而非第一个线程操作后的数据。
      当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。

    3. 原子性

      一个操作(有可能包含有多个步骤)要么全部执行(生效),要么全部都不执行(都不生效)。

如何保证线程安全

  1. volatile 关键字:只保证了顺序,不能保证线程安全

  2. 原子操作(原子类型)

  3. 线程同步(也称为锁)

    线程同步是指 多个线程协同工作,协商如何使用共享资源。

互斥锁

头文件:#include <mutex>

C++ 11 中有四种互斥锁:

  • mutex:互斥锁。

  • timed_mutex:带超时机制的互斥锁。

  • recursive_mutex:递归互斥锁。

  • recursive_timed_mutex:带超时机制的递归互斥锁。

mutex 类

多人排队等待一个公共卫生间。

  1. lock() 加锁

    互斥锁有锁定和未锁定两种状态。

    如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。

    如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。

  2. unlock() 解锁

    只有持有锁的线程才能解锁。

  3. 尝试加锁try_lock()

    有多个公共卫生间,这个锁住,那就判断尝试别的也可以。

    如果互斥锁是未锁定状态,则加锁成功,函数返回true。

    如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)

示例:

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
#include <iostream>
#include <thread> // 线程类头文件。
#include <mutex> // 互斥锁类的头文件。
using namespace std;

mutex mtx; // 创建互斥锁,保护共享资源cout对象。

// 普通函数。
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
mtx.lock(); // 申请加锁。
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
mtx.unlock(); // 解锁。
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}

int main()
{
// 用普通函数创建线程。
thread t1(func, 1, "我是一只傻傻鸟。");
thread t2(func, 2, "我是一只傻傻鸟。");
thread t3(func, 3, "我是一只傻傻鸟。");
thread t4(func, 4, "我是一只傻傻鸟。");
thread t5(func, 5, "我是一只傻傻鸟。");

t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
t3.join(); // 回收线程t3的资源。
t4.join(); // 回收线程t4的资源。
t5.join(); // 回收线程t5的资源。
}

timed_mutex 类

如同:等待一个卫生间超过5分钟,则换别的卫生间。

判断在一定时间是否能获取锁,若是不行,则切换到其它函数。

增加了两个成员函数:

1
2
bool try_lock_for(时间长度);
bool try_lock_until(时间点);

recursive_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
27
28
#include <iostream>
#include <mutex> // 互斥锁类的头文件。
using namespace std;

class AA
{
recursive_mutex m_mutex;
public:
void func1() {
m_mutex.lock();
cout << "调用了func1()\n";
m_mutex.unlock();
}

void func2() {
m_mutex.lock();
cout << "调用了func2()\n";
func1();
m_mutex.unlock();
}
};

int main()
{
AA aa;
//aa.func1();
aa.func2();
}

lock_guard 模板类

lock_guard是模板类,可以简化互斥锁的使用,也更安全。

lock_guard的定义如下:

1
2
3
4
5
template<class Mutex>
class lock_guard
{
explicit lock_guard(Mutex& mtx);
}

lock_guard 的构造函数初始化参数是某一种类型的互斥锁。

lock_guard在构造函数中加锁,在析构函数中解锁。

lock_guard采用了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
#include <iostream>
#include <thread>
#include <mutex>

using std::cout;
using std::endl;

int a = 0;
std::mutex mt;

void add()
{
for (int i = 0; i < 1000000; i++)
{
std::lock_guard<std::mutex> mlock(mt);
// mt.lock();
a++;
// mt.unlock();
}
};

int main()
{
std::thread t1(add);
std::thread t2(add);

t1.join();
t2.join();

cout << a << endl;

return 0;
}

条件变量-生产消费者模型

条件变量是一种线程同步机制。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。

为了保护共享资源,条件变量需要和互斥锁一起使用。

条件变量最常用的就是实现 生产/消费者模型(高速缓存队列)

生产/消费者模型

生产/消费者模型示意图:

如客服收集用户需求,然后提出工单,工人收到工单处理需求。

piWZbcT.png

  1. 生产者先把需要处理的数据放在缓存队列(仓库)中,然后向消费者发送通知。
  2. 消费者接收到通知后,从缓存队列中把数据拿出来进行处理。

示例代码:

持有锁时间越短,效率越高。第 32 行,通过大括号增加了一个作用域,使得unique_lock 的锁出了大括号这个作用域就解锁。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <string>
#include <thread> // 线程类头文件。
#include <mutex> // 互斥锁类的头文件。
#include <deque> // deque容器的头文件。
#include <queue> // queue容器的头文件。
#include <condition_variable> // 条件变量的头文件。
using namespace std;
class AA
{
mutex m_mutex; // 互斥锁。
condition_variable m_cond; // 条件变量。
queue<string, deque<string>> m_q; // 缓存队列,底层容器用deque。
public:
void incache(int num) // 生产数据,num指定数据的个数。
{
lock_guard<mutex> lock(m_mutex); // 申请加锁。
for (int ii=0 ; ii<num ; ii++)
{
static int bh = 1; // 超女编号。
string message = to_string(bh++) + "号超女"; // 拼接出一个数据。
m_q.push(message); // 把生产出来的数据入队。
}
m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程。
}

void outcache() // 消费者线程任务函数。
{
while (true)
{
string message;
{
// 把互斥锁转换成unique_lock<mutex>,并申请加锁。
unique_lock<mutex> lock(m_mutex);

while (m_q.empty()) // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
m_cond.wait(lock); // 等待生产者的唤醒信号。

// 数据元素出队。
message = m_q.front(); m_q.pop();
}
// 处理出队的数据(把数据消费掉)。
this_thread::sleep_for(chrono::milliseconds(1)); // 假设处理数据需要1毫秒。
cout << "线程:" << this_thread::get_id() << "," << message << endl;
}
}
};

int main()
{
AA aa;

thread t1(&AA::outcache, &aa); // 创建消费者线程t1。
thread t2(&AA::outcache, &aa); // 创建消费者线程t2。
thread t3(&AA::outcache, &aa); // 创建消费者线程t3。

this_thread::sleep_for(chrono::seconds(2)); // 休眠2秒。
aa.incache(3); // 生产3个数据。

this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。
aa.incache(5); // 生产5个数据。

t1.join(); // 回收子线程的资源。
t2.join();
t3.join();
}

条件变量

C++11的条件变量提供了两个类:

头文件:#include <condition_variable>

condition_variable:只支持与普通mutex搭配,效率更高。

condition_variable_any:是一种通用的条件变量,可以与任意mutex搭配(包括用户自定义的锁类型)。

condition_variable类

notify_one 和 notify_all,如果生产的数据只有一个,用前者合适,生产的数据有多个,则用后者(可以调用多线程执行操作)。

wait() 函数

wait() 函数的作用不只是等待生产者的唤醒信号,还会进行解锁操作,使得多个线程都可以获得锁,然后阻塞等待生产者的唤醒信号,最后等到了信号后进行加锁操作以便执行对后面共享变量的操作。

  1. 对传入的互斥锁进行解锁
  2. 阻塞等待生产者的唤醒信号
  3. 等到了信号后给互斥锁加锁
1
2
3
4
5
6
7
8
9
10
11
1condition_variable() 默认构造函数。
2condition_variable(const condition_variable &)=delete 禁止拷贝。
3)condition_variable& condition_variable::operator=(const condition_variable &)=delete 禁止赋值。
4notify_one() 通知一个等待的线程。
5notify_all() 通知全部等待的线程,使得所有线程处理获取的数据。
6wait(unique_lock<mutex> lock) 阻塞当前线程,直到通知到达。
7wait(unique_lock<mutex> lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。
8wait_for(unique_lock<mutex> lock,时间长度)
9wait_for(unique_lock<mutex> lock,时间长度,Pred pred)
10wait_until(unique_lock<mutex> lock,时间点)
11wait_until(unique_lock<mutex> lock,时间点,Pred pred)
unique_lock 模板类

template <class Mutex> class unique_lock 是模板类,模板参数为互斥锁类型。

unique_lock和lock_guard都是管理锁的辅助类,都是RAII风格(在构造时获得锁,在析构时释放锁)。它们的区别在于:为了配合condition_variable,unique_lock还有lock()和unlock()成员函数。

普通互斥锁需要转换为 unique_lock 才能用于条件变量。因为lock_guard 没有 lock() 和 unlock() 函数,而 unique_lock 有,因为 wait 函数需要解锁和加锁两个功能。

unique_lock 除了增加作用域,也可以调用 unlock() 函数手动解锁。

示例代码

while() 循环有虚假唤醒的可能,即因为没有抢到数据,就是虚假唤醒了线程。

除了用循环避免虚假唤醒,还可以用 wait() 函数的重载函数 wait(unique_lock<mutex> lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。

示例代码:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <string>
#include <thread> // 线程类头文件。
#include <mutex> // 互斥锁类的头文件。
#include <deque> // deque容器的头文件。
#include <queue> // queue容器的头文件。
#include <condition_variable> // 条件变量的头文件。
using namespace std;
class AA
{
// timed_mutex t_mutex;
// condition_variable_any am_cond; // 其它互斥锁使用该条件变量。
mutex m_mutex; // 互斥锁。
condition_variable m_cond; // 条件变量。
queue<string, deque<string>> m_q; // 缓存队列,底层容器用deque。
public:
void incache(int num) // 生产数据,num指定数据的个数。
{
lock_guard<mutex> lock(m_mutex); // 申请加锁。
for (int ii=0 ; ii<num ; ii++)
{
static int bh = 1; // 超女编号。
string message = to_string(bh++) + "号超女"; // 拼接出一个数据。
m_q.push(message); // 把生产出来的数据入队。
}
//m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程。
m_cond.notify_all(); // 唤醒全部被当前条件变量阻塞的线程。
}

void outcache() { // 消费者线程任务函数。
while (true) {
// 把互斥锁转换成unique_lock<mutex>,并申请加锁。
unique_lock<mutex> lock(m_mutex);

// 条件变量虚假唤醒:消费者线程被唤醒后,缓存队列中没有数据。
//while (m_q.empty()) // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
// m_cond.wait(lock); // 1)把互斥锁解开;2)阻塞,等待被唤醒;3)给互斥锁加锁。
m_cond.wait(lock, [this] { return !m_q.empty(); }); // this 表示lambda函数可以访问当前类对象的成员变量,如 m_q

// 数据元素出队。
string message = m_q.front(); m_q.pop();
cout << "线程:" << this_thread::get_id() << "," << message << endl;
lock.unlock(); // 手工解锁。

// 处理出队的数据(把数据消费掉)。
this_thread::sleep_for(chrono::milliseconds(1)); // 假设处理数据需要1毫秒。
}
}
};

int main()
{
AA aa;

thread t1(&AA::outcache, &aa); // 创建消费者线程t1。
thread t2(&AA::outcache, &aa); // 创建消费者线程t2。
thread t3(&AA::outcache, &aa); // 创建消费者线程t3。

this_thread::sleep_for(chrono::seconds(2)); // 休眠2秒。
aa.incache(2); // 生产2个数据。

this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。
aa.incache(5); // 生产5个数据。

t1.join(); // 回收子线程的资源。
t2.join();
t3.join();
}

原子类型

头文件:#include <atomic>

原子操作由CPU指令提供支持,是轻量级的锁,不是完全没有锁。

C++11提供了atomic<T>模板类(结构体),用于支持原子类型,模板参数可以是bool、char、int、long、long long、指针类型(不支持浮点类型和自定义数据类型)。

原子操作由CPU指令提供支持,它的性能比锁和消息传递更高,并且,不需要程序员处理加锁和释放锁的问题,支持修改、读取、交换、比较并交换等操作。

构造函数:

1
2
3
atomic() noexcept = default;  // 默认构造函数。
atomic(T val) noexcept; // 转换函数。
atomic(const atomic&) = delete; // 禁用拷贝构造函数。

赋值函数:

1
atomic& operator=(const atomic&) = delete;   // 禁用赋值函数。

常用函数:

是CPU指令提供的标准操作。

1
2
3
4
5
6
7
void store(const T val) noexcept;   // 把val的值存入原子变量。
T load() noexcept; // 读取原子变量的值。
T fetch_add(const T val) noexcept; // 把原子变量的值与val相加,返回原值。
T fetch_sub(const T val) noexcept; // 把原子变量的值减val,返回原值。
T exchange(const T val) noexcept; // 把val的值存入原子变量,返回原值。
T compare_exchange_strong(T &expect,const T val) noexcept; // 比较原子变量的值和预期值expect,如果当两个值相等,把val存储到原子变量中,函数返回true;如果当两个值不相等,用原子变量的值更新预期值,函数返回false。CAS指令。
bool is_lock_free(); // 查询某原子类型的操作是直接用CPU指令(返回true),还是编译器内部的锁(返回false)。

注意点:

  • atomic<T>模板类重载了整数操作的各种运算符。

  • atomic<T>模板类的模板参数支持指针,但不表示它所指向的对象是原子类型。

    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>
    #include <atomic>
    #include <thread>

    using std::cout;
    using std::endl;

    // std::atomic_int num = 0;
    int num = 0;
    std::atomic<int*> numptr = &num;

    void add()
    {
    for (int i = 0; i < 1000000; i++)
    (*numptr)++; // ++num;
    }

    int main()
    {
    std::thread t1(add);
    std::thread t2(add);

    t1.join();
    t2.join();

    cout << num << endl;

    return 0;
    }
  • 原子整型可以用作计数器,原子布尔型可以用作开关。

  • CAS指令是实现无锁队列基础。

示例:

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>
#include <atomic> // 原子类型的头文件。
using namespace std;

int main()
{
atomic<int> a = 3; // atomic(T val) noexcept; // 转换函数。
cout << "a=" << a.load() << endl; // 读取原子变量a的值。输出:a=3
a.store(8); // 把8存储到原子变量中。
cout << "a=" << a.load() << endl; // 读取原子变量a的值。 输出:a=8

int old; // 用于存放原值。
old = a.fetch_add(5); // 把原子变量a的值与5相加,返回原值。
cout << "old = " << old <<",a = " << a.load() << endl; // 输出:old=8,a=13
old = a.fetch_sub(2); // 把原子变量a的值减2,返回原值。
cout << "old = " << old << ",a = " << a.load() << endl; // 输出:old=13,a=11

atomic<int> ii = 3; // 原子变量
int expect = 4; // 期待值
int val = 5; // 打算存入原子变量的值
// 比较原子变量的值和预期值expect,
// 如果当两个值相等,把val存储到原子变量中;
// 如果当两个值不相等,用原子变量的值更新预期值。
// 执行存储操作时返回true,否则返回false。
bool bret = ii.compare_exchange_strong(expect, val);
cout << "bret=" << bret << endl;
cout << "ii=" << ii << endl;
cout << "expect=" << expect << endl;
}

可调用对象的绑定器和包装器

C11 的三神器:智能指针、移动语义和可调用对象的绑定器和包装器。

可调用对象

在C++中,可以像函数一样调用的有:**普通函数、类的静态成员函数、仿函数、lambda函数、类的非静态成员函数、可被转换为函数的类的对象 ** ,统称可调用对象或函数对象。

可调用对象有类型,可以用指针存储它们的地址,可以被引用(类的成员函数除外)

普通函数

C++ 中,函数是一种数据类型,函数的实体被看成对象,而不只是一段代码。函数是对象,可以调用的对象,或者叫函数对象。

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
#include <iostream>
using namespace std;

using Fun = void (int, const string&); // 普通函数类型的别名。
Fun show; // 声明普通函数。


int main()
{
show(1, "我是一只傻傻鸟。"); // 直接调用普通函数。

void(*fp1)(int, const string&) = show; // 声明函数指针,指向普通函数。
void(&fr1)(int, const string&) = show; // 声明函数引用,引用普通函数。
fp1(2, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
fr1(3, "我是一只傻傻鸟。"); // 用函数引用调用普通函数。

Fun* fp2 = show; // 声明函数指针,指向普通函数。
Fun& fr2 = show; // 声明函数引用,引用普通函数。
fp2(4, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
fr2(5, "我是一只傻傻鸟。"); // 用函数引用调用普通函数。
}

// 定义普通函数
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}

// 以下代码是错误的,不能用函数类型定义函数的实体。
//Func show1 {
// cout << "亲爱的" << bh << "," << message << endl;
//}

类的静态成员函数

类的静态成员函数和普通函数本质上是一样的,把普通函数放在类中而已。

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>
using namespace std;

using Fun = void (int, const string&); // 普通函数类型的别名。

struct AA // 类中有静态成员函数。
{
static void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

int main()
{
AA::show(1, "我是一只傻傻鸟。"); // 直接调用静态成员函数。

void(*fp1)(int, const string&) = AA::show; // 用函数指针指向静态成员函数。
void(&fr1)(int, const string&) = AA::show; // 引用静态成员函数。
fp1(2, "我是一只傻傻鸟。"); // 用函数指针调用静态成员函数。
fr1(3, "我是一只傻傻鸟。"); // 用函数引用调用静态成员函数。

Fun* fp2 = AA::show; // 用函数指针指向静态成员函数。
Fun& fr2 = AA::show; // 引用静态成员函数。
fp2(4, "我是一只傻傻鸟。"); // 用函数指针调用静态成员函数。
fr2(5, "我是一只傻傻鸟。"); // 用函数引用调用静态成员函数。
}

仿函数

仿函数的本质是类,重载小括号 () 运算符,调用的代码像函数。

仿函数的类型就是类的类型。

普通类创建的对象不是可调用对象,因为它不能像函数一样直接调用(调用成员函数不一样,是可以的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

struct BB // 仿函数。
{
void operator()(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

int main()
{
BB bb;
bb(11, "我是一只傻傻鸟。"); // 用对象调用仿函数。
BB()(12, "我是一只傻傻鸟。"); // 用匿名对象调用仿函数。

BB& br = bb; // 引用函数
br(13, "我是一只傻傻鸟。"); // 用对象的引用调用仿函数。
}

lambda 函数

lambda函数的本质是仿函数,仿函数的本质是类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main()
{
// 创建lambda对象。
auto lb = [](int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
};

auto& lr = lb; // 引用lambda对象。

lb(1, "我是一只傻傻鸟。"); // 用lambda对象调用仿函数。
lr(2, "我是一只傻傻鸟。"); // 用lambda对象的引用调用仿函数。
}

类的非静态成员函数

与其他可调用对象不同,所以才需要可调用对象的包装器和绑定器。这是最重要的可调用对象。

类的非静态成员函数有地址,但是,只能通过类的对象才能调用它,所以,C++对它做了特别处理。

普通函数有函数类型,而类的非静态成员函数只有指针类型,没有引用类型,不能引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

struct CC // 类中有普通成员函数。
{
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

int main()
{
CC cc;
cc.show(14, "我是一只傻傻鸟。");

void (CC::* fp11)(int, const string&) = &CC::show; // 定义类的成员函数的指针。这里取地址&符号不可以省略,而普通函数指针可以。
(cc.*fp11)(15, "我是一只傻傻鸟。"); // 用类的成员函数的指针调用成员函数。

using pFun = void (CC::*)(int, const string&); // 类成员函数的指针类型。
pFun fp12 = &CC::show; // 让类成员函数的指针指向类的成员函数的地址。
(cc.*fp12)(16, "我是一只傻傻鸟。"); // 用类成员函数的指针调用类的成员函数。
}

可以转换为函数指针的类对象

不重要。

类可以重载类型转换运算符operator 数据类型() ,如果数据类型是函数指针或函数引用类型,那么该类实例也将成为可调用对象。

它的本质是类,调用的代码像函数。

在实际开发中,意义不大。

类中的这个转换函数只能返回普通全局函数和类的静态成员函数,不能返回类的非静态成员函数,而普通全局函数和类的静态成员函数本身就是可调用对象,直接调用就行,没必要多此一举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

// 定义函数
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}

struct DD // 可以被转换为函数指针的类。
{
using Fun = void (*)(int, const string&);
operator Fun() {
return show; // 返回普通函数。
}
};

int main()
{
DD dd;
dd(17, "我是一只傻傻鸟。"); // 可以被转换为函数指针的类对象。可以直接用对象名调用普通全局函数。
}

包装器

用于统一模板中的写法。

std::function模板类是一个通用的可调用对象的包装器,用简单的、统一的方式处理可调用对象。

头文件:#include <functional>

1
2
template<class _Fty>
class function……

_Fty 是可调用对象的类型(只有一个模板参数),格式:返回类型(参数列表)。

注意:

  • 重载了bool运算符,用于判断是否包装了可调用对象。
    即包装器对象可以直接判断。

  • 如果std::function对象未包装可调用对象,使用std::function对象将抛出std::bad_function_call异常。

示例:

包装器的使用:

  1. 仿函数:可以使用匿名对象,也可以使用已有对象。
  2. lambda 函数:可以预先定义一个lambda表达式,也可以直接给一个没有名字的。
  3. 类的非静态成员函数:function<void(CC&,int, const string&)> fn11 = &CC::show; 模板参数的第一个参数是类对象的引用,调用时的第一个参数是类对象。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
#include <functional>
using namespace std;

// 普通函数
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}

struct AA // 类中有静态成员函数。
{
static void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

struct BB // 仿函数。
{
void operator()(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

struct CC // 类中有普通成员函数。
{
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

struct DD // 可以被转换为普通函数指针的类。
{
using Fun = void (*)(int, const string&); // 函数指针的别名。
operator Fun() {
return show; // 返回普通函数show的地址。
}
};

int main()
{
using Fun = void(int, const string&); // 函数类型的别名。

// 普通函数。
void(*fp1)(int, const string&) = show; // 声明函数指针,指向函数对象。
fp1(1, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
function<void(int, const string&)> fn1 = show; // 包装普通全局函数show。模板参数可以填函数类型别名,但是当前的可读性高。
fn1(1, "我是一只傻傻鸟。"); // 用function对象调用普通全局函数show。

// 类的静态成员函数。
void(*fp3)(int, const string&) = AA::show; // 用函数指针指向类的静态成员函数。
fp3(2, "我是一只傻傻鸟。"); // 用函数指针调用类的静态成员函数。
function<void(int, const string&)> fn3 = AA::show; // 包装类的静态成员函数。
fn3(2, "我是一只傻傻鸟。"); // 用function对象调用类的静态成员函数。

// 仿函数。
BB bb;
bb(3, "我是一只傻傻鸟。"); // 用仿函数对象调用仿函数。
function<void(int, const string&)> fn4 = BB(); // 包装仿函数。这里使用的是匿名对象,也可以使用 bb 已创建的对象。
fn4(3, "我是一只傻傻鸟。"); // 用function对象调用仿函数。

// 创建lambda对象。
auto lb = [](int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
};
lb(4, "我是一只傻傻鸟。"); // 调用lambda函数。
function<void(int, const string&)> fn5 = lb; // 包装lamba函数。这里定义了 lb 这个lambda表达式,也可以直接在等于号右边写lambda表达式。
fn5(4, "我是一只傻傻鸟。"); // 用function对象调用lamba函数。

// 类的非静态成员函数。
CC cc;
void (CC:: * fp11)(int, const string&) = &CC::show; // 定义类成员函数的指针。
(cc.*fp11)(5, "我是一只傻傻鸟。"); // 用类成员函数的指针调用类的成员函数。
function<void(CC&,int, const string&)> fn11 = &CC::show; // 包装成员函数。模板参数的第一个参数是类对象的引用,调用时的第一个参数是类对象。
fn11(cc,5, "我是一只傻傻鸟。"); // 用function对象调用成员函数。

// 可以被转换为函数指针的类对象。
DD dd;
dd(6, "我是一只傻傻鸟。"); // 用可以被转换为函数指针的类对象调用普通函数。
function<void(int, const string&)> fn12 = dd; // 包装可以被转换为函数指针的类。
fn12(6, "我是一只傻傻鸟。"); // 用function对象调用它。

function<void(int, const string&)> fx=dd;
try {
if (fx) fx(6, "我是一只傻傻鸟。");
}
catch (std::bad_function_call e) {
cout << "抛出了std::bad_function_call异常。";
}
}

适配器(绑定器)bind

头文件:#include <functional>

std::bind()模板函数是一个通用的函数适配器(绑定器),它用一个可调用对象及其参数,生成一个新的可调用对象,以适应模板。也就是把一个可调用对象改头换面,变成一个新的可调用对象。

函数原型:

1
2
template< class Fx, class... Args >
function<> bind (Fx&& fx, Args&...args);

Fx:需要绑定的可调用对象(可以是前两节课介绍的那六种,也可以是function对象)。

args:绑定参数列表,可以是左值、右值和参数占位符 std::placeholders::_n,如果参数不是占位符,缺省为值传递,std:: ref (参数)则为引用传递。

std::bind() 返回 std::function 的对象。

std::bind() 的本质是仿函数。

bind() 第一个参数传入可调用对象(表示将可调用对象绑定给包装器对象),后面传入占位符。

bind 基本用法示例:

函数参数顺序不同,形成重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <functional>
using namespace std;

// 普通函数
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "号," << message << endl;
}

int main()
{
function<void(int, const string&)> fn1 = show;
function<void(int, const string&)> fn2 = bind(show, placeholders::_1, placeholders::_2); // 将可调用对象传入,bind 返回一个新的可调用对象
fn1(1, "我是一只傻傻鸟。");
fn2(1, "我是一只傻傻鸟。");

function<void(const string&, int)> fn3 = bind(show, placeholders::_2, placeholders::_1); // 避免函数重载
fn3("我是一只傻傻鸟。", 1);
function<void(const string&)> fn4 = bind(show, 3, placeholders::_1); // 预先传入参数,为了适应并生成一个需要参数少的可调用对象。
fn4("我是一只傻傻鸟。");

function<void(int, const string&,int)> fn5 = bind(show, placeholders::_1, placeholders::_2);
fn5(1, "我是一只傻傻鸟。", 88); // 也可以传入多的参数
}

绑定六种可调用对象示例:

  1. 仿函数和lambda函数:可以直接传入定义了的对象,也可以传入匿名对象或者lambda函数,你懂的。
  2. 类的非静态成员函数:
    1. function<void(CC&, int, const string&)> fn11 = bind(&CC::show, placeholders::_1, placeholders::_2, placeholders::_3); 这样的话,在调用包装器对象时,还需要将类对象传入。
    2. function<void(int, const string&)> fn11 = bind(&CC::show,&cc,placeholders::_1, placeholders::_2); 可以预先将类对象的地址传入,这样在调用时就不需要额外传入类对象了,也就可以实现模板化了。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <functional>
using namespace std;

// 普通函数
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}

struct AA // 类中有静态成员函数。
{
static void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

struct BB // 仿函数。
{
void operator()(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

struct CC // 类中有普通成员函数。
{
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};

struct DD // 可以被转换为普通函数指针的类。
{
using Fun = void (*)(int, const string&); // 函数指针的别名。
operator Fun() {
return show; // 返回普通函数show的地址。
}
};

int main()
{
// 普通函数。
function<void(int, const string&)> fn1 = bind(show, placeholders::_1, placeholders::_2); // 绑定普通全局函数show。
fn1(1, "我是一只傻傻鸟。"); // 用function对象调用普通全局函数show。

// 类的静态成员函数。
function<void(int, const string&)> fn3 = bind(AA::show, placeholders::_1, placeholders::_2); // 绑定类的静态成员函数。
fn3(2, "我是一只傻傻鸟。"); // 用function对象调用类的静态成员函数。

// 仿函数。
function<void(int, const string&)> fn4 = bind(BB(), placeholders::_1, placeholders::_2); // 绑定仿函数。
fn4(3, "我是一只傻傻鸟。"); // 用function对象调用仿函数。

// 创建lambda对象。
auto lb = [](int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
};
function<void(int, const string&)> fn5 = bind(lb, placeholders::_1, placeholders::_2); // 绑定lamba函数。
fn5(4, "我是一只傻傻鸟。"); // 用function对象调用lamba函数。

// 类的非静态成员函数。
CC cc;
//function<void(CC&, int, const string&)> fn11 = bind(&CC::show, placeholders::_1, placeholders::_2, placeholders::_3); // 绑定成员函数。
//fn11(cc, 5, "我是一只傻傻鸟。"); // 用function对象调用成员函数。
function<void(int, const string&)> fn11 = bind(&CC::show,&cc,placeholders::_1, placeholders::_2); // 绑定成员函数。
fn11(5, "我是一只傻傻鸟。"); // 用function对象调用成员函数。

// 可以被转换为函数指针的类对象。
DD dd;
function<void(int, const string&)> fn12 = bind(dd, placeholders::_1, placeholders::_2); // 绑定可以被转换为函数指针的类。
fn12(6, "我是一只傻傻鸟。"); // 用function对象调用它。
}

可变函数和参数的实现

使用了可变参数模板,并且为了提高可用性使用了完美转发。

代码示例类似 thread 的构造函数:

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
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <thread>
#include <functional>
using namespace std;

void show0() { // 普通函数。
cout << "亲爱的,我是一只傻傻鸟。\n";
}

void show1(const string& message) { // 普通函数。
cout << "亲爱的," << message << endl;
}

struct CC // 类中有普通成员函数。
{
void show2(int bh, const string& message) {
cout << "亲爱的" << bh << "号," << message << endl;
}
};

template<typename Fn, typename...Args>
auto show(Fn&& fn, Args&&...args) -> decltype(bind(forward<Fn>(fn), forward<Args>(args)...))
{
cout << "表白前的准备工作......\n";

auto f = bind(forward<Fn>(fn), forward<Args>(args)...); // bind 会将可变参数依次展开
f();

cout << "表白完成。\n";
return f;
}

int main()
{
show(show0);
show(show1,"我是一只傻傻鸟。");
CC cc;
auto f = show(&CC::show2,&cc, 3,"我是一只傻傻鸟。");
f();

//thread t1(show0);
//thread t2(show1,"我是一只傻傻鸟。");
//CC cc;
//thread t3(&CC::show2,&cc, 3,"我是一只傻傻鸟。");
//t1.join();
//t2.join();
//t3.join();
}

回调函数

在消息队列和网络库的框架中,当接收到消息(报文)时,回调用户自定义的函数对象,把消息(报文)参数传给它,由它决定如何处理。

回调函数也可以说是一个通用函数,将外部其它的函数传入,让外部函数进行业务处理。将外部可调用对象绑定到回调函数也就是注册回调函数。

回调函数的参数是需要处理的数据。

现代C++中,回调函数一般用类的成员函数,足够强大。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
#include <string>
#include <thread> // 线程类头文件。
#include <mutex> // 互斥锁类的头文件。
#include <deque> // deque容器的头文件。
#include <queue> // queue容器的头文件。
#include <condition_variable> // 条件变量的头文件。
#include <functional>
using namespace std;

void show(const string& message) { // 处理业务的普通函数
cout << "处理数据:" << message << endl;
}

struct BB { // 处理业务的类
void show(const string& message) {
cout << "处理表白数据:" << message << endl;
}
};

class AA // 假设这个类是开发框架,处理业务不能直接修改框架,所以最好采用回调函数处理不同的业务(用不同的可调用对象传入)。
{
mutex m_mutex; // 互斥锁。
condition_variable m_cond; // 条件变量。
queue<string, deque<string>> m_q; // 缓存队列,底层容器用deque。
function<void(const string&)> m_callback; // 回调函数对象。
public:
// 注册回调函数,回调函数只有一个参数(消费者接收到的数据)。
template<typename Fn, typename ...Args>
void callback(Fn && fn, Args&&...args) {
m_callback = bind(forward<Fn>(fn), forward<Args>(args)..., std::placeholders::_1); // 绑定回调函数。bind 的第二个可变参数包:如果传入的可调用对象是普通函数等,则没有参数;如果是类的非静态成员变量,则会有一个参数被展开,即类对象地址。
}

void incache(int num) // 生产数据,num指定数据的个数。
{
lock_guard<mutex> lock(m_mutex); // 申请加锁。
for (int ii = 0; ii < num; ii++)
{
static int bh = 1; // 超女编号。
string message = to_string(bh++) + "号超女"; // 拼接出一个数据。
m_q.push(message); // 把生产出来的数据入队。
}
//m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程。
m_cond.notify_all(); // 唤醒全部被当前条件变量阻塞的线程。
}

void outcache() { // 消费者线程任务函数。
while (true) {
// 把互斥锁转换成unique_lock<mutex>,并申请加锁。
unique_lock<mutex> lock(m_mutex);

// 1)把互斥锁解开;2)阻塞,等待被唤醒;3)给互斥锁加锁。
m_cond.wait(lock, [this] { return !m_q.empty(); });

// 数据元素出队。
string message = m_q.front(); m_q.pop();
cout << "线程:" << this_thread::get_id() << "," << message << endl;
lock.unlock(); // 手工解锁。

// 处理出队的数据(把数据消费掉)。
if (m_callback) m_callback(message); // 回调函数,把收到的数据传给它。
}
}
};

int main()
{
AA aa;
// aa.callback(show); // 把普通函数show()注册为回调函数。
BB bb;
aa.callback(&BB::show, &bb); // 把类成员函数BB::show()注册为回调函数。

thread t1(&AA::outcache, &aa); // 创建消费者线程t1。
thread t2(&AA::outcache, &aa); // 创建消费者线程t2。
thread t3(&AA::outcache, &aa); // 创建消费者线程t3。

this_thread::sleep_for(chrono::seconds(2)); // 休眠2秒。
aa.incache(2); // 生产2个数据。

this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。
aa.incache(5); // 生产5个数据。

t1.join(); // 回收子线程的资源。
t2.join();
t3.join();
}

如何取代虚函数

C++虚函数在执行过程中会跳转两次(先查找对象的函数表,再次通过该函数表中的地址找到真正的执行地址),这样的话,CPU会跳转两次,而普通函数只跳转一次。

CPU每跳转一次,预取指令要作废很多,所以效率会很低。(百度)

为了管理的方便(基类指针可指向派生类对象和自动析构派生类),保留类之间的继承关系。

包装器和绑定器可以实现虚函数的功能,并且不会有性能的损失。

包装器和绑定器不要求两个类是否有继承关系,但是保留是为了方便处理基类的指针。

示例:

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
39
40
41
42
43
44
45
46
47
48
#include <iostream>         // 包含头文件。
#include <functional>
using namespace std;

struct Hero { // 英雄基类
//virtual void show() { cout << "英雄释放了技能。\n"; }
function<void()> m_callback; // 用于绑定子类的成员函数。

// 注册子类成员函数,子类成员函数没有参数。
template<typename Fn, typename ...Args>
void callback(Fn&& fn, Args&&...args) {
m_callback = bind(forward<Fn>(fn), forward<Args>(args)...);
}
void show() { m_callback(); } // 调用子类的成员函数。取代虚函数。
};

struct XS :public Hero { // 西施派生类
void show() { cout << "西施释放了技能。\n"; }
};

struct HX :public Hero { // 韩信派生类
void show() { cout << "韩信释放了技能。\n"; }
};

int main()
{
// 根据用户选择的英雄,施展技能。
int id = 0; // 英雄的id。
cout << "请输入英雄(1-西施;2-韩信。):";
cin >> id;

// 创建基类指针,将指向派生类对象,用基类指针调用派生类的成员函数。
Hero* ptr = nullptr;

if (id == 1) { // 1-西施
ptr = new XS;
ptr->callback(&XS::show, static_cast<XS*>(ptr)); // 注册子类成员函数。
}
else if (id == 2) { // 2-韩信
ptr = new HX;
ptr->callback(&HX::show, static_cast<HX*>(ptr)); // 注册子类成员函数。
}

if (ptr != nullptr) {
ptr->show(); // 调用子类的成员函数。
delete ptr; // 释放派生类对象。
}
}