C++并发编程 - 线程管理

线程管理

  多线程编程是开发中经常用的技术,多数情况下,我们只是知道怎么启线程、回收线程以及常规的一些用法,对于其具体技术细节以及还有哪些巧妙的用法并未挖掘。

  本篇参考《C++并发编程实战》及其他优秀的博客,做一次对C++的线程管理的梳理,方便后续使用查阅。

并发编程的方法

  计算机领域的并发指的是在单个系统里同时执行多个独立的任务, 而非顺序的进行一些活动。通常并发方式有两种: 多进程和多线程。

多进程并发

  将场景任务以两个或以上进程实现,这些独立的进程相互通信,共同完成任务,称之为多进程并发。

  由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:

  • 使用信号、套接字,还是文件、管道等方式进行进程间通信,存在使用麻烦或者通信速度较慢等问题。
  • 在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

多线程并发

  在同一个进程中执行多个线程,称之为多线程并发。

  线程可看做是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。

  相较于多进程间通信,多线程可利用共享的地址设计线程间的通信,这就使多线程通信更简单。另一方面,共享地址的滥用,也会导致程序异常。多线程并发一直值得程序员谨慎和敬畏,因此能不使用尽量不用。

线程管理基础

启动线程
  线程在 std::thread 对象创建(为线程指定任务)时启动,在创建对象时会传入任务函数作为参数。此任务函数有两种方式:函数指针和lambda表达式:

// 函数指针形式
void thread1()
{
    LOGD("--> This is thread1.\n");
}
std::thread th1(thread1);

// lambda形式
std::thread th2([]() {
    LOGD("-> This is thread2.\n");
});

等待线程完成
 假设进程内部的线程未使用join()或deatch(),会导致std::thread对象在销毁时,程序异常终止(无论全局还是局部线程)。

 因此,每个线程在使用时,都要确定回收方式。若线程在局部函数启动时,要注意线程在局部销毁前回收。

std::thread 使用 join() 阻塞等待线程结束。调用 join() 的行为,还清理了线程相关的存储部分, 这样 std::thread 对象将不再与已经完成的线程有任何关联。
  这意味着, 只能对一个线程使用一次 join() 一旦已经使用过 join()std::thread 对象就不能再次加入了, 当对其使用joinable()时, 将返回否 (false)

std::thread th2([]() {
    LOGD("-> This is thread2.\n");
});
th2.join();

特殊情况下等待
  通过上述分析,好的程序员都应该在启动线程时,考虑好在何时回收线程(即使用join()或detach()的位置)。

  借鉴《C++并发编程》的一种做法: 使用“资源获取即初始化方式”(RAII, Resource Acquisition Is Initialization), 并且提供一个类, 在析构函数中使用join(), 如同下面清单中的代码:

class thread_guard
{
    std::thread& t;
 public:
    explicit thread_guard(std::thread& t_):
    t(t_)
    {}
    
    ~thread_guard()
    {
        if(t.joinable()) // 1
        {
            t.join(); // 2
        }
    } 
    
    thread_guard(thread_guard const&)=delete; // 3
    thread_guard& operator=(thread_guard const&)=delete;
};

void f()
{
    int some_local_state=0;
    std::thread t([](){
        LOGD("-> This is thread.\n");
    });
    thread_guard g(t);
    do_something_in_current_thread();
} // 4

  当线程执行到④处时, 局部对象就要被逆序销毁了。 因此, thread_guard对象g是第一个被销毁的, 这时线程在析构函数中被加入②到原始线程中。 即使do_something_in_current_thread抛出一个异常, 这个销毁依旧会发生。


后台运行
  通过调用detach()会使程序后台独立运行,即不会再与主线程直接交互。
  如果线程分离,主线程就失去了对分离线程的控制权,即无法再捕获分离线程,自然也无法再join此线程。即使主线程结束,分离线程可能还在运行,此时由C++运行时库负责清理与子线程相关的资源。
  分离线程一般用于执行时间过长的线程,使用join()会导致主线程长时间阻塞。

向线程函数传递参数

  线程函数传参,是在线程启动时向任务函数传递参数。两种启动线程的方式分别对应以下的传参形式:

// 函数指针
void thread1(const char* name)
{
    LOGD("--> This is %s.\n", name);
}
std::thread th1(thread1, "thread1");

// lambda
std::thread th2([](const char *name) {
    LOGD("-> This is %s.\n", name);
}, "thread2");

注: 当参数为字符串常量(如"thread1")或者字符串变量时,任务函数参数类型应为const char*。

转移线程所有权

  转移线程所有权是将一个线程的任务函数的控制权转移到另一个线程。
  转移所有权,我理解的是在局部函数或特定阶段,能够随意控制指定线程而不受外部影响,另外也会减少资源开销。

  std::thread 支持移动的好处是可以创建thread_guard类的实例, 并且拥有其线程的所有权。 当thread_guard对象所持有的线程已经被引用, 移动操作就可以避免很多不必要的麻烦; 这意味着, 当某个对象转移了线程的所有权后, 它就不能对线程进行加入或分离。 为了确保线程程序退出前完成,下面的代码里定义了scoped_thread类。

class scoped_thread
{
    std::thread t;
public:
    explicit scoped_thread(std::thread t_): // 1
    t(std::move(t_))
    {
        if(!t.joinable()) // 2
            throw std::logic_error(“No thread”);
    } 
    ~scoped_thread()
    {
        t.join(); // 3
    } 
    scoped_thread(scoped_thread const&)=delete;
    scoped_thread& operator=(scoped_thread const&)=delete;
};

void f()
{
    int some_local_state;
    scoped_thread t(std::thread t([]() {
        LOGD("-> This is thread.\n");
    });); // 4
        
    do_something_in_current_thread();
} // 5

运行时决定线程数量

std::thread::hardware_concurrency()这个函数用于获取程序可以调动的最大线程数,在多核系统中可能代表CPU核数。 这个函数返回值仅可以作为参考,因为有可能返回0。

识别线程

  线程标识类型是 std::thread::id , 可以通过两种方式进行检索:

  • 线程内通过std::this_thread::get_id()获取线程ID。
  • 线程外部通过 std::thread 对象的成员函数 get_id() 获取。
{
    std::thread th2([](const char *name) {
        std::stringstream ss;
        ss << std::this_thread::get_id();  // 1
        LOGD("ID: %s -> This is %s.\n", ss.str().c_str(), name);
    }, "thread2");

    std::stringstream ss;
    ss << th2.get_id();  // 2
    LOGD("ID: %s -> This is %s.\n", ss.str().c_str(), "thread2");
    th2.join();
}

  ①在线程th2任务函数内通过std::this_thread::get_id()获取当前线程的ID。 ②在线程外部,通过th2成员函数get_id()获取th2线程的ID。

总结

  • 多线程并发是一种双刃剑,在涉及到多线程交互的设计时,一定要慎之又慎。能不用则不用,需要用时做好多线程共享数据设计。
  • 相比Linux原生多线程接口,C++多线程封装的接口使用起来更方便。

参考

本文由mdnice多平台发布

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容