C++并发(2)

线程管理:

(在编译的时候要加上-std=c++11)

每个程序至少有一个线程:执行main()函数的线程,其余线程都有其各自的入口函数。

当线程执行完入口函数后,线程就会退出。

启动线程:

需要先包含thread头文件

在创建std::thread对象时指定一个任务则启动一个新线程,任务在最简单的情况下是无参数无返回的函数,此thread实例就与新线程相关联

例如:

如果一个类中有函数调用操作符(),那么可以将此类的实例传给thread的构造函数,建立新线程

#include <iostream>
#include <thread>
using namespace std;

void do_something(int x)
{
        cout<<"doing :"<<x<<endl;
}
class Background_task
{
        private:
                int value=4;
        public: 
                void operator()()
                {       
                        value=3;
                        do_something(value);
                }
                int getvalue(){return value;}
};

int main()
{
Background_task f;
thread my_thread(f);
my_thread.join();
cout<<"value: "<<f.getvalue()<<endl;
}

运行结果:

从上述程序可以看出,f实例是复制了一个副本到my_thread新创建的执行线程的存储器中,并从那里被调用。

但当传递一个未命名的变量给thread实例时,如

std::thread my_thread(Background_task());

这时编译器可能会解释成声明了一个函数,此函数返回thread对象,且参数是一个函数指针(一个不接收任何参数并返回Background_task对象的函数指针)

为了避免这种情况可以使用

例子:

#include <iostream>
#include <thread>
using namespace std;

void do_something(int x)
{
        cout<<"doing :"<<x<<endl;
}
class Background_task
{
        private:
                int value=4;
        public: 
                Background_task()
                {
                        cout<<"hi"<<endl;
                }
                void operator()()
                {       
                        value=3;
                        do_something(value);
                }
                int getvalue(){return value;}
};

int main()
{
Background_task f;
thread my_thread(f);
my_thread.join();

cout<<"value: "<<f.getvalue()<<endl;
thread hi1((Background_task()));
thread hi2{Background_task()};
hi1.join();
hi2.join();
}

传递给hi1 ,hi2 这两个thread实例的就是临时的未命名的Background_task()变量。

开启了线程之后则有两个选择:1通过join()结合它,2通过detach()分离它

如果在thread对象被销毁之前还没有做决定,那么程序将会被终止(thread的析构函数将调用terminate())

如果决定不等待线程完成,用detach()分离它,则要注意下面的情况:

首先创建了一个func实例my_func,构造时是用some_local_state的引用,则func里的i指向的和some_local_state是同一个对象

因为func里有函数调用操作符,所以可以传递func构建thread实例,此时传递给thread构造函数的也是func的复制,但func的复制里i依旧指向和some_local_state是同一个对象

然后决定不等待线程,调用my_thread.detach()进行分离,让线程在后台运行。

此时就会出现问题,oops()函数执行完,在栈中的some_local_state变量应该被销毁,但后台运行的线程还是依旧在访问这一个已经被销毁的变量!

解决此问题的方法是:在构建my_func实例时就将some_local_state对象复制过去而不是用引用,这样当some_local_state被销毁时,后台运行的线程也不会去访问,而是访问自己存储器中some_local_state的复制。

还有一个方法是:在一个访问局部变量的函数中创建线程时,则要保证在函数退出前,线程能够完成。此时就需要用到join()(调用join行为会清理所有和该线程相关联的存储器,这样thread对象就不和任何线程关联,该thread对象也不再是可以对线程可连接的了)

但在调用join()后,初始线程在此期间做不了什么事,而在实际中,初始线程也会有自己的工作要去做。

这样可以创建一个类来实现:

#include <iostream>
#include  <thread>
using namespace std;

void do_something(int i)
{
        cout<<i<<endl;
}
struct func
{
        int& i;
        func(int& i_):i(i_){}
        void operator()()
        {
                for(unsigned j=0;j<100000;++j)
                {
                        do_something(i);
                }
        }
};
class Thread_guard
{
        private:
                thread& t;
        public:
                explicit Thread_guard(thread& t_):t(t_){}
                ~Thread_guard()
                {
                        if(t.joinable())
                                t.join();
                        cout<<"bye bye!"<<endl;
                }
                Thread_guard(Thread_guard const&)=delete;
                Thread_guard& operator=(Thread_guard const&)=delete;
};

void test()
{
        int some_local_state=0;
        func my_func(some_local_state);
        thread t(my_func);
        Thread_guard g(t);
}
int main()
{
        test();
}

运行结果:

当test()运行到末尾时,局部对象会按照构造函数的逆序被销毁。因此,Thread_guard对象g首先被销毁,并且在析构函数中会判断线程是否可以join(因为一个线程只能被join()一次,如果join()一个已经被结合的线程就是错误的),如果joinable()返回true,则调用join()等待线程完成。

Thread_guard的拷贝构造函数和拷贝赋值运算符被标记为delete,以确保不会由编译器自动提供。

在后台运行线程:

在thread对象上调用detach()会把线程丢到后台运行,此时即没有方法与之通信,也不可能等待它完成。如果一个线程成为分离的,获取一个引用它的thread对象也是不可能的,所以它也不再能够被结合。但分离的线程确实是在后台运行,与线程相关的资源会在线程退出后正确被回收。

调用detach()后,thread对象不再和执行的实际线程相关联,同时也不能被加入。

对于join()和detach(),相同的地方在于,调用之前要保证thread实例上有相关联的线程,可以通过joinable()进行检查。

例子,使用分离线程去处理其他文档

