目录
前言:
1.线程库的使用
1.1.thread库
1.2.mutex库
1.3.condition_variable库
1.4.atomic库
2.线程安全问题
2.1.智能指针
2.2.STL容器
前言:
操作系统:线程-CSDN博客 我们曾经在这篇博客中提及了“语言”和“pthread库”之间的关系,也知道了:不同的语言实现多线程的本质就是对不同系统实现的多线程的实现进行封装。
// std::thread;
// C++跨平台封装对应系统的thread的实现 ---
#ifdef _WIN32
CreatThread()
#else
pthread_create();
#endif
线程的学习我们已经在前面的几篇操作系统博客中进行了系统的学习了,接下来我们主要对C++11线程库提供的接口进行学习。
在开始这篇博客的学习之前,我们需要回顾 操作系统:线程互斥|线程同步|锁的概念-CSDN博客
1.线程库的使用
1.1.thread库
在Thread库中,C++11线程库实现了一个thread类
下面是class thread提供的常用接口:
operator= | Move-assign thread | 支持移动赋值 |
get_id | Get thread id | 查看当前线程id |
join | Join thread | 等待当前线程 |
detach | Detach thread | 分离该线程 |
joinable | Check if joinable | 检查是否处于等待 |
swap | Swap threads | 交换两个线程 |
thread库的使用
如图为:thread的构造函数,thread支持无参构造和传入可调用对象,与其参数进行初始化。具体用法如代码所示:
void* PrintFunc(const int& num, const string& s)
{
cout <<"当前线程id为: " << this_thread::get_id() << endl;
for (size_t i = 0; i < num; i++)
{
cout << s.c_str() << endl;
}
return nullptr;
}
struct PrintStruct
{
void operator()(const int& num, const string& s)
{
cout << "当前线程id为: " << this_thread::get_id() << endl;
for (size_t i = 0; i < num; i++)
{
cout << s.c_str() << endl;
}
}
};
auto PrintLambda = [](const int& num, const string& s)
{
cout << "当前线程id为: " << this_thread::get_id() << endl;
for (size_t i = 0; i < num; i++)
{
cout << s.c_str() << endl;
}
};
function<void(const int& num, const string& s)> func1 = *PrintFunc;
// thread的初步使用
int main()
{
// 会出现线程安全问题!4个线程同时访问显示器这一块公共资源
// 带参构造,创建可执行线程
thread t1(PrintFunc, 3, "通过函数指针打印");
thread t2(PrintStruct(), 3, "通过仿函数打印");
thread t3(PrintLambda, 3, "通过lambda打印");
thread t4(func1,3, "通过函数指针打印");
cout << "thread-1: " << t1.get_id() << endl;
cout << "thread-2: " << t2.get_id() << endl;
cout << "thread-3: " << t3.get_id() << endl;
cout << "thread-4: " << t4.get_id() << endl;
// 一定需要join不然会崩溃
t1.join();
t2.join();
t3.join();
t4.join();
}
c++11中thread库,通过万能引用可以支持左值的可调用对象和右值的可调用对象进入,并且函数指针、仿函数、lambda表达式、function包装器都可以作为参数传入!
void* PrintFunc(const int& num, const string& s)
{
cout <<"当前线程id为: " << this_thread::get_id() << endl;
for (size_t i = 0; i < num; i++)
{
cout << s.c_str() << endl;
}
return nullptr;
}
int main()
{
int num = 10;
// 创建10个未初始化的线程
vector<thread> threads(num);
size_t i = 0;
// 创造空对象,接着移动赋值
for (auto& e : threads)
{
// 移动赋值---匿名对象为将亡值
e = thread(PrintFunc, 3, "thread-" + to_string(i++));
}
// 主线程进行join
for (auto& e : threads)
{
e.join();
}
// // 创造空对象,接着移动构造
thread t1(PrintFunc, 3, "通过函数指针打印");
thread t2(move(t1));
// 移动构造后 不能再对t1做任何操作
// cout << t1.get_id() << endl;
// t1.join();
cout << t2.get_id() << endl;
t2.join();
}
另外,我们可以通过STL容器进行无参构造多个线程,通过这个thread的容器来进行对线程进行相同的操作!并且我们知道,线程是具有唯一性的,所以不能进行拷贝构造和赋值,但是thread库支持移动赋值和移动构造,将一个线程转交给另一个线程,而不是拷贝两个一样的线程!
另外在thread库中实现了,this_thread这个命名空间,通过这个命名空间,我们可以访问当前线程的信息,也可以对其进行休眠或者是完成任务后将时间片交给其他线程。
1.2.mutex库
我们知道多线程访问临界区时会出现数据错误或者数据不一致的问题,所以C++11也根据不同的操作系统统一封装了mutex库。在mutex库中,我们主要讲解mutex、lock_guard、unique_lock。
mutex
mutex _mutex;
{
_mutex.lock(); // 我们也可以通过_mutex.try_lock(); 如果没加锁就锁上
// 临界区访问
_mutex.unlock();
}
具体应用:
// thread + lambda + 锁
int main()
{
size_t num1 = 0;
size_t num2 = 0;
cin >> num1 >> num2;
size_t result = 0;
// 锁
mutex _mutex;
// 利用捕获列表来传参
thread t1([num1, &result, &_mutex]()
{
for (size_t i = 0; i < num1; i++)
{
_mutex.lock();
result++;
_mutex.unlock();
}
});
thread t2([num2, &result, &_mutex]()
{
for (size_t i = 0; i < num2; i++)
{
_mutex.lock();
result++;
_mutex.unlock();
}
});
}
这里也体现了:在简单的线程逻辑中,我们一般是通过lambda表达式来实现的!
lock_guard和unique_lock
// 守卫锁
int main()
{
mutex _mutex;
{
_mutex.lock();
// todo
_mutex.unlock();
}
// 当在todo区、unlock之前如果抛异常?
// 就会出现死锁的问题
// 所以我们需要一个守卫锁---LockGuard---Linux我们手搓过了
// 来实现在程序异常后,锁资源的正常释放!
// mutex中实现了lock_guard
{
lock_guard<mutex> guard(_mutex);
}
// unique_lock
// 1.支持手动加锁、解锁,配合某些特定场景
{
unique_lock<mutex> u_guard(_mutex);
u_guard.unlock();
// todo
u_guard.lock();
}
}
lock_guard和unique_guard这两个类实现了构造时加锁,析构时解锁。并且unique_lock能够实现在临界区中手动加锁、解锁。
1.3.condition_variable库
condition_variable实例化出对象后,调用wait函数需要和unique_lock配合使用。并且我们可以通过notify_one唤醒一个处于wait阻塞的线程,或者是通过notify_all唤醒所以wait的线程。
// 条件变量
int main()
{
int num = 1;
condition_variable signal;
mutex _mutex;
// 实现逻辑:奇数、偶数依次打印
bool flag = false;
thread t1([&]()
{
for (int i = num; i < 10; i++)
{
unique_lock<mutex> lock(_mutex); // 当前生命周期加锁
while (flag == true) // 轮询,等待另一个设置为false
{
// 阻塞打印奇数,当前线程
signal.wait(lock); // wait时释放锁,被唤醒后加锁
}
cout <<"奇数" << this_thread::get_id() << ", " << num << endl;
num++;
flag = true;
// 唤醒打印偶数
signal.notify_one();
}
});
thread t2([&]()
{
for (int i = num; i < 10; i++)
{
unique_lock<mutex> lock(_mutex);
while (flag == false)
{
// 唤醒打印偶数,阻塞打印奇数
signal.wait(lock);
}
cout << "偶数" << this_thread::get_id() << ", " << num << endl;
num++;
// 偶数打印完计为false,下次循环将奇数释放
flag = false;
signal.notify_one();
}
});
t1.join();
t2.join();
}
代码实现的逻辑如下:
- 在创建线程之前,我们设置此时的flag为false,当创建两个线程之后,此时CPU调度t1、t2的顺序我们是未知的,但是通过flag我们一定确定奇数先打印、然后偶数阻塞直至flag = true
- 当奇数完成打印时,将flag置为true,并且notify_one唤醒wait中的偶数线程,因为t2中while条件为假,不进入while则开始打印偶数。与此同时因为flag为true,在t1中会不断的轮询将t1的线程进行阻塞
- 1、2即为实现的逻辑,另外我们也要知道奇数拿到锁后进入wait阻塞时,需要将锁释放,不然会导致偶数无法获得锁。
另外当我们分析时,需要考虑多个场景:比如假设t1先被调用,t2后被调用或者是t2先被调用,t1后被调用,以及不同的时间片下分析可能会出现不一样的情况,但可以肯定的是,最终都可以实现两个线程交替打印奇偶数……
1.4.atomic库
对于atomic这个结构体的学习,本质上就是学习对变量进行原子操作。具体的实现称为CAS无锁编程,我们在下面挂了网址,无锁编程的原理大家可以学习一下。
// 较短临界区操作---atomic原子操作
int main()
{
atomic<int> num = 0;
// int num = 0;
thread t1([&num]()
{
for (int i = 0; i < 1000; i++)
{
++num;
}
});
thread t2([&num]()
{
for (int i = 0; i < 1000; i++)
{
++num;
}
});
t1.join();
t2.join();
std::cout << num << endl;
// 延申CAS无锁编程
// https://coolshell.cn/articles/8239.html#google_vignette
// atomic的实质借助do while循环和bool CAS函数的实现
}
2.线程安全问题
2.1.智能指针
对于 unique_ptr,,由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。而对于 shared_ptr,,多个对象需要共用一个引用计数变量,,所以会存在线程安全问题.。但是标准库实现的时候考虑到了这个问题。 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。
我们知道我们在实现shared_ptr时需要维护一个int*的指针变量,当在多线程下,我们对这个智能指针对象进行指向时,这个int*的变量就成为了共享资源,也就是会出现数据不一致的问题。具体处理,就是将int*变量通过atomic<int>*来进行替换,这样就实现“原子性”。
2.2.STL容器
原因是: STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.
那么我们具体如何保证STL在多线程编程下的线程安全呢?
- 尽量不修改容器的大小和结构。例如我们在生产、消费模型中,我们设计的模型就是一个固定大小的容器,这样就不涉及了STL的修改,自然就没有线程安全了。
- 再进行对STL容器访问时,通过锁、条件变量等机制,来限定只允许一个线程来对STL容器进行访问
- 使用线程安全的包装器。