0


【Linux】多线程(上)

一、概念

1.1 线程的概念

  • 线程(thread)是进程内的一个执行流,能够执行进程代码的一部分。线程只创建PCB,同一进程内的线程共用进程地址空间和页表
  • 一个进程内部至少有一个线程
  • 线程是CPU调度的基本单位,进程是分配系统资源的基本实体
  • 进程包含多个执行流、地址空间、页表、物理内存分配的空间等系统资源,线程是进程内部的执行流资源

不同操作系统对线程的实现方案可能是不一样的。在部分其他的操作系统中,线程有自己独立的线程控制模块TCB(Thread Control Block),但是这样就要为进程单独重新设计各种调度算法

而Linux中选择直接复用进程的数据结构和管理算法来实现线程,即Linux中的线程是由进程模拟的线程,所以认为Linux没有真正意义上的线程,又称为轻量级进程(LWP)。这也是为何上面说线程也要创建PCB,因为复用了进程的结构

进程与线程是1:n的关系,因此操作系统中线程的数量一定比进程要多

进程与线程的关系可以类比为家庭与家人的关系,家庭中的家人可以共享家庭内的资源,并且各司其职维持家庭的运行,家庭中至少存在一个家人

我们过去学习的进程,实际上就是只有一个线程执行流的进程

1.2 线程周边概念

说线程是轻量级进程,首先在于线程的创建与释放更加轻量化。线程在创建时只需要创建新的PCB,不需要创建新的地址空间和页表,释放时也只需要释放PCB

其次,线程的切换也更加轻量化

  • CPU中存在cache缓存,会进行热数据缓存,即将一些被高频访问的数据存到cache缓存中,提高效率
  • 切换进程时,需要重新在cache中缓存新的数据,数据由冷变热是需要时间的。而切换线程不需要切换cache内的数据

二、线程的优缺点

2.1 优点

  • 创建和释放的成本比进程更低,切换需要的工作量更小
  • 占用资源比线程少
  • 对于多处理器系统,可以充分利用可并行数量
  • 对于在多处理器系统上运行的计算密集型应用,可将计算任务分解到多个进程中运行
  • 对于I/O密集型应用,多线程可同时等待不同的I/O操作

2.2 缺点

  • 若计算密集型线程较少被外部事件阻塞,且线程数量多于处理器数量,会增加额外的同步和调度消耗
  • 如果不加保护,多线程程序中可能存在线程冲突,影响代码健壮性
  • 线程是CPU调度的基本单元,在一个线程中执行了某些系统调用可能会影响到整个进程
  • 多线程程序的编写和调试难度高

三、线程与进程

  • 线程是CPU调度的基本单位,进程是操作系统分配资源的基本单位
  • 线程共享进程的数据,但也有自己独立的数据,如线程ID、寄存器、线程栈、错误码、信号屏蔽字、调度优先级

线程需要自己独立的一批寄存器存放线程的上下文

线程有自己独立的栈结构以避免线程之间出现错乱,不同线程之间的栈不共享!这与我们过去的认知有些区别

  • 同一进程中的多个线程共享线程地址空间,因此定义一个函数,在所有线程中都能调用;定义一个全局变量,所有线程都可以访问到
  • 线程间还共享进程的文件描述符表、不同信号的处理动作、当前工作目录、用户id和组id
  • 线程是进程的执行分支,一旦某个线程出现异常导致线程崩溃,整个进程也会随之崩溃

四、线程控制

4.1 POSIX线程库

Linux中没有明确的线程的概念,因此内核没有给我们提供线程的系统调用,只有轻量级进程的系统调用

但我们用户又需要线程的接口,因此开发者在应用层对轻量级进程的接口进行了封装,为用户提供了许多线程的接口——pthread线程库。几乎所有的Linux平台都默认自带这个第三方库,而我们在编写多线程代码时也需要使用这个库

要使用pthread线程库,需要引入头文件<pthread.h>,并且在编译链接时需要带-lpthread选项

4.2 线程创建

pthread_create

  1. #include <pthread.h>
  2. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
  3. void *(*start_routine) (void *), void *arg);

pthread_create函数用于创建一个新进程,成功返回0,失败返回错误码

