C++并发编程(二):线程管理

本文详细介绍了C++中的线程管理,包括如何启动线程、使用join()和detach()函数、异常处理、传递参数以及获取系统并发线程数等。强调了join()在异常情况下的重要性和detach()可能导致的问题,同时也讲解了如何使用类的成员函数作为线程函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

启动线程


thread类的构造函数接收一个可调用的类型。可调用的类型,除了函数,还可以是函数对象和lamda表达式。

class foo{
public:  
    void operator() (){
        for(int i = 0 ; i < 5; i++)
            std::cout << "foo concurrent \n";
    }
};
void fun(){
    for(int i = 0 ; i < 5; i++)
        std::cout << "lamda concurrent \n";
}
int main()
{
    using std::thread;
    foo my_foo;
    thread t1(my_foo);    //函数对象
    thread t2(fun);       //函数
    thread t3([]{fun();});//lamda表达式
    t1.join();
    t2.join();
    t3.join();
}

注意:在传入函数对象时,不能直接传入临时变量,这会引起c++语法解析错误(c++'s most vexing parse):

thread t2(foo());

foo()返回一个临时变量。对于上述代码,编译器会解析为返回thread类名为t2的函数,而不是创建一个线程。在传入函数对象时,我们除了创建一个非临时变量(如同最上方代码那样),还可以使用如下两种方式:

thread t4((foo()));
thread t5{foo()};

注意,t4的foo()外层还有一层()。

使用join()函数阻塞当前线程


当我们在当前线程中,创建一个新的线程。父线程结束时,会回收自己的资源,但并不会理会子线程是否结束,并强行调用线程的析构函数,此时会报terminate called without an active exception。

class foo{
public:  
    void operator() (){
        for(int i = 0 ; i < 100000; i++)
            std::cout << "foo concurrent \n";
    }
};

int main()
{
    std::thread t{foo()};
}

输出结果为:

foo concurrent 
terminate called without an active exception
foo concurrent 
foo concurrent 
foo concurrent 
foo concurrent 
foo concurrent 
foo concurrent 

我们可以看到报了异常,且子线程并没有和我们想的一样输出100000个字符串,而是在中途就停止了。此时,我们可以使用join函数来让当前线程等待子线程。

join()函数会阻塞当前线程,只有当子线程结束之后,当前线程才会继续往下运行。joinable()函数会判断当前线程是否可以被等待。同一个线程只能被等待一次。

int main()
{
    std::thread t{foo()};
    if(t.joinable())
        t.join();
}

异常情况下完成线程等待


int main()
{
    std::thread t{foo()};
    do_something();
    t.join();
}

假设do_something()会抛出异常。当异常发生时,程序会停止,不再运行下方的代码,t线程也会被终止。为了避免线程被异常终止,我们需要在当前线程退出之前调用join函数。下面介绍2种方法完成异常情况下的线程等待。

1、使用try{}catch{}块

int main()
{
    std::thread t{foo()};
    try{
        do_something();
    }catch(...){
        t.join();
        throw;
    }
    t.join();
}

通过捕获异常的方法,在发生异常之后调用join()函数。

2、资源获取即初始化(RAII,Resource Acquisition Is Initialization)

将线程变量交由一个类来管理,并在类的析构函数中加入join。当发生异常或者线程正常退出时,管理类的对象就会被销毁,调用析构函数来触发join()函数。

class thread_guard
{
    thread &t;
public :
    explicit thread_guard(thread& _t) :
        t(_t){}
    ~thread_guard()
    {
        if (t.joinable())
            t.join();
    }
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;
};
int main()
{
    std::thread t{foo()};
    thread_guard(t);
    do_something();
}

使用detach()函数完成线程分离


有些时候我们希望新建的线程和当前线程脱离关系。

detach()函数会将线程分离,当前线程的状态变化,并不会影响新建线程。

class foo{
public:  
    void operator() (){
        for(int i = 0 ; i < 100000; i++)
            std::cout << "foo concurrent \n";
    }
};

int main()
{
    std::thread t{foo()};
    t.detach();
}

通过使用detach()函数,我们可以在当前线程结束之后,依旧运行新建线程,两者互不干扰。

但是,下面这种情况一定要注意!

class fun
{
public:
    int& m_i;
    fun(int& i) : m_i(i) {}
    void operator() ()
    {
        for (unsigned j=0 ; j<1000000 ; ++j)
        {
            std::cout << m_i;
        }
    }
};

int main()
{
    int i = 0;
    std::thread t{fun(i)};
    t.detach();
}

fun把i的引用作为变量保存。当main结束时,i的内存会被回收,但是此时新建线程仍然在运行,此时访问的i的地址的值是不确定的值,所以上述代码的运行结果是不确定的。所以,如果线程有调用引用或指针,要使用join(),避免使用detach()分离线程,除非可以保证子线程可以比父线程更早完成。

向线程函数传递参数


传递参数,只需要在初始化thread时,接在可调用类型之后。

void foo(int i){ std::cout << i}

int main(){
    thread t(foo,1);
    t.join();
}

但是要注意,默认的传值方式是值拷贝,thread会用新的地址来接收变量的值,即使函数接受的参数是引用也无效。

void swap(int & x, int & y){
    int temp = x;
    x = y;    
    y = temp;
}

int main(){
    int x = 0;
    int y = 1;
    thread t(swap, x, y);
    t.join();
    cout << "x " << x << ",y " << y;
}

结果的x,y值并没有交还。我们需要使用std::ref()函数包裹x和y值才可以实现引用传递。

std::thread t(swap, std::ref(x), std::ref(y));

注:我的编译器上如果不加std::ref(),是无法通过编译的,会提示需要传入引用。我尝试了一下,当声明swap(int &, int &)时,thread初始化时,必须使用ref()传入参数,即必须使用std::thread t(swap, std::ref(x), std::ref(y)),否则无法通过编译。这是新版标准吗?还希望有大神解答。

使用类的成员函数作为线程函数


class f1{
public:
    void hello(int i){
        std::cout << i << std::endl;
    }
};
int main(){
    f1 f;
    std::thread t(&f1::hello, &f, 1);
    t.join();
}

第一个参数为成员函数,第二参数为对象,之后参数为成员函数的参数。

转移线程所有权和线程id


thread类支持移动语义,我们可以将一个右值赋值给一个thread变量,从而实现线程所有权的转移。但是,thread禁止了左值的赋值操作。将一个左值赋值给thread对象是违法的。我们可以通过get_id函数来查看进程的id号。

class f1{
public:
    void hello(int i){
    }
};
int main(){
    f1 f;
    std::thread t(&f1::hello, &f, 1);
    std::cout << t.get_id() << std::endl;
    std::thread t1 = std::move(t);
    std::cout << t1.get_id();
    //std::thread t2 = t1;    //左值赋值违法!
    //t.join();    //t没有控制权,操作违法!
    t1.join();
}

程序输出为:

140189120116480
140189120116480

我们可以看出同一个线程在多个对象之间进行了传递。同时,所有权转移之后,t对象没有指向任何线程,此时调用join和detach将是违法的。

获取系统能并行的线程数量


使用std::thread::hardware_concurrency()函数可以获取系统能同时并发在一个程序中的线程数量。通过此数值,我们可以设置启动时的最佳线程数。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值