多线程编程介绍
线程介绍
什么是线程?
线程是轻量级进程,是操作系统统一调度的最小单元。每个进程都有独立的用户地址空间,而运行在进程中的多个线程共享进程的地址空间,因此进程内线程之间的通信就会变得简单,但是线程又有独立的栈,因此我们可以写出线程安全的函数(即该函数只使用的栈空间,不使用堆空间,那么该函数就可以说是线程安全的)。
线程与进程的关系:
线程私有:
局部变量
函数参数
线程局部存储(Thread Local Storage,TLS数据)
线程共享:
全局变量
堆上的数据
函数里的静态变量
程序代码
打开文件
为什么要用到多线程?
在编程中,为了提高代码的运行效率,希望代码能够并行运行,因此就用到了多线程。另外,多进程也可以起到并行的作用,但是进程间通信往往会比较重量级,比如进程的创建/销毁时,资源消耗较线程大;且多进程通信不管使用哪种方式(管道、套接字、信号、共享队列、共享内存等)都实现起来比较复杂,且有的通信速度比较慢。因此需要同主机进程内通信的场景下,多线程变得很有必要。
由于多线程缺少操作系统的保护,因此在多线程进行共享数据及通信时,就需要程序员对共享数据做保护,防止竞争发生,而让代码以预想的方式运行,这就用到了锁。(后面介绍)
线程的创建与运行
c++ 11中可以通过thread库对多线程进行管理,使用时需要包含#include头文件,同时需要链接-pthread库,并指定c++版本为c++11,如:
g++ --std=c++11 -pthread main.cpp
c++线程创建可以支持如下多种方式:
- 普通函数
#include <iostream>
#include <thread>
void fun() {
std::cout << "hello multithread" << std::endl;
}
int main(int argc, const char * argv[]) {
// insert code here...
std::thread t(fun);
t.join();
return 0;
}
构造thread时传入了一个函数,这里传入可调用类型就可以。
可调用对象:
#include <iostream>
#include <thread>
class MultiTask {
public:
MultiTask(int count) : count_(count) {}
~MultiTask() {}
void operator()() {
std::cout << "this is task: " << count_ << std::endl;
}
private:
int count_;
};
int main(int argc, const char * argv[]) {
// insert code here...
for (int i = 0; i < 5; i++) {
std::thread t{MultiTask{i}};
t.join();
}
return 0;
}
这里Multitask重载了()运算符,初始化thread传入了一个可调用对象。
lambda表达式:
#include <iostream>
#include <thread>
int main(int argc, const char * argv[]) {
// insert code here...
int a = 2;
std::thread t([a](){
std::cout << "value is " << a << std::endl;
});
t.join();
return 0;
}
还可以使用lambda表达式初始化线程,lambda表达式是一个匿名函数。
线程传递参数
在创建线程时,只需要将参数作为入参传递给线程函数;需要注意的是默认情况下,会将参数拷贝到线程空间,即使该参数是一个引用,如果想要在线程中改变该参数的值,则需要通过调用std::ref才能将参数的引用传递给线程。
#include <iostream>
#include <thread>
void fun(int &n) {
n++;
std::cout << "value is " << n << std::endl;
}
int main(int argc, const char * argv[]) {
// insert code here...
int n = 2;
std::thread t(fun, std::ref(n));
t.join();
std::cout << n << std::endl;
return 0;
}
线程的退出
在主线程退出前,一定要指定线程的退出方式,以便操作系统进行资源回收,线程退出有两种方式:
- detach方式
detach代表进程分离,即调用detach后,线程独立运行,不再受管制与约束,即使主线程退出,子线程不受影响,会在后台运行,直到该进程执行完毕。调用detach后,thread对象不再表示任何线程,joinable()==false,即使该线程还在运行没有退出。 - join方式
调用join会阻塞当前代码,只有线程运行结束,当前代码才会继续往下执行。join()只能调用一次,调用后joinable()==false;另外,如果某个线程执行完毕,没有调用join()函数,即使该线程已退出,它的joinable()还是等于true,线程资源没有被回收。当线程以terminal()的方式结束,即使没有调用join(),该线程的joinable()==false
线程所有权转移
std::thread不支持复制,但是支持所有权转移,可以通过std::move来转移线程的所有权,来灵活决定什么时候join()或detach()
std::thread t1(fun);
std::thread t2(std::move(t1));
以上将t1线程的所有权转移给t2,所有权转移后,在t1上调用join()或detach()就会出现异常,thread可以作为函数返回值或者参数传递给函数,能够更方便得管理线程。
线程管理(RAII)
当线程以join的方式退出时,需要在主线程合适的位置调用join()函数,如果调用join()前出现了异常,thread被销毁,线程就会被异常终结,无法正常调用到join,或者由于某些原因,比如线程访问了局部变量,就要保证线程一定要在函数退出前完成。
有一种比较好的方法就是使用资源获取即初始化(RAII,Resource Acquisition Is Initialization)
#include <iostream>
#include <thread>
class ThreadGaurd {
public:
ThreadGaurd(std::thread& t) : th_(t) {}
~ThreadGaurd() {
if (th_.joinable()) {
th_.join();
}
}
ThreadGaurd(const ThreadGaurd&) = delete;
ThreadGaurd& operator=(const ThreadGaurd&) = delete;
private:
std::thread &th_;
};
void fun();
int main(int argc, const char * argv[]) {
// insert code here...
std::thread t([] {
std::cout << "hello, multithread!" << std::endl;
});
ThreadGaurd tg(t);
return 0;
}
线程同步
在多线程场景中,由于并发线程并发执行,会出现同时读写一个临界区的情况,此时如果不加锁的话,代码可能会以非预期的方式运行。C++ 11线程同步可以通过互斥锁(std::mutex)和条件变量(std::condition_variable)配合实现多线程同步
锁
c++ 11提供了四种互斥锁:
- std::mutex:互斥锁,是一个同步原语,可用于保护共享数据不被多个线程同时访问。当有一个线程持有该锁时,其他线程尝试获取锁,都会被阻塞。通过lock()函数获取锁,通过unlock()函数释放锁。
int g_num = 0; // 全局变量
std::mutex mtx;
void fun() {
for (int i = 0; i < 5; i++) {
mtx.lock();
g_num++;
mtx.unlock();
}
}
- std::timed_mutex:带超时的独占互斥锁,可以通过try_lock_for() 或 try_lock_until().方法指定一个超时时间。
- std::recursive_mutex:递归互斥锁,提供独占的递归语义,保护共享数据不被多个线程同时访问。在线程持有递归锁的一段时间内,该线程可能会额外调用lock或try_lock,当在一段时间内达到最大锁定次数时,会抛出std::system_error异常(最大锁定次数未定义),用于解决同一线程需要多次获取互斥锁而发生死锁的情况。
- std::recursive_timed_mutex:带超时的递归互斥锁
std::lock_guard:c++ 11提供的一个模板类,可以简化lock()/unlock()的写法,也更安全,使用了RAII的设计思想。在构造时获取锁,析构时自动释放锁,避免因为忘记调用unlock()而造成线程死锁。
int g_num = 0; // 全局变量
std::mutex mtx;
void fun() {
for (int i = 0; i < 5; i++) {
std::lock_guard<std::mutex> lk(mtx);
g_num++;
}
}
std::unique_lock:是c++标准库中互斥量的封装类,对互斥量的控制更加灵活,与std::lock_guard类似,也是用来管理lock()和unlock()的行为。可以在构造时加锁,析构时解锁,也可以手动控制加锁解锁的时机,可以与条件变量配合使用,支持条件等待,也支持移动语义。
构造时加锁,析构时解锁例子:
std::mutex mtx;
int g_num = 0;
void fun() {
std::unique_lock<std::mutex> lk(mtx); // 构造时加锁
g_num++;
} // 析构时解锁
手动控制加锁、解锁时机例子:
std::mutex mtx;
int g_num = 0;
void fun() {
std::unique_lock<std::mutex> lk(mtx, std::defer_lock);
// 执行一些非互斥操作
lk.lock(); // 加锁
// 临界区
lk.unlock();// 解锁
}
与条件变量配合使用例子:
std::mutex mtx;
std::condition_variable cv;
std::queue<int> msg_queue;
int g_num = 0;
void fun() {
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, [this](){
return !msg_queue.empty();
});
// 执行一些操作
}
条件变量
std::condition_variable是一个与std::mutex一起使用的同步原语,用于阻塞一个或多个线程,直到某个线程修改了共享变量并通知给std::condition_variable.
想要修改共享变量的线程必须:
- 获取std::mutex(通常是std::lock_guard)
- 在持有锁的情况下修改共享变量(即使该共享变量是原子变量,也必须在持有互斥锁的时候对其进行修改,才能将修改正确地发布到等待线程)
- 调用notify_one()或notify_all()唤醒线程()
在std::condition_variable上等待的线程都必须: - 获取std::unique_lockstd::mutex锁用来保护共享变量
- 执行以下操作:
2.1 检查条件,是否已经更新或通知
2.2 在std::condition_variable上调用wait()或wait_for()或wait_until()(std::condition_variable原子地释放锁并暂停该线程,直到条件变量得到通知或超时到期或者被唤醒,然后返回到之前原子地获取互斥锁)
2.3 检查条件,如果不满足,则继续等待
使用条件变量实现一个线程安全队列
template <typename T> class MultiThreadQueue {
public:
MultiThreadQueue() {}
MultiThreadQueue(const MultiThreadQueue& other) {
std::lock_guard<std::mutex> lk(mtx_);
queue_ = other.queue_;
}
~MultiThreadQueue() {
while (!queue_.empty()) {
queue_.pop();
}
}
void push(const T& value) {
std::lock_guard<std::mutex> lk(mtx_);
queue_.push(value);
cv_.notify_one();
}
void wait_and_pop(T& value) {
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [this]() {
return !queue_.empty();
});
value = queue_.front();
queue_.pop();
}
std::shared_ptr<T> wait_and_pop() {
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [this]() {
return !queue_.empty();
});
std::shared_ptr<T> res = std::make_shared<T>(queue_.front());
queue_.pop();
return res;
}
bool try_pop(T& value) {
std::lock_guard<std::mutex> lk(mtx_);
if (queue_.empty()) {
return false;
}
value = queue_.front();
queue_.pop();
return true;
}
std::shared_ptr<T> try_pop() {
std::lock_guard<std::mutex> lk(mtx_);
if (queue_.empty()) {
return std::shared_ptr<T>();
}
std::shared_ptr<T> res(std::make_shared<T>(queue_.front()));
queue_.pop();
return res;
}
bool empty() {
std::lock_guard<std::mutex> lk(mtx_);
return queue_.empty();
}
private:
std::mutex mtx_;
std::condition_variable cv_;
std::queue<T> queue_;
};
实现一个线程池
在实际应用中,我们需要多个任务并发进行,如果为每个任务都开一个线程进行执行,当任务数量比较多时,会对系统资源造成了浪费,此时我们通过实现一个线程池,在线程池里的空闲线程就可以去任务队列中拿取任务进行执行,这样就能避免线程的频繁创建和释放 ,并且提高了线程的利用率,我们甚至还可以根据需要对线程池进行监控、调优。
线程池的实现原理:
线程池用来管理线程,即负责线程的创建与退出,并不断从任务队列中调取任务执行。包含一个线程队列、任务队列和一个任务提交接口。
普通线程池
接口设计:
#include <atomic>
#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
using Task = std::function<void()>;
class ThreadPool {
public:
ThreadPool(int threadNum=3) : thread_num_(threadNum) {
running_ = true;
startThreads();
}
~ThreadPool() {
stopThreads();
}
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
void runTask(Task task);
private:
void startThreads();
void stopThreads();
int thread_num_;
std::vector<std::thread> threads_;
std::mutex mtx_;
std::condition_variable cv_;
std::queue<Task> tasks_;
std::atomic_bool running_{false};
};
实现:
void ThreadPool::startThreads() {
for (int i = 0; i < thread_num_; i++) {
std::thread t([&]() {
// 设置线程名
std::string name = "MyThread" + std::to_string(i);
pthread_setname_np(pthread_self(), name.c_str());
// 运行线程,并从任务队列中调配任务
while (running_) {
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [this]() {
return !tasks_.empty() || !running_;
});
if (!running_) {
return;
}
Task task = tasks_.front();
tasks_.pop();
// 执行任务
task();
}
});
threads_.emplace_back(std::move(t));
}
}
void ThreadPool::stopThreads() {
running_ = false;
cv_.notify_all();
for (int i = 0; i < thread_num_; i++) {
if (threads_[i].joinable()) {
threads_[i].join();
}
}
}
void ThreadPool::runTask(Task task) {
std::lock_guard<std::mutex> lk(mtx_);
tasks_.push(task);
cv_.notify_one();
}
可伸缩线程池
上面线程池存在的问题:线程数量固定,当任务数量过多或任务比较耗时时,线程消费能力不足,导致任务队列膨胀,占用过多内存,当任务数量少且耗时短时,还会分配固定数量的线程,耗费过多资源,不够灵活,因此,我们可以根据更加灵活地实现可伸缩线程池。
该线程池的实现可以指定线程池内最大线程数,具体使用线程可以根据任务队列中的任务数量来定,当任务队列中积累20条以上任务,就新开一个线程,相反,如果任务队列在3min内没有任务,则结束一个线程。
接口设计如下:
using Task = std::function<void()>;
class ThreadPool {
public:
ThreadPool(const std::string& name="TheadPool", uint32_t maxThreadNum=2);
~ThreadPool();
ThreadPool(const ThreadPool& tp) = delete;
ThreadPool(ThreadPool&& tp) = delete;
ThreadPool& operator=(const ThreadPool& tp) = delete;
ThreadPool& operator=(ThreadPool&& tp) = delete;
void stopAll();
void setName(const std::string& name) {
name_ = name;
}
std::string getName() const {
return name_;
}
uint32_t getThreadNum() {
return work_thread_num_.load(std::memory_order_acquire);
}
uint32_t getTaskNum() {
std::lock_guard<std::mutex> lk(task_que_mtx_);
return task_que_.size();
}
void runTask(Task task);
private:
void newThread();
Task take();
void add_exited_thread(std::thread::id id);
void clear_exited_threads();
void add_new_thread(std::thread::id &&id, std::thread && t);
void clear_all_threads();
std::string name_;
std::atomic_uint32_t work_thread_num_; // 当前工作的线程数
uint32_t max_thread_num_; // 最大线程数
std::atomic_bool running_; // 线程池是否运行
std::mutex task_que_mtx_;
std::queue<Task> task_que_;
std::condition_variable wakeup_cv_;
std::mutex task_mtx_;
std::unordered_map<std::thread::id, std::thread> threads_map_;
std::list<std::thread::id> exit_threads_;
std::mutex exit_thread_mtx_;
};
实现:
ThreadPool::ThreadPool(const std::string& name, uint32_t maxThreadNum)
: name_(name), work_thread_num_(0), max_thread_num_(maxThreadNum), running_(true) {
}
ThreadPool::~ThreadPool() {
stopAll();
clear_all_threads();
}
void ThreadPool::stopAll() {
running_.store(false, std::memory_order_release);
wakeup_cv_.notify_all();
}
void ThreadPool::runTask(Task task) {
uint32_t task_num = 0;
{
std::lock_guard<std::mutex> lk(task_que_mtx_);
if (!running_) {
std::cout << "thread pool " << name_ << " is already stopped" << std::endl;
return;
}
task_que_.emplace(std::move(task));
task_num = task_que_.size();
}
if (work_thread_num_ == 0 || (task_num > 200 && work_thread_num_ < max_thread_num_)) {
newThread();
}
wakeup_cv_.notify_one();
}
Task ThreadPool::take() {
// 如果3min未取到任务或线程池退出,则线程退出
std::unique_lock<std::mutex> lk(task_que_mtx_);
wakeup_cv_.wait_for(lk, std::chrono::seconds(180), [&](){
return !running_.load(std::memory_order_acquire) || !task_que_.empty();
});
Task task;
if (!running_.load(std::memory_order_acquire)) {
std::cout << "thread is not running." << std::endl;
return task;
}
if (!task_que_.empty()) {
task = task_que_.front();
task_que_.pop();
wakeup_cv_.notify_all();
}
return task;
}
void ThreadPool::newThread() {
clear_exited_threads();
++work_thread_num_;
std::thread t([&](){
std::string tName = name_ + std::to_string(work_thread_num_);
if (tName.length() > 15) {
tName = tName.substr(tName.length() - 15, 15);
}
pthread_setname_np(pthread_self(), tName.c_str());
std::cout << "start a thread, name: " << tName << std::endl;
while(running_.load(std::memory_order_acquire)) {
Task task(take());
if (task) {
task();
} else {
break;
}
}
--work_thread_num_;
add_exited_thread(std::this_thread::get_id());
std::cout << "stop a thread, name: " << tName << std::endl;
});
add_new_thread(t.get_id(), std::move(t));
}
void ThreadPool::add_exited_thread(std::thread::id id) {
std::lock_guard<std::mutex> lk(exit_thread_mtx_);
exit_threads_.push_back(id);
}
void ThreadPool::clear_exited_threads() {
std::lock_guard<std::mutex> lk(exit_thread_mtx_);
for (std::list<std::thread::id>::iterator it = exit_threads_.begin(); it != exit_threads_.end();) {
auto threads_it = threads_map_.find(*it);
if (threads_it != threads_map_.end()) {
if (threads_it->second.joinable()) {
threads_it->second.join();
threads_map_.erase(*it);
}
}
exit_threads_.erase(it++);
}
}
void ThreadPool::add_new_thread(std::thread::id &&id, std::thread && t) {
threads_map_.insert(std::pair<std::thread::id, std::thread>(std::move(id), std::move(t)));
}
void ThreadPool::clear_all_threads(){
for (auto it = threads_map_.begin(); it != threads_map_.end(); ) {
if (it->second.joinable()) {
it->second.join();
}
threads_map_.erase(it++);
}
}
c++11异步编程
std::aync:创建一个异步任务
template< class F, class… Args >
async( std::launch policy, F&& f, Args&&… args );
函数模板异步运行f(可能在单独的线程中运行,也可能是线程池的一部分)并返回保存该函数的调用结果std::future
与直接起线程不同的是,直接起线程无法拿到线程运行的结果。
policy可以是std::launch::async或std::launch::deferred
std::async实际上封装了std::promise、std::packaged_task、std::thread和std::futrue,该接口提供给了我们异步编程的高级封装,我们无需关注太多细节。
std::package_task
该模板类将各种可调用对象(包括函数、lambda表达式,bind语句等)封装了起来,方便作为线程入口函数来调用,它的返回值或异常抛出可以存储在共享状态中,由std::futrue对象访问。 std::promise能够在某个线程中给它赋值,然后可以在其他线程中把它取出来。
std::promise
基本模板类,非void特化用于线程间对象的通信,void特化用于传达无状态事件。
类模板std::promise提供了一种存储值或者异常的功能,可以通过对象创建的std::futrue异步获取该值或者对象,需要注意的是std::promise对象只能使用一次。
每个promise都与一个共享状态相关联,可以利用共享状态做三件事情:
- make ready:promise将结果或异常存储在共享状态中,将状态标记为就绪并解除阻塞任何等待与共享状态相关的线程;
- release:promise放弃对共享状态的引用,如果这是最后一个引用,则共享状态将被销毁。
- abandon:承诺存储类型为std::future_error且错误代码为std::futrue_errc::broken_promise的异常,使共享状态为ready并且释放它。
promise在共享状态中存储值的操作与在获取共享状态上等待的函数(std::futrue::get)必须是同步的,否则,对同一共享状态的并发访问可能会产生冲突,例如std::shared_future::get的多个调用者必须全部为只读或者提供外部同步。
std::future
提供了一种访问异步操作结果的机制。异步操作的创建者可以使用get、wait、wait_for等方法从std::futrue中提取异步操作返回值,如果异步操作还未完成,那么这些接口可能会阻塞。
多线程调试
命令
- 查看线程信息
info thread [id],不指定id则查看所有线程信息 - 切换线程
thread [id] - 在多个线程执行指令
thread apply all bt - 锁定当前线程,暂停其他线程
set scheduler-locking on
如果只想n或s单步调试锁定线程时:set scheduler-locking step - 取消锁定
set scheduler-locking off - 生成core文件
generate-core-file
示例程序
调试
c++并发编程实战翻译:https://nj.gitbooks.io/c/content/