0.前言
并发编程的两种模型:
1.多进程
每个进程只是一个线程,进程间可以相互通信;进程通信的方式有:文件、管道、消息队列。
2.多线程
一个进程有多个线程,线程间通过共享内存的方式进行通信。
3.优缺点:
- 相对于进程来说,线程启动速度很快,因为系统会分配一系列的内部资源来管理进程,所以线程更轻量级。
- 线程开销更低,进程开销更大;操作系统要对进程提供很多的保护,而且通过共享内存的通信方式要比通过文件、管道、消息队列的方式速度快很多。所以多线程要比多进程性能优越。
- 但是多线程很难管理,有很多多线程的特定问题需要处理
- 多线程不能在分布式系统下运行,而多进程则没有这个问题
由于并发编程是一个很深的问题,这篇博客先记录一些多线程基础的内容,后面会针对每一个核心的子内存单独更新学习笔记!
1.核心对象和函数
1.1thread
创建一个线程有多种方式
方式1:通过函数构造
void function_1() {
cout << "www.lalal.com" << endl;
}
int main() {
thread t1(function_1);
//主线程会等待t1线程的执行
t1.join();
//各自不关联的执行
t1.detach();
//detach后进行join是会报错的,可以进行判断能否join来做安全保护所以要判断
if (t1.joinable()) {
t1.join();
}
return 0;
}
方式2:通过任何可被调用的对象构造
class Factor {
public:
void operator()() {
for (int i = 0; i > -10; i--) {
cout << "from t1:" << i << endl;
}
}
void operator()(string& msg) {
for (int i = 0; i > -10; i--) {
cout << "from t1:" << i << endl;
}
}
};
int main() {
Factor fct;
thread t1(fct);
//注意括号;两种方式等效
thread t2((Factor()));
string s = "asd";
thread t3((Factor()),move(s));
//如果不做异常处理,当主线程中发生异常时,t1线程会被直接销毁
try
{
for (int i = 0; i < 100; i++) {
cout << "from main:" << i << endl;
}
}
catch (...)
{
t1.join();
throw;
}
t1.join();
return 0;
}
1.2 线程id
异步时不一定准确
//输出当前线程(主线程)id
cout << this_thread::get_id() << endl;
//输出t2线程id
cout << t2.get_id() << endl;
在实际应用中,对于一个复杂的问题,我们要创建多少个线程来解决呢?
这需要由CPU的核心数来决定,如果超出了CPU线程得限制反而会导致很多转换操作影响性能。
//支持得并发线程数
thread::hardware_concurrency();
1.3线程阻塞相关
yield :把CPU当前所有权返回给系统
sleep_for:阻塞至少时间,期间会让开执行权,时间到后会进入调度队列。所以等待时间为阻塞时间+等待时间
sleep_until:阻塞当前线程,直至抵达指定时间。期间不会让开执行权,一直忙等。
2.数据竞争与互斥对象
进行以下代码测试:
我们在主线程和t1线程中同时打印信息
void function_1() {
for (int i = 0; i > -10; i--) {
cout << "From t1:" << i << endl;
}
}
int main() {
thread t1(function_1);
for (int i = 0; i < 10; i++) {
cout << "From main:" << i << endl;
}
t1.join();
return 0;
}
可以发现,执行的过程中由于两个线程在为同一个资源cout竞争,所以输出很混乱。
2.1使用互斥对象同步资源
我们使用一个互斥锁来解决上面的问题,修改为如下代码:
#include"pch.h"
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
using namespace std;
//创建互斥对象
mutex mut;
void shared_print(const string msg, int id) {
mut.lock();
cout << msg <<id<< endl;
mut.unlock();
}
void function_1() {
for (int i = 0; i > -10; i--) {
shared_print("From t1:", i);
}
}
int main() {
thread t1(function_1);
for (int i = 0; i < 10; i++) {
shared_print("From main:", i);
}
t1.join();
return 0;
}
可以看到,现在不会出现打印混乱的情况了。
但这个代码仍然有一个致命的问题,就是当执行完mu.lock();后,程序出现了异常,那这个互斥锁将永远处于锁住的状态。
所以不推荐使用 lock()和 unlock()。可以使用 lock_guard<mutex>来解决这个问题
void shared_print(const string msg, int id) {
//有异常出现时会永远锁住,不推荐使用
//mut.lock();
//当guard对象析构时,不管有没有异常发生,都会对mut自动解锁
lock_guard<mutex> guard(mut);
cout << msg <<id<< endl;
//mut.unlock();
}
但异常情景下的问题虽然解决了,这个程序仍然是有问题的。由于cout是一个全局变量,所以cout并没有完全在mut的保护之下。其他的线程仍然可以在不加锁的情况下直接使用cout。所以需要更好的方法来解决这个问题。为了更好的保护资源,必须将互斥对象与资源进行绑定!
创建一个类来进行竞争资源的管理
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<fstream>
using namespace std;
//创建互斥对象
mutex mut;
class LofFile {
public:
LofFile() {
f.open("log.txt");
}
void shared_print(string id, int value) {
lock_guard<mutex> locker(m_mutex);
f << "From" << id << ":" << value << endl;
}
~LofFile()
{
f.close();
}
protected:
private:
mutex m_mutex;
std::ofstream f;
};
void function_1(LofFile& log) {
for (int i = 0; i > -10; i--) {
log.shared_print("From t1", i);
}
}
int main() {
LofFile log;
thread t1(function_1,ref(log));
for (int i = 0; i < 10; i++) {
log.shared_print("From main:", i);
}
t1.join();
return 0;
}
3.死锁
下面的代码由于申请锁和释放锁的顺序在两个线程中发生了冲突,所以会导致死锁
//创建互斥对象
mutex mut;
class LofFile {
public:
LofFile() {
//f.open("log.txt");
}
void shared_print(string id, int value) {
lock_guard<mutex> locker(m_mutex1);
lock_guard<mutex> locker2(m_mutex2);
cout << "From" << id << ":" << value << endl;
}
void shared_print2(string id, int value) {
lock_guard<mutex> locker2(m_mutex2);
lock_guard<mutex> locker(m_mutex1);
cout << "From" << id << ":" << value << endl;
}
~LofFile()
{
//f.close();
}
protected:
private:
mutex m_mutex1;
mutex m_mutex2;
//std::ofstream f;
};
void function_1(LofFile& log) {
for (int i = 0; i > -100; i--) {
log.shared_print("From t1", i);
}
}
int main() {
LofFile log;
thread t1(function_1,ref(log));
for (int i = 0; i < 100; i++) {
log.shared_print2("From main:", i);
}
t1.join();
return 0;
}
如何避免死锁:
- 评估程序是否需要两个以上的mutex
- 避免在锁住mutex的同时去使用一些陌生的函数
- 如果必须同时锁住两个或以上的mutex,那么使用std::lock(mutex1,mutex2);去同时锁住,编译器会确保锁住的顺序不会有问题
4.UniqueLock与Initialization
4.1 unique_lock
uniqye_lock可以被移动,lock_guard不能被移动
unique_lock可以用于对特定的代码块加锁,如下述代码:
void shared_print(string id, int value) {
std::unique_lock<std::mutex> locker(m_mutex1);
cout << "From" << id << ":" << value << endl;
locker.lock();
cout << "被锁住内容" << endl;
locker.unlock();
cout << "没被锁住的内容" << endl;
locker.lock();
cout << "又被锁住了!!" << endl;
locker.unlock();
//支持移动语义
unique_lock<mutex> locker2 = move(locker);
}
unique_lock<>比lock_guard多了很多功能,但是带来方便的同时也带来了性能的损耗;如果更关注性能并且不需要这么多操作就不要使用unique_lock;
4.2 initialization