第一部分:thread类

c++头文件<thread>主要包含了std::thread类,此外,命名空间std::this_thread也封装在这一头文件中。

构造

thread类的构造函数为thread (Fn&& fn, Args&&... args);,线程会调用fn函数,函数fn有需要传递的参数在args里给出。 构造函数结束的同时函数fn将被调用**。

方法

  • join方法:
    • 声明:void join();
    • 只有当这个线程结束并返回后主线程才会继续执行。
  • detach方法:
    • 声明:void detach();
    • 把这个线程与主线程分离运行。

一个例程:

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

int main()
{
std::ios::sync_with_stdio(false);
std::thread thread_a([]()
{
for (int i = 0; i < 100000; i++)
std::cout << "thread a: " << i << std::endl;
});
std::thread thread_b([]()
{
for (int i = 0; i < 100000; i++)
std::cout << "thread b: " << i << std::endl;
});
thread_a.detach();
thread_b.detach();
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}

以下是程序中的部分输出:

1
2
3
4
5
6
7
8
9
thread a: 61856
thread b: 60031
thread a: 61857
thread b: 60032
thread a: 61858
thread b: 60033
thread a: 61859
thread b: 60034
thread a: 61860

可以看到thread_athread_b两个线程在同时向命令行输出信息。

第二部分:mutex类

多线程很方便,许多任务都可以通过并行式运算加快计算速度,但是同时也带来了一些问题,例如当多个线程同时竞争一个内存区域时,就会产生线程安全问题,导致程序运行出现一些意想不到的问题。

这样的现象就是多个进程同时竞争一个内存资源导致的。

为了解决这种问题,我们引入了一种工具——。所谓,顾名思义,是用来保护内存的工具,在多线程开发中,必须合理地使用锁才能保证程序的安全。

c++11中提供了mutex(互斥类),它们封装在头文件<mutex>中。通过mutex可以比较方便的在多线程中使用

std::mutex也被称为互斥锁,同一时间只能被一个线程上锁,其他线程想上锁时,必须等待当前锁被释放后,才能成功上锁。也就是说,std::mutex可以保证,处于同一个mutex对象下的一对lock()和unlock()间的代码,不会有多个线程在同时运行。

下面先介绍mutex类。

构造

mutex的构造不需要传递参数,直接std::mutex mtx即可。

方法

  • lock()方法
    • 如果mutex现在没有被任何线程在占有,那么调用mtx.lock()函数可以锁住mutex,即mutex被调用mtx.lock的线程占有。
    • 如果mutex已经被其他线程占有,那么调用mtx.lock()函数的线程将会被阻滞,直到mutex被占有它的进程释放(执行mtx.unlock())且本线程成功锁住mutex,线程才会恢复运行。
    • 如果在mutex已经被本线程锁定的情况下执行mtx.lock()函数,那么将会造成死锁。由于本教程为速成教程,故不在此解释死锁概念和其解决方法。但死锁概念在多线程编程中十分重要,建议读者百度自学。在此,你可以将死锁理解为一把会将进程彻底锁死的锁,产生死锁后线程将被阻滞且不会被唤醒。
  • unlock()方法
    • 解锁本线程锁定的mutex,以便其他线程可以占有mutex

下面使用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
29
30
31
32
33
34
#include <iostream>
#include <thread>
#include <mutex>

int num = 0;
std::mutex mtx;

int main()
{
std::ios::sync_with_stdio(false);
std::thread thread_a([&]()
{
for (int i = 0; i < 100000; i++)
{
mtx.lock();
num++;
mtx.unlock();
}
});
std::thread thread_b([&]()
{
for (int i = 0; i < 100000; i++)
{
mtx.lock();
num++;
mtx.unlock();
}

});
thread_a.join();
thread_b.join();
std::cout << "result: " << num << std::endl;
return 0;
}
1
result: 200000

mutex已经十分方便了,但是c++11还提供了更方便的工具unique_lock,他和mutex一样封装在<mutex>头文件下,下面将介绍unique_lock

构造

由于std::mutex需要手动unlock(),当一个代码段有多个退出点时,需要在每个退出点都加上unlock(),容易漏。而且如果该代码段还会抛出异常,手动unlock()会更加麻烦。这时我们通常会使用std::unique_lock或std::scoped_lock,这两个类型的作用就是在构造这个类型的对象时,传入一个std::mutex,随后会自动加锁,在对象析构时,也会自动释放锁。

unique_lock类较简单的构造方法为使用mutex进行构造,构造函数为unique_lock (mutex_type& m);。 e.g.:

1
unique_lock lock(mtx);

