Linux之多线程(下)——线程控制

简介: Linux之多线程(下)——线程控制

前言

本文介绍了Linux下的线程控制。


一、POSIX线程库

1.概念

与线程有关的函数构成了一个完整的系列,大多数函数名都是以“pthread_”为开头的,要使用这些函数需要引入头文件pthread.h。链接这些线程函数库需要使用编译器命令的-lpthread选项。

2.pthread线程库是应用层的原生线程库

我们在Linux之多线程(上)这篇文章中了解:在Linux中没有真正意义上的线程,因此系统无法直接给我们提供创建线程的系统接口,只能提供创建轻量级进程额度接口。

在用户角度,当我们想创建一个线程时会想使用thread_sreate这样的接口,而不是像vfork这样的函数。用户不能直接访问OS,所以OS在用户和系统调用之间提供了编写好的用户级线程库,这个库一般称为pthread库。任何Linux操作下系统都必须默认携带这个库,因此这个库也称为原生线程库。

原生线程库本质上是对轻量级进程的系统调用(clone)做了封装——pthread_create,用户层也因此模拟实现了一套线程相关的接口。

用户眼中的线程实际上会在OS内部被转化为轻量级进程。

3.错误的检查

传统的函数,成功就返回0,失败返回-1,并且给全局变量errno赋错误码以指示错误。

pthread函数出错时并不会设置全局变量errno(大部分其他POSIX函数会设置),而是讲错误码通过返回值返回。当然,pthread函数是提供了线程内的errno变量,以支持其他使用errno的代码。对于pthread函数的错误,建议通过返回值判定,因为读取返回值比读取线程内的errno变量的开销更小

二、线程控制

1.创建线程——pthread_create

pthread_create函数

参数:

  1. thread:获取线程的ID,该参数是输出型参数;
  2. attr:用于设置创建线程的属性,传入nullptr表示默认,这个属性一般不用管直接传nullptr就行;
  3. start_routine:函数地址,表示线程启动后要执行的函数;
  4. arg:传给线程例程的参数。

返回值:

成功返回0,失败返回错误码。

例子

创建一个新线程

文件mythread.cc

1 #include<iostream>
  2 #include<string>
  3 #include<unistd.h>
  4 #include<pthread.h>
  5 #include<assert.h>
  6 using namespace std;
  7 void* thread_routine(void* args)
  8 {
  9         string name = static_cast<const char*>(args);//安全的进行强制类型转换
 10         while(1)
 11         {
 12                 cout<<"这是新线程, name:"<<name<<endl;
 13                 sleep(1);
 14         }
 15 }
 16 int main()
 17 {
 18         pthread_t id;
 19         int n = pthread_create(&id, nullptr, thread_routine, (void*)"thread new");
 20         assert(n == 0);
 21         (void)n;
 22         while(1)
 23         {
 24                 cout<<"我是主线程,我正在运行"<<endl;
 25                 sleep(1);
 26         }
 27         return 0;
 28 }

这里编译运行需要注意的是,pthread_create接口是库提供给我们的,我们使用的接口如果不是语言上的接口或者操作系统的接口,而是库提供的接口,那么在编译的时候是无法通过的,需要链接这个库才能编译成功。要链接这个库首先要找到这个库,-L:找到库在哪里;-l:找到头文件在哪里,库已经在系统中安装好了,所以除了高所系统库和头文件在哪里以外,还要知道是链接哪一个库(库名字)。

所以要加上-lpthread

此时我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们只能看到一个进程,因为这两个线程是属于一个进程的:

要想查看到轻量级进程需要使用ps -aL指令:

其中LWP(Light Weight Process)表示的是轻量级进程的ID,可以看到显示出的两个轻量级进程的PID是相同的(因为它们属于同一个进程),而每个轻量级进程都有唯一的LWP。

注意:主线程的PID和LWP是相同的,PID和LWP不相同的是新线程,所以CPU进行调度时,是以LWP为标识符进行标定一个线程执行流。

线程一旦被创建,几乎所有的资源都是被所有线程所共享的,因此线程之间想要进行交互是很容易的,因为直接就可以看到同一份资源。

线程要有自己的私有资源:

线程被调度就要有独立的PCB属性——LWP;

线程被切换时正在运行,需要进行上下文的保存,因此线程要有私有的上下文结构;

每个线程都要独立的运行,所以线程要有自己独立额度栈结构。

主线程创建一批新线程

让主线程一次性创建十个新线程,并让创建的每个新线程都去执行start_routine函数,即start_routine这个函数会被重复进入。并且start_routine函数是可重入函数(不会产生二义性),没有因为一个线程去影响另一个线程。在函数定义内定义的变量都是局部变量具有临时性,所以在多线程的情况下也没有问题。

文件mythread.cc

这也说明了每个线程都有自己独立的栈结构

2.获取线程ID——pthread_self

获取线程ID:1.创建线程时通过输出型参数获取;2.通过pthread_self接口函数获取。

我们可以通过主线程打印出新线程的ID,再通过新线程打印出自己的ID,判断是否相同。

结果是相同的。

3.线程等待——pthread_join

一个线程退出时和进程一样是需要等待的,如果线程不等待,对应的PCB没有被释放也会造成类似僵尸进程的问题(内存泄漏)。所以线程也需要被等待:1.获取新线程的退出信息;2.回收新线程对应的PCB等内核资源,防止内存泄漏。

参数:

thread是被都能打线程的ID;

retval:线程退出时的退出码。

void** retval:输出型参数,主要用于获取线程退出时返回的退出结果。之所以是void**,是因为如果想作为输出型结果返回就必须是void**(因为线程函数的返回结果是void*)

返回值:线程等待成功返回0,等待失败返回错误码。

