1. 什么是线程?
在计算机科学中,线程是进程中的一个执行流,是轻量级的进程。一个标准的进程内有一个或多个线程,这些线程共享该进程的内存空间、文件句柄等资源。
1.1 为什么需要使用线程?
使用线程可以使得程序代码执行更加高效,在多核处理器的情况下可以使得多个线程同时执行从而避免资源的浪费。此外,使用线程还允许程序完成一些异步操作,如同步输入和输出,使得程序更加稳定响应。
1.2 线程的基本操作
线程的基本操作包括创建线程、启动线程、等待线程结束和销毁线程等。在C++中,我们可以使用标准库中的thread头文件来进行线程编程。下面是一个简单的例子:
#include <iostream>
#include <thread>
void my_func()
{
std::cout << "hello from thread" << std::endl;
}
int main()
{
std::thread t1(my_func);
t1.join(); //等待线程结束
return 0;
}
上面的代码中,我们首先使用std::thread类创建了一个新的线程t1,然后调用t1.join()函数等待线程结束。在my_func()函数中,我们简单地输出一条消息表示该线程的执行。
2. 线程的同步与互斥
在实际的应用中,多个线程经常需要对同一资源进行操作,这时候我们就需要使用同步和互斥来保证程序的正确性。
2.1 同步
同步是指多个线程在执行时遵循某种规定,以防止线程之间出现冲突。同步机制的目的是为了尽量避免死锁和资源争用等问题。
2.2 互斥
互斥是指多个线程在执行时需要互相协调,只有当某个线程执行完成后,才能让其他线程访问其资源。互斥信号量(Mutual Exclusion Semaphore)主要是为了协调程序中的线程资源而出现的,其目的是为了避免进程间的竞态条件以及其他形式的线程之间的矛盾。
2.3 实例:生产者-消费者问题
生产者-消费者问题是指有一个容器,生产者向容器中添加元素,消费者从容器中获取元素,容器的大小有限制。我们可以使用互斥锁和条件变量来进行同步控制。
下面是一个生产者-消费者实例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex g_mutex;
std::condition_variable g_cond;
std::queue<int> g_queue; //容器
bool g_done = false;
//生产者线程函数
void producer_func()
{
for (int i = 0; i < 10; ++i)
{
std::unique_lock<std::mutex> lock(g_mutex); //加锁
g_queue.push(i); //向容器中添加元素
g_cond.notify_one(); //通知消费者
}
g_done = true;
g_cond.notify_all();
}
//消费者线程函数
void consumer_func()
{
while (!g_done)
{
std::unique_lock<std::mutex> lock(g_mutex); //加锁
//容器为空时等待
g_cond.wait(lock, []{ return !g_queue.empty(); });
//从容器中获取元素
int value = g_queue.front();
g_queue.pop();
std::cout << "value = " << value << std::endl;
}
}
int main()
{
std::thread t1(producer_func);
std::thread t2(consumer_func);
t1.join();
t2.join();
return 0;
}
上面的代码中,我们首先定义了一个互斥锁和一个条件变量,然后定义了一个全局的队列作为容器。在producer_func()函数中,我们向容器中添加元素,并且使用条件变量通知消费者线程。在consumer_func()函数中,我们从容器中获取元素,如果容器为空则等待,等待期间会自动解锁互斥锁以允许其他线程访问资源,当容器非空时再次加锁处理。当生产者线程结束时,设置g_done为true并唤醒所有等待中的线程,消费者线程在下次等待时发现g_done为true就会结束等待。
3. 线程池的实现
在实际的应用中,线程的创建和销毁带来的系统开销很大,常常会导致性能下降。线程池的作用就是为了避免线程重复创建和销毁的开销,提高线程的性能和可扩展性。
3.1 线程池的基本实现
线程池的基本实现是由多个线程共享一个任务队列,当有任务到来时,调度线程会将任务添加到队列中,并通知等待的工作线程去执行任务。线程池中工作线程数量通常会根据系统负载动态改变,具有很好的伸缩性。下面是一个简单的线程池实现:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
template <typename T>
class thread_pool
{
public:
explicit thread_pool(size_t thread_count = std::thread::hardware_concurrency())
: m_done(false)
{
try
{
for (size_t i = 0; i < thread_count; ++i)
{
m_threads.emplace_back([this]
{
for (;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->m_mutex);
this->condition.wait(lock, [this]{ return this->m_done || !this->m_tasks.empty(); });
if (this->m_done && this->m_tasks.empty())
return;
task = std::move(this->m_tasks.front());
this->m_tasks.pop();
}
task();
}
});
}
}
catch (...)
{
m_done = true;
throw;
}
}
~thread_pool()
{
{
std::unique_lock<std::mutex> lock(m_mutex);
m_done = true;
}
condition.notify_all();
for (auto &thread : m_threads)
thread.join();
}
template <typename F>
void submit(F &&f)
{
{
std::unique_lock<std::mutex> lock(m_mutex);
m_tasks.emplace(std::forward<F>(f));
}
condition.notify_one();
}
private:
std::vector<std::thread> m_threads;
std::queue<std::function<void()>> m_tasks;
std::mutex m_mutex;
std::condition_variable condition;
bool m_done;
};
//测试任务
void do_work(int i)
{
std::cout << "work #" << i << std::endl;
}
int main()
{
thread_pool<std::function<void()>> pool;
for (int i = 0; i < 10; ++i)
pool.submit(std::bind(do_work, i));
return 0;
}
上面的代码中,我们定义了一个thread_pool类,它的的构造函数会为线程池中创建指定数量的工作线程,使用lambda表达式作为线程函数达到函数名不必须指定的目的,每个工作线程都会从任务队列中取出任务执行,如果队列为空则等待。submit()函数用于提交任务,将任务添加到队列中并唤醒等待的工作线程执行。在main()函数中,我们向线程池中提交了10个任务,每个任务都是一个简单的do_work函数,do_work函数用来输出当前任务的编号。
3.2 线程池的优化
上面的实现过程中,任务队列使用了一个简单的STL队列,但是这样会导致在多个线程同时对队列进行操作时出现竞争,可能会引起死锁等问题。我们可以使用无锁队列(deque)来避免这个问题。此外,在实际的应用中,线程池中的工作线程数量通常需要动态调节,可以根据系统的负载情况自动增加或减少工作线程的数量。
4. 总结
线程编程是现代程序设计不可缺少的一部分,使用线程可以提高程序的运行效率和可扩展性。在进行线程编程时,需要注意线程的同步和互斥机制,合理使用锁和条件变量可以避免程序出现竞争和死锁等问题。在开发中可以使用C++标准库中提供的thread头文件来进行线程编程,也可以使用第三方的线程库(如boost)进行开发。