unique_lock在构造时会自动获取mutex,如果无法锁住mutex,则此线程会停滞,直到成功获取mutex。 在unique_lock被析构时,他会自动unlock自己获取的mutex。这使得他在许多地方非常方便实用。

下面将用unique_lock实现上一段代码中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
29
30
31
32
#include <iostream>
#include <thread>
#include <mutex>

int num = 0;
std::mutex mtx;

int main()
{
std::ios::sync_with_stdio(false);
std::thread thread_a([&]()
{
for (int i = 0; i < 100000; i++)
{
std::unique_lock<std::mutex> lock(mtx);
num++;
}
});
std::thread thread_b([&]()
{
for (int i = 0; i < 100000; i++)
{
std::unique_lock<std::mutex> lock(mtx);
num++;
}

});
thread_a.join();
thread_b.join();
std::cout << "result: " << num << std::endl;
return 0;
}

这段程序也可以得到正确的输出结果。

同时,unique_lock还有一个很方便的方法:try_lock(),这个函数会尝试锁住当前线程并返回一个bool值表示是否成功锁住,但不论是否成功锁住当前线程,当前线程都会继续运行。

原子变量和互斥锁的区别

互斥锁是通过线程休眠来等待其他线程完成当前任务,而原子变量是通过CPU空转来等待其他线程完成当前任务(准确的说是通过总线锁,但表现形式上和空转等待差不多)。因为没有发生线程切换,所以也就节省了线程切换时的开销。

第三部分:condition_variable

在有些场景下,一个线程可能希望满足一个条件后才继续运行,否则线程休眠等待该条件的发生。最典型的情况就是线程等待用户的输入,在没有用户输入时,线程直接休眠等待输入。这种情况就可以使用条件变量。

condition_variable库提供了方便的与条件变量相关的类和函数,封装在头文件<condition_variable>下。

构造

condition_variable类有默认构造函数,可以直接构造,不需要传递参数。 e.g.:

1
std::condition_variable cv;

方法

  • wait(unique_lock<mutex>& lck, Predicate pred)方法
    • wait方法传入的两个参数分别为unique_lock和一个返回值为bool的函数
    • 如果pred函数返回值为真,线程会正常运行,否则线程会被阻滞。
    • 在调用wait函数的同时,lck.unlock()会被自动调用,以保证其他线程正常运行。
    • 当函数notified_all()被调用时,如果函数pred为真,线程会恢复运行。
  • notified_all()方法
    • notified_all将试图唤醒所有被此condition_variable阻滞的线程,只有在线程wait函数的返回值为真的情况下程序会恢复运行

下面是一个例子:

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
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;

bool flag = false;

int main()
{
std::ios::sync_with_stdio(false);
std::thread thread_a([]()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [=](){return flag;});
lock.unlock();
for (int i = 0; i < 100; i++)
std::cerr << "thread a once\n";
});
std::thread thread_b([]()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [=](){return flag;});
lock.unlock();
for (int i = 0; i < 100; i++)
std::cerr << "thread b once\n";
});
thread_a.detach();
thread_b.detach();
mtx.lock();
for (int i = 0; i < 10; i++)
std::cout << "thread main: " << i << std::endl;
flag = true;
mtx.unlock();
cv.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(5));
return 0;
}

通过condition_variable的延迟使得主线程中的内容先输出后子线程的内容才输出。

另外一个例子:

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

// 定义条件变量
std::condition_variable cv;
// 条件变量需要配合互斥锁使用
std::mutex mtx;

// 定义一个函数模拟用户产生输入
void user(std::string &str, bool &is_exit){
// 共计产生100个输入
for(int i=0; i<100; i++){
{
std::unique_lock lock(mtx);
// 产生一个输入
str = "input " + std::to_string(i);
// 唤醒一个等待中的线程
cv.notify_one();
}
// 线程休眠1s,模拟用户每秒产生一个输入
std::this_thread::sleep_for(1s);
}
is_exit = true;
// 唤醒一个等待中的线程
cv.notify_one();
}

int main(){
bool is_exit = false; // 用于标志线程是否运行完毕
std::string str; // 用于接收用户输入
std::thread t1(user, str, is_exit); // 启动模拟用户输入线程
while(true){
std::unique_lock lock(mtx);
// 这个条件变量等待的条件是,线程执行完毕或字符串非空(表示有用户输入)
cv.wait(lock, [](){return is_exit || !str.empty()});
// 如果线程结束,则退出主循环
if(is_exit) break;
// 打印用户输入,并清空用户输入,用于接收下一个用户输入
std::cout << str << std::endl;
str.clear();
}
}

条件变量的使用有几个关键点:

  • 必须配合互斥锁使用
  • 每当条件变量等待的条件发生变化,必须手动调用条件变量的唤醒函数notify_one()或notify_all()