在上述例子中,在构建thread实例t时传递了一个参数new_name 给任务函数edit_document

基本上,传递参数给任务对象或函数,基本上就是简单地将额外的参数传递给thread构造函数,重要的是,参数会以复制出副本的形式传递过去。

#include <iostream>
#include <thread>
using namespace std;
void f(int x,int* y)
{
        x=2;
        *y=2;
        cout<<"x= "<<x<<",y="<<*y<<endl;
}

int main()
{
        int a=1,b=1;
        thread t(f,a,&b);
        t.join();
        cout<<"main thread: "<<endl;
        cout<<"a= "<<a<<", b="<<b<<endl;
}

结果:

#include <iostream>
#include <thread>
using namespace std;

void f(int x,int& y)
{
        x=3;
        y=3;
        cout<<"x="<<x<<",y="<<y<<endl;
}

int main()
{
        int a=2,b=2;
        thread t(f,a,ref(b));
        t.join();
        cout<<"a="<<a<<",b="<<b<<endl;
}
~       

t的构造中传递了给函数f的参数,因为b是以引用传递,所以需要加上ref(),a是通过值传递,所以会在新开启的线程里面复制一个新的值

注意的是如果任务函数的参数是希望以引用传递,则要加上ref(),否则thread的构造函数会不知道,会盲目复制提供的值,最后传给引用参数的变量会是原变量的一个复制的副本,最后本应该在原变量上进行的操作都在副本上进行,当任务函数结束,副本变量被销毁,原变量依旧什么都没有改变。

结果:

同时thread构造也可以指定类的成员函数

例子:

#include <iostream>
#include <thread>
using namespace std;
class X
{
        public:
                void do_something(int& a){
                        a=2;
                        cout<<"do something..."<<a<<endl;
                }
};

int main()
{
        int a=3;
        X my_x;
        thread t(&X::do_something,&my_x,ref(a));
        t.join();
}

传入一个成员函数的指针作为任务函数,然后提供一个该类对象的指针作为第一个参数,后面的第三个参数是成员函数的第一个参数。

C++的有些资源是非可复制的,只是可移动的,如unique_ptr,ifstream等,thread也属于这一类

移动构造函数和移动赋值运算符允许一个对象的所有权在实例之间进行转移,这种转移给源对象留下一个NULL指针

这种转移必须通过move()来请求

例如:

在thread构造函数中指定move(p),可以将big_object的所有权先被转移到新创建的线程的内部存储中,然后进入process_big_object

thread的实例也是可移动的而不是可复制的。


每一个thread实例负责管理一个执行线程,这种所有权可以在实例之间进行转移。这种转移给源对象留下一个NULL指针。

这种转移必须直接通过std::move()来完成

一个例子:

#include <iostream>
#include <thread>
using namespace std;

void f(int x,int& y)
{
        x=3;
        y=3;
        cout<<"x="<<x<<",y="<<y<<endl;
}
void hi()
{
        cout<<"hi"<<endl;
}
int main()
{
        int a=2,b=2;
        thread t1(f,a,ref(b));
        thread t2=move(t1);
        t1 = thread(hi);
        thread t3=move(t1);
        t2.join();
        t3.join();
//      t1.join();
        cout<<"a="<<a<<",b="<<b<<endl;
}

首先启动一个新线程与t1关联,然后显式地将与t1关联的线程的控制权交给t2,接下来再启动一个新线程和临时的thread对象关联,隐式地将控制权交给t1,t1再将该线程的控制权显式的交给t3。若将t1.join()的注释去掉,程序将产生join failed 异常终止程序。因为此时t1并没有和任何线程关联。同样的,也不能向管理一个线程的thread实例赋值一个新的值来舍弃之前的线程。

从函数中返回thread:

调用这两个函数,得到返回的thread实例对应的线程的控制权。

同样也可以将thread作为参数,将线程的控制权转移到函数中,函数的参数只能以值的形式接收thread的实例

一个例子:

#include <iostream>
#include <thread>
using namespace std;
struct func
{
        int& i;
        func(int& i_):i(i_){}
        void operator()()
        {
                for(unsigned j=0;j<100000;++j)
                {
                        cout<<i<<endl;
                }
        }
};
class Scope_thread
{
        thread t;
        public:
        explicit Scope_thread(thread t_):t(move(t_))
        {
                if(!t.joinable())
                        throw logic_error("no thread");
        }
        ~Scope_thread()
        {
                t.join();
        }
        Scope_thread(Scope_thread const&)=delete;
        Scope_thread& operator=(Scope_thread const&)=delete;
};

void test()
{
        int some_local_state=0;
        Scope_thread t((thread(func(some_local_state))));
}
int main()
{
        test();
}

Scope_thread实例t在构造时将新构建的thread未命名变量的关联线程的控制权转移到自己内部的thread变量

在运行时选择线程数量:

调用thread::hardware_currency()可以获得能真正实现并发线程数量的指示(cpu核心的数量)

标识线程:

线程标识符是thread::id 类型的,可以通过thread对象中调用get_id()成员函数来获得,如果thread对象没有关联的线程,则调用返回一个默认构造的thread::id对象,表示没有线程。当前线程的标识符可以通过 std::this_thread::get_id()来获得。

如果两个thread::id类型的对象相等,表示它们代表同一个线程,或者都没有线程

如果两个thread::id类型的对象不相等,则代表不同的线程,或者一个有线程另一个没有线程

猜你喜欢

转载自blog.csdn.net/ziggyPLAYguitar/article/details/82425404