线程、进程
线程(Thread) 是操作系统能够进行运算调度的最小单位。它是进程中的一个执行流程,一个进程可以包含多个线程。为了更好地理解线程,我们需要先了解 进程(Process)。
1. 进程 vs. 线程
-
进程:
-
进程是程序的一次执行过程,是操作系统资源分配的基本单位。
-
每个进程有独立的内存空间(代码、数据、堆栈等)。
-
进程之间相互隔离,一个进程崩溃不会影响其他进程。
-
-
线程:
-
线程是进程中的一个执行单元,是 CPU 调度的基本单位。
-
线程共享进程的内存空间(代码、数据、堆等),但每个线程有自己的栈空间。
-
线程之间可以方便地共享数据,但也可能导致数据竞争(Data Race)等问题。
-
2. 线程的特点
-
轻量级:线程的创建和切换比进程更快,因为它们共享进程的资源。
-
并发执行:多个线程可以在同一时间内并发执行(如果是多核 CPU,甚至可以并行执行)。
-
共享资源:线程可以访问进程的全局变量和堆内存,这使得线程之间的通信更加高效。
-
独立性:每个线程有自己的程序计数器(PC)、栈和寄存器状态。
3. 线程的用途
-
提高程序性能:通过多线程,可以将任务分解为多个子任务并发执行,充分利用多核 CPU 的计算能力。
-
例如:一个视频播放器可以用一个线程解码视频,另一个线程播放音频。
-
-
响应性:在图形用户界面(GUI)程序中,主线程负责更新界面,而其他线程可以处理后台任务(如网络请求、文件读写等),避免界面卡顿。
-
例如:浏览器可以用一个线程加载网页,另一个线程响应用户输入。
-
-
异步任务:线程可以用于执行异步任务,比如定时任务、后台计算等。
4. 线程的生命周期
线程的生命周期包括以下几个状态:
-
新建(New):线程被创建,但尚未启动。
-
就绪(Runnable):线程已经启动,等待 CPU 调度执行。
-
运行(Running):线程正在执行。
-
阻塞(Blocked):线程因为某些原因(如等待 I/O 操作、锁等)暂时停止执行。
-
终止(Terminated):线程执行完毕或被强制终止。
5. 多线程的挑战
-
数据竞争(Data Race):
-
当多个线程同时访问共享资源时,可能会导致数据不一致。
-
例如:两个线程同时对一个变量进行写操作,结果可能不符合预期。
-
-
死锁(Deadlock):
-
多个线程互相等待对方释放资源,导致所有线程都无法继续执行。
-
-
线程安全问题:
-
如果多个线程同时修改共享数据,可能会导致程序行为异常。
-
6. 线程的示例
以下是一个简单的多线程示例(C++11 及以上):
#include<iostream>
#include<thread>
using namespace std;
//线程执行的函数
void printHello(int id)
{
cout << "Hello from thread " << id << endl;
}
int main()
{
//创建两个线程
thread t1(printHello, 1);
thread t2(printHello, 2);
//等待线程执行完毕
t1.join();
t2.join();
cout << "Main thread finished." << endl;
return 0;
}
代码说明:
-
std::thread
是 C++ 标准库中的线程类。 -
t1
和t2
是两个线程,分别执行printHello
函数。 -
join()
用于等待线程执行完毕。
输出可能:
Hello from thread 1
Hello from thread 2
Main thread finished.
(注意:线程的执行顺序是不确定的,因此输出的顺序可能会变化。)
7. 线程与单例模式的关系
在上一篇:小明的购物车(单例模式)中,getInstance()
使用了静态局部变量来实现单例模式。在 C++11 及以上标准中,静态局部变量的初始化是线程安全的。这意味着即使多个线程同时调用 getInstance()
,也只会创建一个实例。
如果没有线程安全机制,多个线程可能会同时创建多个实例,破坏单例模式的唯一性。
我们来看其它输出情况:
Hello from thread Hello from thread 21
Main thread finished.
Hello from thread Hello from thread 2
1
Main thread finished.
Hello from thread 2
Hello from thread 1
Main thread finished.
为什么会有这些情况出现?
这是因为 多线程并发访问 std::cout
导致的输出混乱。
1. 为什么输出会混乱?
原因 1:std::cout
不是线程安全的
std::cout
是 C++ 标准库中的一个全局对象,用于输出数据到控制台。当多个线程同时调用 std::cout
时,它们的输出可能会交错在一起,因为 std::cout
的内部实现并不是线程安全的。
原因 2:操作系统的线程调度
操作系统对线程的调度是非确定性的。也就是说,线程的执行顺序无法预测。例如:
-
线程 1 开始输出
"Hello from thread 1"
。 -
在线程 1 完成输出之前,线程 2 开始输出
"Hello from thread 2"
。 -
结果就是输出内容混在一起。
具体分析
Hello from thread Hello from thread 2
1
Main thread finished.
-
线程 1 开始输出
"Hello from thread 1"
,但在输出到一半时被操作系统中断。 -
线程 2 开始输出
"Hello from thread 2"
,并完成了输出。 -
线程 1 恢复执行,继续输出剩余的
"1"
。 -
最后,主线程输出
"Main thread finished."
。
2. 如何解决这个问题?
要解决这个问题,需要确保多个线程不会同时访问 std::cout
。以下是几种常见的解决方法:
方法 1:使用互斥锁(Mutex)
互斥锁可以确保同一时间只有一个线程访问共享资源(如 std::cout
)。
#include<iostream>
#include<thread>
#include<mutex>//引入互斥锁
using namespace std;
mutex mtx;//全局互斥锁
void printHello(int id)
{
lock_guard<mutex>lock(mtx);//加锁
cout << "Hello from thread" << id << endl;
//锁在lock_guard析构时自动释放
}
int main()
{
thread t1(printHello, 1);
thread t2(printHello, 2);
t1.join();
t2.join();
cout << "Main thread finished." << endl;
return 0;
}
代码说明:
-
std::mutex
是 C++ 标准库中的互斥锁。 -
std::lock_guard
是一个 RAII 风格的锁管理器,确保在作用域结束时自动释放锁。 -
通过加锁,确保同一时间只有一个线程访问
std::cout
。
方法 2:使用线程安全的输出函数
如果你需要频繁地输出日志或调试信息,可以封装一个线程安全的输出函数。
#include<iostream>
#include<thread>
#include<mutex>
#include<sstream>
using namespace std;
mutex mtx;
void safeprint(const string& message)
{
lock_guard<mutex>lock(mtx);
cout << message;
}
void printHello(int id)
{
ostringstream oss;
oss << "Hello from thread " << id << endl;
safeprint(oss.str());
}
int main()
{
thread t1(printHello, 1);
thread t2(printHello, 2);
t1.join();
t2.join();
safeprint("Main thread finished.");
cout << endl;
return 0;
}
代码说明:
-
使用
std::ostringstream
将输出内容先写入字符串流,然后一次性输出。 -
这样可以减少锁的竞争,提高性能。