多线程编程

多线程编程介绍

线程介绍

什么是线程?
线程是轻量级进程,是操作系统统一调度的最小单元。每个进程都有独立的用户地址空间,而运行在进程中的多个线程共享进程的地址空间,因此进程内线程之间的通信就会变得简单,但是线程又有独立的栈,因此我们可以写出线程安全的函数(即该函数只使用的栈空间,不使用堆空间,那么该函数就可以说是线程安全的)。
线程与进程的关系:
在这里插入图片描述
线程私有:
局部变量
函数参数
线程局部存储(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.
想要修改共享变量的线程必须:

  1. 获取std::mutex(通常是std::lock_guard)
  2. 在持有锁的情况下修改共享变量(即使该共享变量是原子变量,也必须在持有互斥锁的时候对其进行修改,才能将修改正确地发布到等待线程)
  3. 调用notify_one()或notify_all()唤醒线程()
    在std::condition_variable上等待的线程都必须:
  4. 获取std::unique_lockstd::mutex锁用来保护共享变量
  5. 执行以下操作:
    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都与一个共享状态相关联,可以利用共享状态做三件事情:

  1. make ready:promise将结果或异常存储在共享状态中,将状态标记为就绪并解除阻塞任何等待与共享状态相关的线程;
  2. release:promise放弃对共享状态的引用,如果这是最后一个引用,则共享状态将被销毁。
  3. 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/

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值