基本概念
线程
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程则指的是在单一程序中同时运行多个线程来完成不同的工作,以提高程序的执行效率和响应时间。
并发与并行
并发是指两个或多个事件在同一时间间隔内运行,但在单核CPU上,由于只有一个CPU,某一时刻只能执行一个任务,因此实际上是通过任务切换来模拟并发,称为软件并发或假并发。
并行则是指两个或多个事件在同一时刻运行,这通常发生在多核CPU上,多个CPU核心可以同时执行不同的任务,实现真正的硬件并发。
进程
进程是系统资源分配的最小单位,是应用程序运行的环境。每个进程都有自己的地址空间和系统资源,进程之间相互独立,不能直接共享资源。
线程的创建与启动
在C++11之前,C++并没有直接提供线程支持,C++11引入了语言层面上的多线程支持,包含在头文中。
C11中的线程类
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
1.包含头文件#include 。
2.定义线程要执行的函数或可调用对象(如函数指针、lambda表达式、bind表达式、类成员函数等)。
3.创建std::thread对象,将线程要执行的函数或可调用对象作为构造函数的参数传入。
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([] {cout << "Thread2" << endl; });
// 线程函数为函数对象
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
同步与互斥
线程的同步与互斥确保了多个线程在访问共享资源时的正确性和数据的一致性。
原子操作
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
在C++中,原子操作是指那些在执行过程中不会被线程调度机制中断的操作。换句话说,原子操作是不可分割的,一旦开始执行,就会一直执行到完成,中间不会被其他线程的操作所打断。
unsigned long sum = 0;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
可以看到两个线程发生了数据竞争,当两个线程几乎同时访问sum时,它们的操作可能会交错进行,导致sum的增加次数少于预期的20000000次。这是因为,比如当t1和t2都读到sum的同一个值时(比如1000),它们各自将这个值加1后写回,但写回的结果只反映了其中一个线程的修改(比如两个线程都写回了1001)。
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
{
m.lock();
sum++;
m.unlock();
}
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。
#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum ++; // 原子操作
}
int main()
{
cout << "Before joining, sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining, sum = " << sum << std::endl;
return 0;
}
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
mutex(互斥锁)
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
函数名 | 功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
线程函数调用lock()时,可能会发生以下三种情况:
1.如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
2.如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用try_lock()时,可能会发生以下三种情况:
1.如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
2.如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
lock_guard
自动管理锁:std::lock_guard是一个简单的RAII封装器,它在构造时自动锁定std::mutex,并在析构时自动解锁。这确保了即使在发生异常时,锁也能被正确释放。
不可复制和不可移动:std::lock_guard对象是不可复制和不可移动的,这有助于防止潜在的死锁问题。
用途:适用于简单的锁定场景,其中不需要在锁保持期间进行条件等待或手动解锁。
unique_lock
更灵活的锁管理:std::unique_lock提供了比std::lock_guard更灵活的锁管理功能。它可以在需要时手动锁定和解锁,支持延迟锁定(在构造时不立即锁定),以及条件变量的等待。
可复制和可移动(但通常不推荐):虽然std::unique_lock对象在技术上是可以复制和移动的,但这种操作会释放原始锁并获取新锁(或保持无锁状态),这通常不是期望的行为,因此在实际使用中应谨慎。
用途:适用于需要更细粒度控制锁的场景,如条件等待、锁的所有权转移等。
std::lock_guard和std::unique_lock是基于RAII原则的互斥锁封装器,用于自动管理锁的生命周期。
支持两个线程交替打印,一个打印奇数,一个打印偶数
#include <thread>
#include <mutex>
#include <condition_variable>
void two_thread_print()
{
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);
c.wait(lock, [&]()->bool {return flag; });
cout << i << endl;
flag = false;
i += 2; // 偶数
c.notify_one();
}
});
thread t2([&]() {
int j = 1;
while (j < n)
{
unique_lock<mutex> lock(mtx);
c.wait(lock, [&]()->bool {return !flag; });
cout << j << endl;
j += 2; // 奇数
flag = true;
c.notify_one();
}
});
t1.join();
t2.join();
}
int main()
{
two_thread_print();
return 0;
}