目录
一 概念
1 多线程
2 多进程与多线程
3 多线程理解
二 创建线程
1 thread
2 join() 和 detach()
3 this_thread
三 std::mutex
1 lock 和 unlock
2 lock_guard
3 unique_lock
四 condition_variable
五 std::atomic
一 概念
1 多线程
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接 口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的 线程,必须包含< thread >头文件.
C++11引入了对多线程编程的支持,主要提供了以下几个组件:
- std::thread类:用于创建和管理线程的对象。
- std::mutex类:用于实现互斥访问,保护共享资源的完整性。
- std::condition_variable类:用于线程间的条件同步。
- std::atomic模板类:用于实现原子操作,确保数据的原子性
2 多进程与多线程
进程是指一个程序的运行实例,而线程是指进程中独立的执行流程。一个进程可以有多个线程,多个线程之间可以并发执行。
- 一个程序有且只有一个进程,但可以拥有至少一个的线程。
- 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。
创建一个新线程的代价要比创建一个新进程小得多 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多 线程占用的资源要比进程少很多
3 多线程理解
二 创建线程
1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的 状态。
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
3. C++支持多线程编程,主要使用的是线程库<thread>
1 thread
示例1: 线程函数(函数指针)
#include<iostream>
#include<thread>
using namespace std;
void Print1(int n, int j)
{
cout << "n: " << n << " j: " << j << endl;
}
void Print2()
{
cout << "Test2" << endl;
}
int main()
{
// 线程函数为函数指针
thread t1(Print1, 100, 5);
thread t2(Print2);
t1.join();
t2.join();
return 0;
}
上述示例中,我们创建了两个线程t1和t2,使用函数Print1()
和Print2()
作为线程的执行函数,并使用join()
函数等待线程执行完成。
示例2: 执行函数有引用参数
void Print(int n, int& rx)
{
rx += n;
cout << "Test: " << rx << endl;
}
int main()
{
int x = 0;
thread t1(Print, 10000, ref(x));// 只有添加 ref 才能会被底层认为是引用
thread t2(Print, 20000, ref(x));
t1.join();
t2.join();
cout << x << endl;
return 0;
}
std::ref
可以包装按引用传递的值为右值。std::cref
可以包装按const
引用传递的值为右值。
示例3: lambda 表达式
int main()
{
int x = 0;
auto Func = [&](int n)
{
x += n;
};
thread t1(Func, 10000);
thread t2(Func, 20000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
2 join() 和 detach()
当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。比如上例中的join。
- detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
- join方式,等待启动的线程完成,才会继续往下执行。
示例1: join
oin后面的代码不会被执行,除非子线程结束
void thread_1()
{
while (1)
{}
}
void thread_2(int x)
{
while (1)
{}
}
int main()
{
thread first(thread_1); // 开启线程,调用:thread_1()
thread second(thread_2, 100); // 开启线程,调用:thread_2(100)
first.join();
second.join(); //join完了之后,才能往下执行。
while (1)
{
std::cout << "主线程\n";
}
return 0;
}
示例2: detach
主线程不会等待子线程结束。如果主线程运行结束,程序则结束。
void thread_1()
{
while (1)
{
cout << "子线程1" << endl;
}
}
void thread_2(int x)
{
while (1)
{
cout << "子线程2" << endl;
}
}
int main()
{
thread first(thread_1); // 开启线程,调用:thread_1()
thread second(thread_2, 100); // 开启线程,调用:thread_2(100)
first.detach();
second.detach();
for (int i = 0; i < 10; i++)
{
std::cout << "主线程\n";
}
return 0;
注意:
1. 线程是在thread对象被定义的时候开始执行的,而不是在调用join()函数时才执行的,调用join()函数只是阻塞等待线程结束并回收资源。
2. 分离的线程(执行过detach()的线程)会在调用它的线程结束或自己结束时自动释放资源。
3. 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
4. 若没有执行join()或detach()的线程在程序结束时会引发异常
3 this_thread
this_thread是一个类,它有4个功能函数,具体如下:
此外,this_thread
还包含重载运算符==
和!=
,用于比较两个线程是否相等。
void my_thread()
{
std::cout << "Thread " << std::this_thread::get_id() << " start!" << std::endl;
std::this_thread::yield(); // 让出当前线程的时间片
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 线程休眠200毫秒
std::cout << "Thread " << std::this_thread::get_id() << " end!" << std::endl;
}
int main()
{
std::cout << "Main thread id: " << std::this_thread::get_id() << std::endl;
std::thread t1(my_thread);
std::thread t2(my_thread);
t1.join();
t2.join();
return 0;
}
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个 线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
a 采用无参构造函数构造的线程对象 b 线程对象的状态已经转移给其他线程对象 c 线程已经调用jion或者detach结束
并发与并行的区别?
并发和并行都可以是调用了很多线程. 如果这些线程能同时被多个处理器执行,那就是并行的;如果是轮流切换执行,那就是并发.
三 std::mutex
在多线程编程中,需要注意以下问题:
- 线程之间的共享数据访问需要进行同步,以防止数据竞争和其他问题。可以使用互斥量、条件变量等机制进行同步。
- 可能会发生死锁问题,即多个线程互相等待对方释放锁,导致程序无法继续执行。
- 可能会发生竞态条件问题,即多个线程执行的顺序导致结果的不确定性。
mutex头文件主要声明了与互斥量(mutex)相关的类。mutex提供了4种互斥类型,如下表所示。
1 lock 和 unlock
std::mutex
是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。
示例1:
#include <mutex>
std::mutex mtx;
int num = 0;
void thread_func(int& n)
{
for (int i = 0; i < 10; ++i)
{
mtx.lock();
n++;
cout << "n: " << n << endl;;
mtx.unlock();
}
}
int main()
{
std::thread myThread[10];
for (std::thread& a : myThread)
{
a = std::thread(thread_func, std::ref(num));
a.join();
}
std::cout << "num = " << num << std::endl;
std::cout << "Main thread exits!" << std::endl;
return 0;
}
2 lock_guard
std::lock_guard是C++标准库中的一个模板类,用于实现资源的自动加锁和解锁。它是基于RAII的设计理念,能够确保在作用域结束时自动释放锁资源,避免了手动管理锁的复杂性和可能出现的错误。
std::lock_guard的主要特点如下:
1 自动加锁: 在创建std::lock_guard对象时,会立即对指定的互斥量进行加锁操作。这样可以确保在进入作用域后,互斥量已经被锁定,避免了并发访问资源的竞争条件。
2 自动解锁:std::lock_guard对象在作用域结束时,会自动释放互斥量。无论作用域是通过正常的流程结束、异常抛出还是使用return语句提前返回,std::lock_guard都能保证互斥量被正确解锁,避免了资源泄漏和死锁的风险。
3 适用于局部锁定: 由于std::lock_guard是通过栈上的对象实现的,因此适用于在局部范围内锁定互斥量。当超出std::lock_guard对象的作用域时,互斥量会自动解锁,释放控制权。
#include <mutex>
std::mutex mtx;
int num = 0;
void thread_func(int& n)
{
std::lock_guard<std::mutex> lock(mtx); // 加锁互斥量
std::cout << "n: " << n++ << std::endl;
// 执行需要加锁保护的代码
}
int main()
{
std::thread myThread[10];
for (std::thread& a : myThread)
{
a = std::thread(thread_func, std::ref(num));
a.join();
}
std::cout << "num == " << num << std::endl;
return 0;
}
3 unique_lock
std::unique_lock的主要特点如下:
- 自动加锁和解锁: 与std::lock_guard类似,std::unique_lock在创建对象时立即对指定的互斥量进行加锁操作,确保互斥量被锁定。在对象的生命周期结束时,会自动解锁互斥量。这种自动加锁和解锁的机制避免了手动管理锁的复杂性和可能出现的错误。
- 支持灵活的加锁和解锁: 相对于std::lock_guard的自动加锁和解锁,std::unique_lock提供了更灵活的方式。它可以在需要的时候手动加锁和解锁互斥量,允许在不同的代码块中对互斥量进行多次加锁和解锁操作。
- 支持延迟加锁和条件变量:std::unique_lock还支持延迟加锁的功能,可以在不立即加锁的情况下创建对象,稍后根据需要进行加锁操作。此外,它还可以与条件变量(std::condition_variable)一起使用,实现更复杂的线程同步和等待机制。
#include <mutex>
std::mutex mtx; // 互斥量
void thread_function()
{
std::unique_lock<std::mutex> lock(mtx); // 加锁互斥量
std::cout << "Thread running" << std::endl;
// 执行需要加锁保护的代码
lock.unlock(); // 手动解锁互斥量
// 执行不需要加锁保护的代码
lock.lock(); // 再次加锁互斥量
// 执行需要加锁保护的代码
}
// unique_lock对象的析构函数自动解锁互斥量
int main()
{
std::thread t1(thread_function);
t1.join();
std::cout << "Main thread exits!" << std::endl;
return 0;
}
在上述示例中,std::unique_lock
对象lock
会在创建时自动加锁互斥量,析构时自动解锁互斥量。我们可以通过调用lock
和unlock
函数手动控制加锁和解锁的时机,以实现更灵活的操作。
四 condition_variable
std::condition_variable
是C++标准库中的一个类,用于在多线程编程中实现线程间的条件变量和线程同步。它提供了等待和通知的机制,使得线程可以等待某个条件成立时被唤醒,或者在满足某个条件时通知其他等待的线程。其提供了以下几个函数用于等待和通知线程:
std::condition_variable的主要特点如下:
- 等待和通知机制:std::condition_variable允许线程进入等待状态,直到某个条件满足时才被唤醒。线程可以调用wait函数进入等待状态,并指定一个互斥量作为参数,以确保线程在等待期间互斥量被锁定。当其他线程满足条件并调用notify_one或notify_all函数时,等待的线程将被唤醒并继续执行。
- 与互斥量配合使用:std::condition_variable需要与互斥量(std::mutex或std::unique_lock<std::mutex>)配合使用,以确保线程之间的互斥性。在等待之前,线程必须先锁定互斥量,以避免竞争条件。当条件满足时,通知其他等待的线程之前,必须再次锁定互斥量。
- 支持超时等待:std::condition_variable提供了带有超时参数的等待函数wait_for和wait_until,允许线程在等待一段时间后自动被唤醒。这对于处理超时情况或限时等待非常有用。
示例:支持两个线程交替打印,一个打印奇数,一个打印偶数, 并且偶数优先
#include <thread>
#include <mutex>
#include <condition_variable>
int main()
{
std::mutex mtx;
condition_variable c;
int n = 100;
bool flag = true;
thread t1([&]() {
int i = 0;
while (i < n)
{
unique_lock<mutex> lock(mtx);
while (!flag)
c.wait(lock);
cout << i << endl;
flag = false;
i += 2; // 偶数
c.notify_one();
}
});
thread t2([&]() {
int j = 1;
while (j < n)
{
unique_lock<mutex> lock(mtx);
while (flag)
c.wait(lock);
cout << j << endl;
j += 2; // 奇数
flag = true;
c.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
五 std::atomic
std::mutex可以很好地解决多线程资源争抢的问题,但它每次循环都要加锁、解锁,这样固然会浪费很多的时间。
在 C++ 中,std::atomic 是用来提供原子操作的类,atomic,本意为原子,原子操作是最小的且不可并行化的操作。这就意味着即使是多线程,也要像同步进行一样同步操作原子对象,从而省去了互斥量上锁、解锁的时间消耗。
使用 std::atomic 可以保证数据在操作期间不被其他线程修改,这样就避免了数据竞争,使得程序在多线程并发访问时仍然能够正确执行。
示例:
#include<vector>
#include <atomic>
int main()
{
atomic<int> x = 0;//不用加锁了
auto Func = [&](int n)
{
for (int i = 0; i < n; i++)
{
x++;
}
};
int n;
cin >> n;
vector<thread> vthds(n);
for (auto& thd : vthds)
{
thd = thread(Func, 10);
}
for (auto& thd : vthds)
{
thd.join();
}
cout << x << endl;
return 0;
}
总结: C++ 多线程基础就是这些, 难度不大, 重在理解, 但是要注意线程安全, 合理使用. 继续加油!