其中:

  • thread:输出型参数,返回新线程的pthread_t(线程ID)
  • attr:设置线程的属性,一般设置为nullptr表示使用默认属性
  • start_routine:函数的地址,线程启动后将要执行的函数
  • arg:传给start_routine函数的参数,一般将对应参数强转为void*后传入

例子:

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. using namespace std;
  6. void* threadRoutine(void* arg) //新线程的例程
  7. {
  8. while(true)
  9. {
  10. cout << "new thread, pid:" << getpid() << endl;
  11. sleep(2);
  12. }
  13. return nullptr;
  14. }
  15. int main()
  16. {
  17. pthread_t tid;
  18. pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
  19. while(true) //主线程
  20. {
  21. cout << "main thread, pid:" << getpid() << endl;
  22. sleep(2);
  23. }
  24. return 0;
  25. }

运行结果:

可以看到主线程和新线程的PID是一样的,操作系统怎么区分呢?

运行程序,输入 ps -aL 可以看到当前所有线程的信息

可以看到同一进程内的线程虽然PID相同,但LWP是不同的,操作系统根据LWP对线程进行调度

主线程的PID与LWP相同

pthread_t及地址空间的布局

需要额外一提,pthread_t所说的线程ID与我们前面指的线程ID不是一回事,前面提到的线程ID属于线程调度的范畴,用于表示线程的唯一性。而pthread_t类型的参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID

线程是如何在进程地址空间中布局的?

我们的线程库需要被加载到内存中,线程库也需要维护我们创建的线程,因此在线程库中就需要对线程进行管理。因此,我们创建的线程,在线程库中还有库级别的TCB,其起始地址就是线程的tid

线程的独立栈结构是在线程库内部进行维护的,线程库是动态库,需要被加载到共享区

线程需要有自己独立的栈结构,因为每个线程都有自己独立的调用链。除了主线程,其他所有线程的独立栈结构都在共享区——准确来说是在pthread库中tid指向的用户TCB内部

4.3 线程终止

前面提到,线程如果被异常终止,那整个进程也会随之崩溃,那如何正常的使线程终止呢?

  • 对于非主线程的线程,可以直接在线程的例程中return
  • 线程调用pthread_exit终止自己
  • 线程调用pthread_cancel终止同一进程中的其他线程

我们先来看pthread_exit函数

pthread_exit

  1. #include <pthread.h>
  2. void pthread_exit(void *retval);

retval是一个输出型参数,用于带出线程例程的返回值,pthread_exit函数本身无返回值

注意,调用pthread_exit时,retval指向的内存单元一定得是全局的或者是用malloc分配的,而不能是线程例程中的一个局部变量,否则线程终止后变量被销毁,retval就变成一个野指针了

包括如果我们在线程的例程中return了一个指针,这个指针指向的内存单元也必须符合上面的要求

例子:

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. using namespace std;
  6. void* threadRoutine(void* arg) //新线程的例程
  7. {
  8. int cnt = 0;
  9. while(true)
  10. {
  11. if(cnt == 3)
  12. {
  13. cout << "new thread exit!" << endl;
  14. pthread_exit(nullptr);
  15. }
  16. cout << "new thread, pid:" << getpid() << endl;
  17. cnt++;
  18. sleep(1);
  19. }
  20. return nullptr;
  21. }
  22. int main()
  23. {
  24. pthread_t tid;
  25. pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
  26. while (true) // 主线程
  27. {
  28. cout << "main thread, pid:" << getpid() << endl;
  29. sleep(1);
  30. }
  31. return 0;
  32. }

运行结果:

pthread_cancel

  1. #include <pthread.h>
  2. int pthread_cancel(pthread_t thread);

pthread_cancel函数用于取消一个执行中的线程,成功返回0,失败返回错误码,参数thread传入指定线程的ID

被取消的线程本身在例程中会返回PTHREAD_CANCELED的宏,其内容是(void*)-1

