文章

线程管理

学习一下

线程管理的基础

前言

每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原始线程 (以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执行完入口函数 后,线程也会退出。在为一个线程创建了一个std::thread对象后,需要等待这个线程结束。

启动新线程

  1. 使用C++线程库启动线程,可以归结为构造std::thread对象:
    1
    2
    
         void do_some_work(); //线程工作函数
         std::thread my_thread(do_some_work); //启动线程,传入一个无参函数
    

    std::thread类也可以用一个类构造,但是这个类要重载():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
         class background_task
         {
             public:
                 void operator()()const  //重载()
                 {
                     do_something();
                     do_something_else();
                 }
         };
    
         background_task f; //生成一个类实例
         std::thread my_thread(f); //启动线程,传入一个类实例
    

    代码中,提供的类实例会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间 中进行。

    当把类实例传入到线程构造函数中时,需要避免“C++语法解析”的问题。如果传入了一个临时变量, 而不是一个命名的变量,C++编译器会将其解析为函数声明,而不是类型对象的定义。例如:

    1
    
         std::thread my_thread(background_task())  //相当与声明了一个函数,并非启动了一个线程
    

    使用下面的方法可以避免上面的问题:

    1
    2
    
         std::thread my_thread( (background_task()) ); //方法1
         std::thread my_thread{ background_task() };   //方法2
    

    也可以使用lambda表达式避免上面的问题:

    1
    2
    3
    4
    
         std::thread my_thread([]{
             do_something();
             do_something_else();
         });
    

    启动线程后,需要明确是要等待线程结束join()方法,还是让其自主运行detach()。 如果std::thread()对象销毁之前还没有明确,程序就会终止(std::thread的析构函数会 调用std::terminate())。因此,需要特别注意的是,即使函数有异常存在,也必须,必须,必须 确保线程在执行完毕前,能够正确的加入joined()或者detached

    如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题—单线程代码中, 对象销毁之后再去访问,也会产生未定义行为—不过,线程的生命周期增加了这个问题发生的几率。这种 情况很可能发生在线程还没结束,函数退出的时候,这时线程函数还持有函数局部变量的指针或引用。比如 下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
         struct func{
             int& i_;
             func(int& i): i_(i) {}
             void operator()()  //重载()
             {
                 for (unsigned j=0; j<1000000; ++j)
                 {
                     do_something(i_); //潜在访问隐患:悬空引用   
                 }
             }
         };
    
         void oops()
         {
             int some_local_state=0;
             func my_func(some_local_state); //类实例
             std::thread my_thread(my_func);
             my_thread.detach();   //选择分离,不等待线程结束
         } //到这里类实例my_func会释放,新线程my_thread可能还在运行,此时my_func成员变量`i_`就没定义了
    

等待线程

如果选择等待线程,可以使用join(),即将上面的my_thread.detach()替换为my_thread.join(),就可以 确保局部变量在线程完成后,才被销毁。

值得注意的是join()是简单粗暴的等待线程完成或不等待。当需要对等待线程有更灵活的控制时,比如,看一下某个 线程是否结束,或者只等待一段时间(超过某个时间就判定为超时)。想要做这些,需要使用其他机制来完成,比如条件变量 和期待(futures)。

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

特殊情况下的等待

如前所述,如果打算等待对应线程,则需要细心挑选调用join()的位置。当在线程运行之后产生异常, 在jon()调用之前抛出,就意味着join()不会被调用。

如果想避免应用被抛出的异常所终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使用join() 时,需要在异常处理过程中调用join(),从而避免生命周期的问题。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        struct func{
            int& i_;
            func(int& i): i_(i) {}
            void operator()()  //重载()
            {
                for (unsigned j=0; j<1000000; ++j)
                {
                    do_something(i_); //潜在访问隐患:悬空引用   
                }
            }
        };
        void f()
        {
            int some_local_state=0;
            func my_func(some_local_state);
            std::thread t(my_func);
            try
            {
                do_something_in_current_thread();
            }
            catch(...)
            {
                t.join();  //异常发生时,也会加入
            }
            t.join();//正常执行加入
        }

另一种比较简单的机制,来解决这个问题的方法是“资源获取即初始化方法”(RAII, Resource Acquistion Is Initialization),并且提供一个类,在析构函数中使用join()。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
        class thread_guard
        {
            std::thread& t_;
            public:
                explicit thread_guard(std::thread& t):t_(t){}
                ~thread_guard()
                {
                    if(t_.joinable())
                    {
                        t.join();
                    }
                }
                thread_guard(thread_guard const&)=delete;
                thread_guard& operator=(thread_guard const&)=delete;
        };

        struct func{
            int& i_;
            func(int& i): i_(i) {}
            void operator()()  //重载()
            {
                for (unsigned j=0; j<1000000; ++j)
                {
                    do_something(i_); //潜在访问隐患:悬空引用   
                }
            }
        };

        void f()
        {
            int some_local_state=0;
            func my_func(some_local_state);
            std::thread t(my_func);
            thread_guard g(t);
            do_someing_in_current_thread();
        } //执行到这里的时候,局部对象会被逆序销毁,因此,thread_guard对象g是第一个
        //被销毁的,这时线程在系够函数中被加入到晕是线程中,即使do_somethin_in_current_thread
        //抛出一个异常,这个销毁依旧会发生。

后台运行进程

使用detach()会线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待 这个线程结束; 如果线程分离,那么就不可能有std::thread对象能引用它,分离线程的确在后台 运行,所以分离线程不能被加入。

通常称分离线程为守护线程(daemon threads),这种线程的特点是长时间运行,线程的生命周期可能 会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构 进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,发后即忘的任务就使用到这种 线程方式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        void edit_document(std::string const& filename)
        {
            open_document_and_display_gui(filename);
            while(!done_editing())
            {
                user_command cmd=get_user_input();
                if(cmd.type==open_new_document)
                {
                    std::string const new_name=get_filename-from_user();
                    std::thread t(edit_document,new_name);
                    t.detach();
                }
                else
                {
                    process_user_input(cmd);
                }
            }
        }

如果用户选择打开一个新文档,需要启动一个新线程去打开新文档,并分离线程。与当前 线程做出的操作一样,新线程只不过是打开另一个文件而已。所以,edit_document函数可以 复用,通过传参的形式打开新的文件

向线程函数传递参数

std::thread构造函数中的可调用对象,或函数传递一个参数很简单。需要注意的是,默认参数 会拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。例如:

1
2
        void f(int i, std::string const& s);
        std::thread t(f, 3, "hello");

代码创建了一个调用f(3,"hello")线程。函数f需要一个std::string对象作为第二个参数, 但这里使用的是字符串的字面值,也就是char const * 类型。之后,在线程的上下文中完成 字面值向std::string对象的转化。需要特别注意,当指向动态变量的指针作为参数传递给 线程的情况,代码如下:

1
2
3
4
5
6
7
8
        void f(int i, std::string const& s);
        void oops(int some_param)
        {
            char buffer[1024];
            sprintf(buffer, "%i",some_param);
            std::thread t(f,3,buffer);
            t.detach();
        }

在这种情况下,buffer是一个指针变量,指向本地变量,然后本地变量通过buffer传递到新线程中。 并且,函数很有可能会在字面值转化成std::string对象之前崩溃,从而导致一些未定义的行为。

解决方案就是在传递到std::thread构造函数之前就将字面值转换为std::string对象:

1
2
3
4
5
6
7
8
        void f(int i, std::string const& s);
        void oops(int some_param)
        {
            char buffer[1024];
            sprintf(buffer, "%i",some_param);
            std::thread t(f,3,std::string(buffer));//使用std::string,避免悬垂指针
            t.detach();
        }

还可能会遇到相反的情况,期望传递一个非常量引用,但整个对象被复制了。你可能会尝试使用线程 更新一个引用传递的数据结构,比如:

1
2
3
4
5
6
7
8
9
        void update_date_for_widget(widget_id w, widget_data& data);
        void oops_again(widget_id w)
        {
            widget_date data;
            std::thread t(update_date_for_widget, w, data);
            display_status();
            t.join();
            process_widget_data(data);
        }

虽然update_data_for_widget的第二个参数期待传入一个引用,但是std::thread的构造函数 并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量,所以编译时会出错。 问题的解决方法是显而易见的:可以使用std::ref将参数转换成引用的形式,从而可将 线程的调用改为以下形式:

1
        std::thread t(update_data_for_widget,w,std::ref(data));

可以传递一个成员函数指针作为线程函数,并提供 一个合适的对象指针作为第一个参数:

1
2
3
4
5
6
7
        class X
        {
            public:
                void do_lengthy_work();
        };
        X m_x;
        std::thread t(&X::do_lengthy_work,&my_x);

这段代码中,新线程将my_x.do_lengthy_work()作为线程函数:my_x的地址作为指针 对象提供给函数。也可以为成员函数提供参数:

1
2
3
4
5
6
7
8
        class X
        {
            public:
                void do_lengthy_work(int);
        };
        X m_x;
        int num(0);
        std::thread t(&X::do_lengthy_work,&my_x, num);

转移线程所有权

主要是可以使用std::move转移已经创建的线程所有权

1
2
3
4
5
6
7
8
        void some_function();
        void some_other_function();
        std::thread t1(some_function); //创建一个线程t1
        std::thread t2=std::move(t1); //将t1线程的所有权转移给t2
        t1=std::thread(some_other_function); //t1重新指向一个新线程
        std::thread t3; //创建一个新线程变量
        t3=std::move(t2); //将t2所有线程权转移给t3
        t1=std::mover(t3);  //程序崩溃,t1此时有别的线程,不能赋值

函数返回std::thread对象

1
2
3
4
5
6
7
8
9
10
11
        std::thread f()
        {
            void some_function();
            return std::thread(some_function);
        }
        std::thread g()
        {
            void some_other_function(int);
            std::thread t(some_other_function,42);
            return t;
        }

当所有权也可以在函数内部传递,就允许std::thread实例可作为参数进行传递,例如:

1
2
3
4
5
6
7
8
        void f(std::thread t);
        void g()
        {
            void some_funciton();
            f(std::thread(some_function)); //线程作为参数传递
            std::thread t(some_function);
            f(std::move(t));
        }

std::thread支持移动的好处是可以创建thread_guard类的实例,并且拥有其线程所有权。 可以用这个类来管理线程,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
        class joining_thread
        {
            std::thread t_;
            public:
                joining_thread(); noexcept=default;
                template<typename Callable, typename ... Args>;
                explicit joining_thread(Callable&& func, Args&& ... args):
                    t(std::forward<Callable>(func), std::forward<Args>(args)...)
                    {}
                explict joining_thread(std::thread t) noexcept:
                    t_(std::move(t))
                    {}
                joining_thread& operator=(joining_thread&& other) noexcept
                {
                    if(joinable()){
                        join();
                    }
                    t_=std::move(other.t);
                    return *this;
                }
                joining_thread& operator=(std::thread other) noexcept
                {
                    if(joinable())
                    {
                        join()
                    }
                    t_=std::move(other);
                    return *this;
                }
                ~joining_thread() noexcept
                {
                    if(joinable())
                    {
                        join();
                    }
                }
                void swap(joining_thread& other) noexcept
                {
                    t_.swap(other.t);
                }
                std::thread::id get_id() const noexcept
                {
                    return t_.get_id();
                }
                bool joinable() const noexcept
                {
                    return t_.joinable();
                }
                void join()
                {
                    t_.join();
                }
                void detach()
                {
                    t_.detach();
                }
                std::thread& as_thread() noexcept
                {
                    return t_;
                }
                const std::thread& as_thread() const noexcept
                {
                    return t_;
                }      
        };

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

第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果 std::thread对象没有与任何线程相关联,get_id()将返回std::thread::type 默认构造值,这个值表示“无线程”

第二种,在当前线程中调用std::thread::get_id()也可以获得线程标识

本文由作者按照 CC BY 4.0 进行授权