服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - C++初级线程管理

C++初级线程管理

2022-07-27 09:52龙小 C/C++

这篇文章主要介绍了C++初级线程管理,C++11中提供了std::thread库,本文将从线程的启动、线程等待、线程分离、线程传参、线程识别等几个方面介绍初级线程管理的知识,需要的朋友可以参考一下

前言:

实际程序运行时,每个程序都有一个程序入口,线程也不例外,使用线程时,需要给线程提供一个入口函数,线程执行完入口函数时,线程将退出。C++11中提供了std::thread库,本文将从线程的启动、线程等待、线程分离、线程传参、线程识别等几个方面介绍初级线程管理的知识。

1 线程启动

C++11中,线程的启动终究是对std::thread的对象进行构造。

线程构造的类别如下:

1.1  线程函数无参数无返回值

此类可以说是最简单的线程启动,函数不需要传参也不需要返回函数执行结果,执行完成后,线程自动退出。

形如:

?
1
2
void FunDoingNothing();
std::thread(FunDoingNothing)

编写代码时,需要加上<thread>头文件以方便编译器能够正确处理thread对象。

1.2  线程函数有参数无返回值

C+=11中,thread的构造函数中使用了可变参数,这样,可以使得构造thread对象时可以自定义传入参数,

构造函数的定义如下:

?
1
template<class F, class... Args> explicit thread(F&& f, Args&&... args);

在实际使用时,线程函数有参数时可以定义形式如下:

?
1
2
3
4
void printMsg(int a, int b) {
  cout << "input params are:" << a <<","<<b<< endl;
}
std::thread my_thread(printMsg, 3, 4)

1.3  调用可调用的类型构造

使用时,可以将带有执行函数的变量传入thread的构造函数中从而替换默认的构造函数,

如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
using namespace std;
class BackGroundTask{
public:
    void operator()() const{
        doSomeThing();
    }
priavte:
    doSomeThing();
};
int main(){
    BackGroundTask f;
    std::thread myThread(f);
}

上面的代码中,在启动线程时同构构造对象f,f对象的重载函数中调用了线程运行时要执行的方法。但有一点需要注意的是,在传入临时的构造对象时,不经过处理,可能会让编译器产生错误的理解。

如:

?
1
std::thread myThread(BackGroundTask());

这里相当与声明了一个名为myTread的函数, 这个函数带有一个参数(函数指针指向没有参数并返回BackGroundTask对象的函数), 返回一个 std::thread 对象的函数, 而非启动了一个线程。

如果要解决这个问题,只需要如下处理即可:

?
1
2
std::thread myThread((BackGroundTask()));
std::thread myThread{BackGroundTask()};

当然,也可以使用lamda表达式实现上述功能,如下:

?
1
2
3
std::thread myThread([]{
  doSomeThing();
  });

2 等待线程

C++11中,确保线程执行完后,主线程在退出,需要在代码中使用join()函数,这样就可以保证变量在线程结束时才会进行销毁。

2.1 join等待

在实际编程时,join函数只是简单的等待或者不等待。在有些场景下就会不使用,如果想要进行更加灵活的控制,需要使用C++11中提供的其他机制,这个也会在后面的推文中进行说明。
在编程时,如果对一个线程使用了join,那么在后续的操作中如果使用joinable()执行结果将返回false。既一旦使用了join。线程对象将不能重复使用。如下代码中,在线程中使用join等待。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BackGroundTask
{
public:
    void operator()()
{
        doSomeThing();
    }
private:
    void doSomeThing() {cout<<"线程退出"<<endl;};
};
int main()
{
    BackGroundTask f;
    std::thread myThread(f);
    myThread.join();
    cout<<"退出"<<endl;
}

上面的代码使用了线程等待,可以输出正确的结果,如下:

线程退出
退出

如果将 myThread.join()语句注释,再次执行时,程序将执行出错,因为在子线程还没有结束时,主线程已经结束。

运行结果如下:

退出
terminate called without an active exception

上面的输出具备不确定性,代码运行时结果随机。

2.2 异常场景的join等待

异常场景中,如果没有充分考虑join的位置,就可能会产生因为异常导致主线程先于子线程退出的情况,解决这些问题可以通过下面两种方法进行处理:

2.2.1  通过异常捕获

