《C++ thread类》

1. thread类

  thread类是C++中表示执行线程的类,位于头文件<thread>中。我们创建一个thread对象就会立即执行一个对应的线程。通过thread类的成员函数,我们可以标识线程或对线程进行控制。

1.1 启动线程

  我们构造一个thread类的对象,就会立即执行一个与该thread对象对应的线程。通常我们会向thread类的构造函数传递一个可调用对象,新创建的线程就会执行这个可调用对象。可调用对象可以是函数函数指针lambda表达式function对象

#include <iostream>
#include <functional>
#include <thread>

using namespace std;

void func(void)
{
    cout << "hello world" << endl;
}

int main(void)
{
    void (*func_ptr)(void) = func;
    function<void(void)> func_obj = func;

    thread t1(func);
    thread t2(func_ptr);
    thread t3(func_obj);
    thread t4([]
              { cout << "hello world" << endl; });

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}

  在上面的例子中,我们分别向thread类的构造函数传递了函数、函数指针、function对象和lambda表达式。

传递参数:

  如果我们想要想可调用对象传递参数,只需要将参数和可调用对象一并放入thread类的构造函数的参数列表中即可:

#include <iostream>
#include <functional>
#include <thread>

using namespace std;

void add(int n1, int n2)
{
    cout << n1 << " + " << n2 << " = " << n1 + n2 << endl;
}

int main(void)
{
    thread t1(add, 2, 3);
    thread t2([](int n1, int n2)
              { cout << n1 << " + " << n2 << " = " << n1 + n2 << endl; },
              5, 6);

    t1.join();
    t2.join();

    return 0;
}

  在以上的例子中,我们向thread类传入可调用对象和两个参数,可调用对象在执行时输出两个参数的和。

  thread类的构造函数可以接受任意多的额外的参数,只需要传入的参数与可调用对象的参数列表一一对应即可。

1.2 连接线程与分离线程

连接线程:

  join成员函数的作用是等待线程完成。请读者试想一下,假如我们在main函数中创建了一个线程对象A,线程A在被创建之后执行了一个非常耗时的任务。而main函数(主线程)在创建完线程A后执行return 0,整个进程就会被终止,此时线程A即使没有执行完成也会被终止,程序的只想结果往往就不是我们所期望的了。所以我们需要在主线程中调用join成员函数来等线程执行完成,这个操作也叫连接线程。当我们调用join时,如果线程尚未执行完成,就对阻塞调用join的线程直至线程执行完成。若干在调用join时线程已经执行完成,则join会立即返回,不会阻塞调用线程。
分离线程:

  如果我们不需要等待线程完成,可以调用thread对象的detach成员函数分离线程,之后这个线程就可以独立运行,不需要我们调用join等待它执行完成。分离线程通常用于执行一些后台任务的线程。

  其中连接线程执行完成后,需要用join来释放该完成线程的资源。分离线程执行完成后,资源就自动释放。

 

可连接线程与不可连接线程:

  如果一个thread对象是符合以下任意一种情况,它将是不可连接的:

1.默认构造的thread对象。
2.该thread对象已经被移动到另一个thread对象。
3.thread对象已经被连接或分离。

  不能对不可连接的线程对象调用join。

1.5 thread类的析构函数

  当一个thread对象被析构时,如果该thread对象对应的线程还没有执行完成,线程仍然会继续执行,不会因为thread对象被析构而停止执行。

  如果一个线程是可连接的,则必须在thread对象被析构前调用join,否则析构函数会调用std::terminate终止程序。

 

1.6 线程标识

  在C++中用thread::id类来标识线程。对于可连接线程,可以通过get_id成员函数获得标识该thread对象的thread::id对象,每个thread对象的id唯一。对于不可连接线程,调用get_id会返回默认构造的thread_id对象,所有不可连接线程的id相等。

  thread::id类重载了一下运算符用于thread::id对象的相等性比较:

bool operator== (thread::id lhs, thread::id rhs) noexcept;
bool operator!= (thread::id lhs, thread::id rhs) noexcept;
bool operator< (thread::id lhs, thread::id rhs) noexcept;
bool operator<= (thread::id lhs, thread::id rhs) noexcept;
bool operator> (thread::id lhs, thread::id rhs) noexcept;
bool operator>= (thread::id lhs, thread::id rhs) noexcept;

  thread::id还重载了<<运算符用于向输出流中输出thread::id:

template <class charT, class traits>
basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);

 

1.7 this_thread命名空间

  this_thread命名空间提供了访问当前线程的一些函数,除了上文提到的get_id外,还有yieldsleep_untilsleep_for

yield:

  调用yield函数可以让出当前线程,让操作系统调度同一进程的其他线程。

sleep_until:

  sleep_until可以阻塞调用线程直至某个时间点。函数原型为:

template <class Clock, class Duration>
  void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);

sleep_for:

  sleep_for可以在制定的时间跨度内阻塞线程的执行。函数原型为:

template <class Rep, class Period>
  void sleep_for (const chrono::duration<Rep,Period>& rel_time);

 

 

 

2.mutex和std::lock_guard的使用

  头文件是#include <mutex>,mutex是用来保证线程同步的,防止不同的线程同时操作同一个共享数据。

  但使用lock_guard则相对安全,它是基于作用域的,能够自解锁,当该对象创建时,它会像m.lock()一样获得互斥锁,当生命周期结束时,它会自动析构(unlock),不会因为某个线程异常退出而影响其他线程。示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <stdlib.h>
 
int cnt = 20;
std::mutex m;
void t1()
{
    while (cnt > 0)
    {    
        std::lock_guard<std::mutex> lockGuard(m);
       // std::m.lock();
        if (cnt > 0)
        {
            //sleep(1);
            --cnt;
            std::cout << cnt << std::endl;
        }
       // std::m.unlock();
        
    }
}
void t2()
{
    while (cnt > 0)
    {
        std::lock_guard<std::mutex> lockGuard(m);
        // std::m.lock();
        if (cnt > 0)
        {
            --cnt;
            std::cout << cnt << std::endl;
        }
        // std::m.unlock();
    }
}
 
int main(void)
{
    std::thread th1(t1);
    std::thread th2(t2);
 
    th1.join();    //等待t1退出
    th2.join();    //等待t2退出
 
    std::cout << "here is the main()" << std::endl;
 
    return 0;
}

  因为lock_guard是基于作用域的,因此可以通过{}(花括号可以看作是作用域标识符,表示某些变量的作用域,决定他们的生命周期),来定义临界区的大小。

  这边举个花括号决定作用域的例子:

void fun()
{   
int a;   { std::lock_guard<std::mutex> lockGuard(m);   int b;    a=0;//正确,a在作用域内   }   b=0; //错误,b的生命周期在上个{}结束了 }

 

posted @ 2023-05-22 17:12  一个不知道干嘛的小萌新  阅读(326)  评论(0编辑  收藏  举报