例子:

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. using namespace std;
  6. void* threadRoutine(void* arg) //新线程的例程
  7. {
  8. while(true)
  9. {
  10. cout << "new thread, pid:" << getpid() << endl;
  11. sleep(1);
  12. }
  13. return nullptr;
  14. }
  15. int main()
  16. {
  17. pthread_t tid;
  18. pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
  19. int cnt = 0;
  20. while (true) // 主线程
  21. {
  22. if(cnt == 3)
  23. {
  24. int n = pthread_cancel(tid);
  25. cout << "new thread cancel!" << endl;
  26. break;
  27. }
  28. cout << "main thread, pid:" << getpid() << endl;
  29. cnt++;
  30. sleep(1);
  31. }
  32. return 0;
  33. }

运行结果:

4.4 线程等待

pthread_join

  1. #include <pthread.h>
  2. int pthread_join(pthread_t thread, void **retval);

pthread_join函数用于阻塞式等待一个线程的结束,成功返回0,失败返回错误码

其中:

  • thread:要等待的线程ID
  • retval:指向一个指针,后者指向线程的返回值

pthread_join函数等待的线程必须是joinable的,即等待的线程不能已经分离(后面会提到)

调用该函数的线程将会阻塞式等待目标线程退出,其中又分为以下几种情况:

  • 目标线程通过return返回,则retval指向的内存空间存放return的返回值
  • 目标线程自己调用pthread_exit终止,则retval指向的内存单元存放pthread_exit的参数
  • 目标线程被其他线程通过pthread_cancel取消,则retval指向的内存单元存放PTHREAD_CANCELED
  • 如果对目标线程的返回值不感兴趣,可以将retval设置为nullptr

例子:

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. using namespace std;
  6. int cnt = 0;
  7. void* threadRoutine(void* arg) //新线程的例程
  8. {
  9. while(true)
  10. {
  11. if(cnt == 3)
  12. {
  13. cout << "new thread exit!" << endl;
  14. pthread_exit(&cnt);
  15. }
  16. cout << "new thread, pid:" << getpid() << endl;
  17. cnt++;
  18. sleep(1);
  19. }
  20. return nullptr;
  21. }
  22. int main()
  23. {
  24. pthread_t tid;
  25. pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
  26. int *retval;
  27. pthread_join(tid, (void**)&retval);
  28. cout << *retval << endl;
  29. return 0;
  30. }

运行结果:

4.5 线程分离

pthread_detach

如果一个线程终止,而我们又没有对其进行线程等待,那么其资源没有被释放就会造成系统泄露。

有的时候,我们可能不需要线程返回一个值,或者不关注线程的返回值,那么对其进行线程等待是一种负担。此时我们就可以选择让线程分离,即线程退出时自己释放线程资源

  1. #include <pthread.h>
  2. int pthread_detach(pthread_t thread);

pthread_detach函数成功返回0,失败返回错误码,参数thread传入线程ID

可以是线程自己让自己分离,也可以是线程组内的其他线程对目标线程进行分离。问题:如果线程自己让自己分离,它怎么知道自己的线程ID呢?

pthread_self

  1. #include <pthread.h>
  2. pthread_t pthread_self(void);

pthread_self函数会返回调用该函数的线程的ID,搭配pthread_detach函数使用就能让线程自己分离

一个线程不能既是joinable的又是分离的,即线程如果分离就不能再被线程等待

例子:

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. using namespace std;
  6. void* threadRoutine(void* arg) //新线程的例程
  7. {
  8. pthread_detach(pthread_self());
  9. int cnt = 0;
  10. while(true)
  11. {
  12. if(cnt == 3)
  13. {
  14. cout << "new thread exit!" << endl;
  15. pthread_exit(&cnt);
  16. }
  17. cout << "new thread, pid:" << getpid() << endl;
  18. cnt++;
  19. sleep(1);
  20. }
  21. return nullptr;
  22. }
  23. int main()
  24. {
  25. pthread_t tid;
  26. pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
  27. sleep(1);
  28. if(pthread_join(tid, nullptr) == 0)
  29. cout << "wait thread success" << endl;
  30. else
  31. cout << "wait thread failed" << endl;
  32. return 0;
  33. }

运行结果:

如有错误,欢迎在评论区指出

【Linux】多线程(上)完.

标签: linux c++ 运维

本文转载自: https://blog.csdn.net/Eristic0618/article/details/143236732
版权归原作者 阿瑾0618 所有, 如有侵权,请联系我们删除。

“【Linux】多线程(上)”的评论:

还没有评论