一、线程创建
创建线程的函数 thread t (函数名f,函数 f 的参数) 或者 用lambda表达式
代码:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void output(string input,int a) {
cout << input << endl;
cout << a << endl;
}
int main() {
string text = "Hello World ! My name is XXX . Nice to meet you . bla bla bla\n";
vector<thread> thread_list;
int a = 10;
for (int i = 0; i < 2; i++) {
thread t(output,text,a);
//thread t([text](int a) {
// cout << text << endl;
// cout << a << endl;
// },a);
//上面为lambda表示法,效果相同,可提高可读性,简化代码,但是运行速度,效率不如内敛函数incline
thread_list.push_back(move(t));
}
for (auto& t : thread_list) {
if (t.joinable()) {
t.join();
}
}
return 0;
}
由于是多线程,导致可能出现不同线程,交替输出甚至短暂截断输出的情况! 比如:第一个线程输出到 “ Hello World ! My name is XXX . ” 时,第二个线程就开始输出,然后第一个线程又继续输出剩余内容,最后就是多个线程抢用一个输出,导致输出结果交叉混杂。
for循环中,给vector添加数据时,使用move函数,相当于将 t 的值直接存入thread_list,不用创建额外thread变量来存入,且move以后,t 就处于未定义状态了
最后一定要 join 释放资源,否则运行程序会报错!
二、线程传参
左值:等式左边的值
右值:等式右边的值
例:a = a+2*b+3; a是左值,可重复使用;(a+2*b+3)是右值,临时计算的数据
如果是函数要求左值引用传参,即int& a,则需要用ref()将参数包装起来,否则编译报错
代码:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void output(string input,int& a) {//此处 a 为引用传值!
cout << input << endl;
cout << ++a << endl;
}
int main() {
string text = "Hello World ! My name is XXX . Nice to meet you . bla bla bla\n";
vector<thread> list;
int a = 10;
for (int i = 0; i < 2; i++) {
thread t(output,text,ref(a));//用 ref 包装参数 a
//thread t([text](int& a) {
// cout << text << endl;
// cout << ++a << endl;
// },ref(a));
// lambda写法的修改 上同
list.push_back(move(t));
}
cout <<"before join: "<< a << endl;
for (auto& t : list) {
if (t.joinable()) {
t.join();
}
}
cout << "after join: " << a << endl;
return 0;
}
输出:
还有一点,左值引用传递时,不可传递右值,否则报错
如果是右值引用传参,即int&& a ,则不能用ref(),否则会报错
输出结果中,先打印 before join: 10 ,是因为线程异步,当for循环结束时,由于线程启动、调用传入函数等操作需要时间开销,导致线程还没开始输出,而这时主程序已经运行到cout这一行了,所以就会先输出 before join: 10
三、Mutex与Atomic
3.1、mutex(互斥锁)
用于保护共享数据免受并发访问的冲突,通过阻塞和唤醒线程来管理共享资源的访问。
主要特点:
- 互斥访问:确保同一时间只有一个线程可以访问被保护的资源。
- 锁的所有权:通过
lock()
和unlock()
成员函数来获取和释放锁。通常使用lock_guard
或unique_lock
来自动管理锁的生命周期。 - 死锁:不当的使用可能导致死锁,例如,两个线程相互等待对方释放锁。
- 上下文切换:当一个线程等待锁时,可能会发生上下文切换,增加系统开销。
优点:
- 提供强同步,确保数据的一致性和线程安全。
- 适用于需要保护复杂数据结构或执行复杂同步操作的场景。
缺点:
- 可能导致性能开销,特别是在高争用的情况下。
- 需要正确管理锁的获取和释放,否则可能引发死锁。
代码:
#include <iostream>
#include <mutex>
#include <thread>
mutex mtx; // 创建互斥锁
void printID(int id) {
lock_guard<mutex> lock(mtx); // 锁定互斥锁
cout << "ID: " << id << endl;
}
int main() {
thread t1(printID, 1);
thread t2(printID, 2);
t1.join();
t2.join();
return 0;
}
互斥锁的释放时机:
std::lock_guard
对象的生命周期结束时,互斥锁会被自动释放。- 这通常发生在以下情况:
- 作用域结束,
lock_guard
对象被销毁。 - 程序流程离开包含
lock_guard
对象的代码块,例子如下
- 作用域结束,
void Function() {
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx); // 互斥锁在此处被锁定
// 在这个作用域内,mtx 被锁定
} // 作用域结束,lock_guard 对象被销毁,互斥锁在此处被释放
// 此时 mtx 已经解锁,可以再次被锁定
}
使用 lock_guard
的好处:
- 自动管理:自动获取和释放互斥锁,减少忘记手动释放锁的风险。
- 异常安全:保证即使在发生异常时互斥锁也能被正确释放,避免死锁。
- 代码简洁:使代码更加简洁,易于理解和维护。
3.2、atomic(原子锁)
C++11引入的用于实现原子操作的模板类。原子操作保证在多线程环境中,单个操作不可分割,不会出现中间状态。
主要特点:
- 不可分割的操作:原子操作要么完全执行,要么完全不执行,不会出现中间状态。
- 内存顺序:提供不同内存顺序选项,如
memory_order_relaxed
、memory_order_acquire
等。 - 不阻塞:原子操作通常不会阻塞线程,而是通过硬件支持来实现同步。
- 只支持基本类型:主要支持基本数据类型(如整数类型、指针类型)。
优点:
- 性能较高,特别是对于简单的数据类型和操作。
- 不会引起线程阻塞,减少了上下文切换的开销。
缺点:
- 功能有限,主要适用于简单的数据类型和操作。
- 需要仔细处理内存顺序,以避免数据竞争和不一致性。
代码:
#include <atomic>
#include <thread>
atomic<int> count(0); // 创建原子变量
void increment() {
count.fetch_add(1, memory_order_relaxed); // 令 count 的值 +1
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "Final count: " << count << endl;
return 0;
}
四、自旋锁spinlock
自旋锁(Spinlock)
定义: 一种简单锁机制,当一个线程尝试获取一个已被其他线程持有的锁时,该线程不会立即阻塞(即不会进入睡眠状态),而是在当前位置循环(自旋,占用CPU),直到锁被释放
用途:
自旋锁适用于锁持有时间短且线程持续占用CPU的场景。
它们常用于避免线程在等待锁时浪费CPU资源。
使用方式: 自旋锁通常在多核或低竞争环境中使用。在单核或高竞争环境中,自旋锁可能导致CPU资源浪费,因为线程在等待锁时仍然占用CPU。
如果有pthread库,使用起来很简单,但是一般不导入第三方库,我们需要使用atomic_flag一种轻量级的自旋锁机制,来达到自旋锁功能。
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;
class spin_lock {
private:
atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(memory_order_acquire)) {
// 循环直到能够设置flag
}
}
void unlock() {
flag.clear(memory_order_release);
}
};
int main() {
spin_lock lock;
thread t1([&]() {
lock.lock();
cout << "t1 thread" << endl;
// 临界区代码
lock.unlock();
});
thread t2([&]() {
lock.lock();
cout <<"t1 : "<< t1.get_id() << endl;
// 临界区代码
lock.unlock();
});
cout <<"t2 : "<< t2.get_id() << endl;
t1.join();
t2.join();
return 0;
}
五、条件变量condition_variable
定义: 一种同步机制,用于实现线程之间的等待和唤醒机制,可以与mutex(互斥锁)结合使用,实现复杂的线程同步和通信
用途:
条件变量用于线程间基于特定条件的协调,如生产者-消费者问题
它们可以减少线程在等待条件满足时的CPU资源浪费
使用方式: 条件变量通常与互斥锁(mutex)一起使用。线程在进入等待状态前需要锁定互斥锁,然后在等待条件变量时释放锁,并在被唤醒后重新尝试获取锁。
主要作用是允许一个或多个线程等待某个条件满足后再执行。在等待期间,线程会被阻塞,不会消耗CPU资源,直到其他线程通过通知(notify)来唤醒它们。
#include <iostream>
#include <thread>
#include <vector>
#include <condition_variable>
#include <mutex>
using namespace std;
condition_variable cv;
mutex mt;
int b = 0;
void out(int id) {
unique_lock<mutex> lock(mt);
cv.wait(lock, [id]() {return (b == id); });
cout << b++ << endl;
cv.notify_one();
}
int main() {
vector<thread> threadList;
thread t;
for (int i = 0; i < 3; i++) {
t = thread(out,i);
threadList.push_back(move(t));
//threadList.emplace_back(out, i);
}
for (auto& tmp : threadList) {
if (tmp.joinable()) {
tmp.join();
}
}
return 0;
}
六、join与detach
(1)join
join函数会阻塞线程,直到线程函数执行结束,再执行后续语句
优点:可以保证资源的清理和数据的一致性,因为主线程会在所有子线程结束后再结束
缺点:如果主线程等待时间过长,可能会导致程序的响应性降低
(2)detach
detach
将线程分离,使其在后台继续运行而不与任何线程关联。分离后的线程称为守护线程(daemon thread)
优点:允许线程在后台运行而不影响主线程的结束,适用于长时间运行的任务,如日志记录或资源监控
缺点:如果分离线程访问了主线程中的对象,而该对象在主线程结束后被销毁,可能会导致未定义行为或程序崩溃