一、线程与多线程编程的基本概念
线程的定义
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程编程的优势与挑战
优势:
- 提高CPU的利用率
- 提高程序的响应速度
- 简化程序的结构
挑战:
- 数据同步问题:多个线程访问共享数据时可能产生数据不一致的问题。
- 线程安全问题:需要确保线程间的数据访问和操作是安全的。
- 线程管理问题:创建、销毁和调度线程都需要消耗资源,过多线程可能导致性能下降。
C++11中线程编程的引入
C++11标准引入了<thread>
库,为C++程序员提供了创建和管理线程的能力。使用<thread>
库,程序员可以更加容易地编写出多线程的C++程序。
二、C++线程编程基础
引入<thread>
头文件
在C++11中,要使用线程编程,首先需要包含<thread>
头文件。
#include <thread>
std::thread
类的使用
std::thread
是C++11中用于表示和管理线程的类。通过创建一个std::thread
对象并传入一个可调用的对象(如函数、函数对象、Lambda表达式等),可以启动一个新的线程。
线程的创建与启动
以下是一个简单的示例,展示如何创建并启动一个线程:
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(print_hello); // 创建并启动线程
t.join(); // 等待线程完成
return 0;
}
在这个例子中,print_hello
函数被一个新的线程t
执行。主线程通过调用t.join()
来等待新线程执行完毕。如果不调用join()
或detach()
,当main
函数结束时,新线程可能还未执行完毕,这会导致程序崩溃。
join()
方法:等待线程完成
join()
方法用于阻塞当前线程(通常是主线程),直到调用该方法的线程对象所代表的线程执行结束。在上述示例中,主线程通过调用t.join()
来等待新线程执行完毕。
detach()
方法:使线程在后台运行
detach()
方法用于分离线程,使得新线程在后台运行,而当前线程(通常是主线程)可以继续执行。一旦线程被分离,就不能再调用join()
方法,且新线程结束时会自动释放其占用的资源。
joinable()
方法:检查线程是否可连接或分离
joinable()
方法用于检查线程对象是否代表一个可连接的线程。如果线程对象是可连接的(即它代表一个活跃的线程,且该线程未被分离),则joinable()
返回true
;否则返回false
。
C++线程编程基础
一、C++标准库中的线程库
在C++11及以后的版本中,标准库引入了<thread>
头文件,它包含了std::thread
类和其他与线程相关的类和函数,使得C++程序员可以方便地进行多线程编程。
二、C++线程的基本操作
1. 创建线程
使用std::thread
类的构造函数可以创建一个线程。构造函数接受一个可调用的对象(如函数、函数对象、Lambda表达式等)作为参数,这个可调用的对象将在新的线程中执行。
示例代码:
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(print_hello); // 创建一个线程t,并执行print_hello函数
// 等待线程执行完毕
t.join();
return 0;
}
在这个例子中,我们定义了一个名为print_hello
的函数,然后在main
函数中创建了一个std::thread
对象t
,并将print_hello
函数作为参数传递给t
的构造函数。这将在新的线程中执行print_hello
函数。主线程通过调用t.join()
来等待新线程执行完毕。
2. 分离线程
使用std::thread::detach
成员函数可以将线程与创建它的线程分离,使它在后台运行。一旦线程被分离,就不能再调用join
或detach
,并且线程结束时会自动释放其占用的资源。
示例代码:
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from detached thread!\n";
}
int main() {
std::thread t(print_hello);
t.detach(); // 分离线程,使它在后台运行
// 主线程可以继续执行其他任务,不需要等待分离后的线程
std::cout << "Main thread continuing...\n";
return 0;
}
在这个例子中,我们创建了一个线程t
并执行print_hello
函数,然后立即调用t.detach()
将线程分离。主线程可以继续执行其他任务,而不需要等待分离后的线程完成。
3. 检查线程是否可连接或分离
使用std::thread::joinable
成员函数可以检查线程对象是否代表一个可连接的线程。如果线程对象是可连接的(即它代表一个活跃的线程,且该线程未被分离),则joinable
返回true
;否则返回false
。
示例代码:
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(print_hello);
if (t.joinable()) {
std::cout << "Thread is joinable, calling join...\n";
t.join();
} else {
std::cout << "Thread is not joinable\n";
}
return 0;
}
在这个例子中,我们创建了一个线程t
并执行print_hello
函数。然后检查线程是否可连接,如果可连接则调用join
等待其完成。由于我们还没有调用detach
,所以线程是可连接的。如果在线程被分离后调用joinable
,它将返回false
。
线程同步与互斥
一、线程同步与互斥的基本概念
在多线程编程中,线程同步与互斥是两个非常重要的概念。由于多个线程可能同时访问共享资源(如内存中的变量、文件、数据库等),如果没有适当的同步机制,就可能导致数据不一致、脏读、脏写等问题。线程同步与互斥就是为了解决这些问题而提出的。
线程同步
线程同步是指多个线程按照某种顺序或规则来访问共享资源,以确保数据的完整性和一致性。线程同步通常通过同步原语(如互斥锁、条件变量、信号量等)来实现。
线程互斥
线程互斥是指同一时刻只允许一个线程访问共享资源,其他线程必须等待当前线程释放资源后才能访问。线程互斥是线程同步的一种特殊情况,它通过互斥锁(如std::mutex
)来实现。
二、C++中的互斥锁
C++11标准库提供了std::mutex
类来实现互斥锁。std::mutex
类提供了对共享资源的互斥访问,确保同一时刻只有一个线程可以访问共享资源。
1. std::mutex
的基本使用
示例代码:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
int counter = 0; // 共享资源
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
在这个例子中,我们定义了一个全局的互斥锁mtx
和一个共享资源counter
。两个线程t1
和t2
都调用increment
函数来增加counter
的值。在increment
函数中,我们使用std::lock_guard
来自动管理互斥锁的加锁和解锁。这样可以确保在修改counter
时,只有一个线程能够访问它。
2. std::lock_guard
的使用
std::lock_guard
是一个简单的互斥锁封装类,它会在构造时自动加锁,在析构时自动解锁。这样可以确保在lock_guard
对象存在期间,互斥锁始终处于锁定状态。使用lock_guard
可以避免因忘记解锁而导致的问题。
三、C++中的条件变量
除了互斥锁之外,C++还提供了条件变量(std::condition_variable
)来实现更复杂的线程同步。条件变量允许线程在满足某个条件之前等待,当条件满足时,它可以唤醒一个或多个等待的线程。
1. std::condition_variable
的基本使用
示例代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false; // 共享条件
void print_id(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{return ready;}); // 等待条件满足
std::cout << "thread " << id << '\n';
}
void go() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟延迟
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 设置条件为真
cv.notify_all(); // 唤醒所有等待的线程
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_id, i);
}
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto &th : threads) {
th.join();
}
return 0;
}
线程高级话题
一、线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在完成任务时终止了,线程池会控制其他线程来补足该线程。
1. 线程池的优势
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2. C++中的线程池实现(简化版)
由于C++标准库并没有直接提供线程池的实现,我们通常需要自己实现或者使用第三方库。下面是一个简化版的线程池实现示例:
示例代码:
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
void WorkerThread() {
while (!stop) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this]{ return !tasks.empty() || stop; });
if (!tasks.empty()) {
task = std::move(tasks.front());
tasks.pop();
}
}
if (task) {
task();
}
}
}
public:
ThreadPool(size_t threads) : stop(false) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back(&ThreadPool::WorkerThread, this);
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for (std::thread &worker : workers) {
worker.join();
}
}
template<class F>
void enqueue(F f) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
// don't allow enqueueing after stopping the pool
if (stop) {
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks.emplace(std::move(f));
}
condition.notify_one();
}
};
// 使用示例
int main() {
ThreadPool pool(4); // 创建一个包含4个线程的线程池
// 提交任务到线程池
for (int i = 0; i < 10; ++i) {
pool.enqueue([&](){
std::cout << "Task " << i << " is running on thread " << std::this_thread::get_id() << std::endl;
});
}
// 等待所有任务完成(在这里,主线程可以执行其他任务或等待线程池销毁)
// ...
return 0;
}
二、线程局部存储(Thread-Local Storage, TLS)
线程局部存储允许你创建只能由单个线程访问的全局变量。这对于每个线程需要自己的副本,但又不想使用线程间通信的情况非常有用。
1. C++中的线程局部存储
C++11引入了thread_local
关键字来实现线程局部存储。
示例代码:
#include <iostream>
#include <thread>
thread_local int tls_counter = 0; // 线程局部存储的变量
void print_and_increment() {
tls_counter++;
std::cout << "Thread " << std::this_thread::get_id() << " tls_counter: " << tls_counter << std::endl;
}
并发编程的高级话题
一、死锁与避免死锁
死锁 是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。
1. 死锁产生的条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
2. 避免死锁的策略
- 预防策略:通过破坏死锁产生的四个必要条件中的一个或多个来预防死锁。
- 避免策略:在资源分配过程中,使用某种方法去防止系统进入不安全状态,从而避免死锁。
- 检测与恢复策略:允许死锁的发生,但是通过系统的检测机构及时检测出死锁的发生,然后采取某种措施将进程从死锁状态中解脱出来。
3. 示例代码(使用锁避免死锁)
在C++中,可以使用std::mutex
或std::lock_guard
等同步原语来避免死锁。下面是一个简单的示例,展示了如何使用std::lock
来避免死锁,它使用了一种称为锁顺序的技术。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtxA, mtxB;
void print_block_A_then_B() {
std::lock(mtxA, mtxB); // 尝试同时锁定两个互斥量
std::lock_guard<std::mutex> lockA(mtxA, std::adopt_lock), lockB(mtxB, std::adopt_lock);
// ... 执行临界区操作 ...
std::cout << "Thread " << std::this_thread::get_id() << " has both A and B\n";
// ... 临界区结束 ...
}
void print_block_B_then_A() {
std::lock(mtxB, mtxA); // 尝试以相反的顺序锁定两个互斥量
std::lock_guard<std::mutex> lockB(mtxB, std::adopt_lock), lockA(mtxA, std::adopt_lock);
// ... 执行临界区操作 ...
std::cout << "Thread " << std::this_thread::get_id() << " has both B and A\n";
// ... 临界区结束 ...
}
int main() {
std::thread th1(print_block_A_then_B);
std::thread th2(print_block_B_then_A);
th1.join();
th2.join();
return 0;
}
在上面的示例中,虽然两个线程试图以不同的顺序锁定互斥量,但std::lock
函数会确保以相同的顺序锁定它们(如果它们没有被其他线程持有),因此避免了死锁。
二、条件变量(Condition Variables)
条件变量通常用于实现生产者-消费者模型,其中一个或多个线程(生产者)将数据添加到队列中,而一个或多个线程(消费者)从队列中移除并处理这些数据。
示例代码(使用条件变量)
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cond_var;
bool data_ready = false; // 标志位,表示数据是否准备好
bool stop_flag = false; // 停止标志位
void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
data_queue.push(i);
data_ready = true; // 通知消费者数据已准备好
cond_var.notify_one(); // 唤醒等待的线程
lock.unlock(); // 注意:在 C++11 中,std::unique_lock 在析构时会自动解锁
}
// 通知所有等待的线程,数据已全部生产完成
{
std::lock_guard<std::mutex> lock(mtx);
stop_flag = true;
}
cond_var.notify_all();
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cond_var.wait(lock, []{ return data_ready || stop_flag; }); // 等待数据准备好或停止信号
if (stop_flag && data_queue.empty()) {
break; // 所有数据已处理,且收到停止信号,退出循环
}
int value = data_queue.front();
data_queue.pop();
data_ready = false; // 重置数据准备标志位
// 处理数据...
std::cout << "Consumed: " << value << std::endl;
lock.unlock(); // 注意:虽然在这里调用了 unlock,但在 C++11 中,std::unique_lock 会在析构时自动解锁
}
}
int main() {
std::thread producer_thread(producer);
std::thread consumer_thread(consumer);
producer_thread.join();
consumer_thread.join();
return 0;
}
在这个示例中,我们使用了std::condition_variable
来实现生产者-消费者模型。生产者线程在添加数据到队列后会设置data_ready
标志,并通过notify_one
唤醒一个等待的消费者线程。消费者线程在检查到数据准备好或收到停止信号后,会从队列中取出数据并处理。注意,我们在等待条件变量时使用了std::unique_lock
来确保互斥量在条件变量等待期间被锁定,并在条件满足后自动解锁。此外,我们还添加了一个stop_flag
来通知消费者线程何时停止处理数据。