没有看到线程退出时对应的退出码是因为线程出异常时收到信号,整个进程都会退出,而退出信息需要进程来关心,所以pthread_join默认会认为函数是调用成功的(等待成功),它不会考虑程序出现异常的情况,异常问题是进程该考虑的情况。

4.线程终止——return、pthread_exit、pthread_cancel

一个线程,如果只是想终止该线程而不是整个进程,有三种做法

  1. 直接从线程的函数结束,return就可以终止该线程;
  2. 线程可以自己调用pthread_exit终止自己;
  3. 一个线程可以调用pthread_cancel来终止同一个进程中的另一个线程。

return

pthread_exit

pthread_cancel

5.分离线程——pthread_detach

线程是可以等待的,等待的时候是join的等待(阻塞式等待)。如果我们不想等待:不去等待线程,而是进行分离线程处理。默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放该线程的资源,造成内存泄漏。

如果我们并不关心线程的返回值,此时join对我们来说是一种负担,这时,我们可以告诉OS,当线程退出时,自动释放线程资源,这种策略就是线程分离

pthread_detach函数

例子

创建新线程,让主线程与新线程运行起来,主线程等待新线程退出,等待完毕返回n。由于我们现在让新线程进行分离,那么按照理论此时主线程的等待结果是失败的。

文件mythread.cc

1 #include<iostream>
  2 #include<unistd.h>
  3 #include<pthread.h>
  4 #include<string.h>
  5 using namespace std;
  6 #include<string>
  7 string changeld(const pthread_t& thread_id)
  8 {
  9         char tid[128];
 10         snprintf(tid, sizeof(tid), "0x%x", thread_id);
 11         return tid;
 12 }
 13 void* start_routine(void* args)
 14 {
 15         string name = static_cast<const char*>(args);
 16         pthread_detach(pthread_self());//线程分离,设置为分离状态
 17         int cnt = 5;
 18         while(cnt--)
 19         {
 20                 cout<<name<<"is running..."<<changeld(pthread_self())<<endl;
 21                 sleep(1);
 22         }
 23         return nullptr;
 24 }
 25 int main()
 26 {
 27         pthread_t tid;
 28         pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
 29         string main_id = changeld(pthread_self());
 30         cout<<"main thread running... new thread id:"<<changeld(tid)<<"main thread id:"<<main_id<<endl;
 31         int n = pthread_join(tid, nullptr);
 32         cout<<"result:"<<n<<":"<<strerror(n)<<endl;
 33         return 0;
 34 }

运行:

但是我们发现等待结果依旧是成功的,这是为什么?

因为,我们创建新线程后,并不确定新线程和主线程哪个先被调度,所以可能导致我们还没有执行新线程的pthread_detach时,主线程就去等待新线程了。也就是说,新线程还没有来得及分离自己,主线程就去等待了。

因此我们可以让主线程sleep(2),保证新线程是分离的状态主线程再去等待,则此时等待是失败的。

文件mythread.c

1 #include<iostream>
  2 #include<unistd.h>
  3 #include<pthread.h>
  4 #include<string.h>
  5 using namespace std;
  6 #include<string>
  7 string changeld(const pthread_t& thread_id)
  8 {
  9         char tid[128];
 10         snprintf(tid, sizeof(tid), "0x%x", thread_id);
 11         return tid;
 12 }
 13 void* start_routine(void* args)
 14 {
 15         string name = static_cast<const char*>(args);
 16         pthread_detach(pthread_self());//线程分离,设置为分离状态
 17         int cnt = 5;
 18         while(cnt--)
 19         {
 20                 cout<<name<<"is running..."<<changeld(pthread_self())<<endl;
 21                 sleep(1);
 22         }
 23         return nullptr;
 24 }
 25 int main()
 26 {
 27         pthread_t tid;
 28         pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
 29         string main_id = changeld(pthread_self());
 30         cout<<"main thread running... new thread id:"<<changeld(tid)<<"main thread id:"<<main_id<<endl;
 31         sleep(2);
 32         int n = pthread_join(tid, nullptr);
 33         cout<<"result:"<<n<<":"<<strerror(n)<<endl;
 34         return 0;
 35 }

运行:

当然,我们也可以直接让主线程直接pthread_detach,而不是让新线程分离:线程运行起来就直接分离了,分离成功就去join了,此时新线程就去等待了。

文件mythread.c

1 #include<iostream>
  2 #include<unistd.h>
  3 #include<pthread.h>
  4 #include<string.h>
  5 using namespace std;
  6 #include<string>
  7 string changeld(const pthread_t& thread_id)
  8 {
  9         char tid[128];
 10         snprintf(tid, sizeof(tid), "0x%x", thread_id);
 11         return tid;
 12 }
 13 void* start_routine(void* args)
 14 {
 15         string name = static_cast<const char*>(args);
 16         int cnt = 5;
 17         while(cnt--)
 18         {
 19                 cout<<name<<"is running..."<<changeld(pthread_self())<<endl;
 20                 sleep(1);
 21         }
 22         return nullptr;
 23 }
 24 int main()
 25 {
 26         pthread_t tid;
 27         pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
 28         string main_id = changeld(pthread_self());
 29         pthread_detach(tid);//让主线程线程分离
 30         cout<<"main thread running... new thread id:"<<changeld(tid)<<"main thread id:"<<main_id<<endl;
 31         int n = pthread_join(tid, nullptr);
 32         cout<<"result:"<<n<<":"<<strerror(n)<<endl;
 33         return 0;
 34 }


总结

以上就是今天要讲的内容,本文介绍了线程控制相关的概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。

最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

相关文章
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
30天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
30天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
30天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
30天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
30天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1
|
30天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
25 1
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
48 6
|
1月前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
41 1
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
25 0
C++ 多线程之线程管理函数
下一篇
无影云桌面