通过分析代码中的异常场景,对异常使用try...catch进行捕获,然后在需要线程等待的地方调用join()函数,这种方法虽然可以轻易地捕获问题并对问题进行修复,但并非是通用法则,还需要根据实际情况进行分析。如检查并确认是否线程函数中是否使用了局部变量的引用等其它原因。

2.2.2 使用RAII方式进行线程等待

RAII可以理解为资源获取既初始化。因为全写为:Resource Acquisition Is Initialization
实际使用时,通过定义一个类,然后在析构函数中使用join函数进行线程等待。这样可以避免场景有遗漏的地方。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class thread_guard
{
private:
    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;
};

 如上,通过在将线程对象传入到类thread_guard中,如果thread_guard类对象的局部变量被销毁,则在析构函数中会将线程托管到原始线程。
thread_guard中,使用delete标识,禁止生成该类的默认拷贝构造、以及赋值函数。
在实际编程时如果不想线程等待,可以使用detach方法,将线程和主线程进行分离。

3 线程分离

线程分离使用detach方法,使用后将不能在对已分离的线程进行管理,但是分离的线程可以真实的在后台进行运行。当线程退出时,C++会对线程资源进行清理和回收。
线程分离通常被用作守护线程或者后台工作线程。

使用方法如下:

?
1
2
3
4
5
6
7
int main()
{
    BackGroundTask f;
    std::thread myThread(f);
    myThread.detach();
    cout<<"退出"<<endl;
}

4 向线程传递参数

向线程传递参数非常简单,在上面的代码中也有提及,这里主要说下向线程中传递参数的陷阱。

看下面的代码:

?
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是一个局部指针变量,使用后,可能会导致线程出现未定义的行为,因为从char*到string的转换时使用的是隐式转换,但是thread在使用时会将变量拷贝到线程私有内存,但是并不知道需要将参数进行转换,因此复制到私有内存的变量就没有转换成期望的对象。
如果要解决这个问题,可以在使用时直接将参数类型转换成函数默认的类型,在上面的例子中可以

做如下操作:

?
1
std::thread t(f,3,std::string(buffer));

但是这样做依然存在问题,既线程在复制变量到私有内存时,只复制了变量值,这样在线程调用后,如果继续使用线程函数处理后的变量时可能变量并没有改造,依旧是线程调用之前的变量。
因此要想在函数传参过程中使得线程拷贝时依旧保持引用,可以在线程调用时使用引用方式,

如:

?
1
std::thread t(f,3,std::ref(std::string(buffer)));

5 线程识别

每个线程都有一个线程标识,在C++11中,线程标识通过std::thread::id进行标识,std::thread::id可以复用并进行比较,如果两个线程的id相等,那么它们就是同一个线程或者没有线程,如果不等就表示两个是不同的线程或者其中一个线程不存在。

线程id的获取方法有两种,如下:

5.1 thread成员函数获取

通过std::thread::get_id()可以获取线程的id。

使用方法如下:

?
1
2
3
4
5
6
7
8
int main()
{
    BackGroundTask f;
    std::thread myThread(f);
    cout<<"线程id:"<<myThread.get_id()<<endl;
    myThread.detach();
    cout<<"退出"<<endl;
}

线程运行结果为:

  • 线程id:139879559096064
  • 退出

5.2 std::this_thread::get_id()

线程id可以用来区分主线程和子线程,通过std::this_thread::get_id()可以先将主线程id保存,然后在和子线程进行比较,从而区分主线程和子线程。

代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
    std::thread::id master_thread=std::this_thread::get_id();
    BackGroundTask f;
    std::thread myThread(f);
    if(master_thread!=myThread.get_id())
    {
        cout<<"子线程id:"<<myThread.get_id()<<endl;
    }
    myThread.detach();
    cout<<"退出"<<endl;
}

代码中,先保存了主线程的id标识,然后获取子线程id,比较两个线程id。如果不相等则输出子线程id。

代码运行结果如下:

子线程id:140161423791872

到此这篇关于C++初级线程管理的文章就介绍到这了,更多相关C++线程管理内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://mp.weixin.qq.com/s?__biz=MjM5ODg5MDIzOQ==&mid=2650491405&idx=1&sn=1fdd4ffc12255a02347a5969a849939b&chksm=becc359d89bbbc8bfc5d3d7be4b0fd441fa5374aaa4180c040f986dc4a00913b6a987a475206&scene=178&cur_album_id=2095208955286192128#rd

延伸 · 阅读

精彩推荐