• linux——多线程,线程控制


    目录

    一.POSIX线程库

    二.线程创建

    1.创建线程接口

    2.查看线程

    3.多线程的健壮性问题

    4.线程函数参数传递

    5.线程id和地址空间

    三.线程终止

    1.pthread_exit

    2.pthread_cancel

    四.线程等待 

    五.线程分离


    一.POSIX线程

    站在内核的角度,OS只有轻量级进程,没有线程的概念,但是站在用户的角度我们只有线程没有轻量级进程的概念。因为Linux下没有真正意义上的线程,而是用进程模拟的线程,所以Linux不会提供直接创建线程的系统调用,最多给我们提供创建轻量级进程的接口。

    所以linux对下对LWP的接口进行封装,对上给用户提供线程控制的接口——POSIX线程库,pthread库,这是任何一个linux系统都会带的库,又叫原生线程库。

    POSIX线程:

    1. 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
    2. 要使用这些函数库,要通过引入头文
    3. 链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

     二.线程创建

    1.创建线程接口

    功能:创建一个新的线程。
    原型:

    1. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    2. void *(*start_routine)(void*), void *arg);

    参数:

    1. thread:返回线程ID。
    2. attr:设置线程的属性,attr为NULL表示使用默认属性。
    3. start_routine:是个函数地址,线程启动后要执行的函数,该函数返回值是void*,参数是void*。
    4. arg:传给线程启动函数的参数。

    返回值:

    • 成功返回0;失败返回错误码。

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void *FuncRun(void *argc)
    7. {
    8. while (1)
    9. {
    10. cout << "I am thread,my pid:" << getpid() << endl;
    11. sleep(1);
    12. }
    13. }
    14. int main()
    15. {
    16. //线程id
    17. pthread_t id;
    18. //创建线程
    19. pthread_create(&id, NULL, FuncRun, NULL);
    20. while (1)
    21. {
    22. cout << "I am main,my pid:" << getpid() << endl;
    23. sleep(1);
    24. }
    25. return 0;
    26. }

    测试结果:

    说明:

    1. 线程没有父子之分,但是线程有主线程,和新线程的区分。
    2. 我们可以看到,主线程pid和新线程的pid是相同的。因为他们本身就是同一个进程的一部分。
    3. 新线程和主线程谁先被调度取决于调度器。

     2.查看线程

    查看线程使用命令:

    ps -aL

     3.多线程的健壮性问题

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void *FuncRun1(void *argc)
    7. {
    8. int count = 0;
    9. while (1)
    10. {
    11. count++;
    12. cout << "I am thread1,my pid:" << getpid() << endl;
    13. if (count == 5)
    14. {
    15. int tmp = count / 0;
    16. }
    17. sleep(1);
    18. }
    19. }
    20. void *FuncRun2(void *argc)
    21. {
    22. while (1)
    23. {
    24. cout << "I am thread2,my pid:" << getpid() << endl;
    25. sleep(1);
    26. }
    27. }
    28. int main()
    29. {
    30. // 线程id
    31. pthread_t id1, id2;
    32. // 创建线程
    33. pthread_create(&id1, NULL, FuncRun1, NULL);
    34. pthread_create(&id2, NULL, FuncRun2, NULL);
    35. while (1)
    36. {
    37. cout << "I am main,my pid:" << getpid() << endl;
    38. sleep(1);
    39. }
    40. return 0;
    41. }

    测试结果:

    说明:

    1. 代码其中有一个线程在第五秒的时候,会出现一个除0的问题。
    2. 当一个线程因为某一个错处而导致线程终止的时候,整个进程也都会直接终止。
    3. 因为信号是发送给进程的,最终OS直接对由信号做出的处理动作也是针对进程的。

     4.线程函数参数传递

     我们想通过给线程函数传参让线程执行更加复杂的任务。

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. // 任务,计算[1-top]的求和,并将结果存储到sum中。
    7. struct task
    8. {
    9. task(int top, int num)
    10. : _thread_name("thread" + to_string(num)), _top(top), _sum(0), _num(num)
    11. {
    12. }
    13. string _thread_name; // 线程名字
    14. int _top; // 计算数据范围
    15. int _sum; // 结果
    16. int _num; // 线程编号
    17. };
    18. // 线程函数
    19. void *FuncRun(void *argc)
    20. {
    21. task *t = (task *)argc;
    22. for (int i = 1; i <= t->_top; i++)
    23. {
    24. t->_sum += i;
    25. }
    26. }
    27. int main()
    28. {
    29. // 线程id
    30. pthread_t id1, id2;
    31. // 创建线程
    32. task t1(100, 1);
    33. task t2(150, 2);
    34. pthread_create(&id1, NULL, FuncRun, &t1);
    35. pthread_create(&id2, NULL, FuncRun, &t2);
    36. // 等待线程计算完再输出结果
    37. sleep(1);
    38. cout << t1._thread_name << ":[1-" << t1._top << "]=" << t1._sum << endl;
    39. cout << t2._thread_name << ":[1-" << t2._top << "]=" << t2._sum << endl;
    40. return 0;
    41. }

    测试结果:

    说明:

    • 由于接口的设计上参数的类型是void* ,这也就使得我们可以给线程函数传递的参数是非常丰富的。

    5.线程id和地址空间

    1. pthread_ create 函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
    2. 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
    3. pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
    pthread_t pthread_self(void);

    pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

    我们在对线程做操作的时候,根本上是使用线程库对线程进行操作,那么线程库本质就是存在于linux上的动态库,在我们使用线程库的时候,线程库也会向普通的动态库一样加载到共享区中,我们使用线程库方法就是访问自己的地址空间。

    线程库中需要被管理的线程会有很多,线程库也必然实现了线程的数据结构——TCB(线程控制块)。

    每个线程都有自己的TCB,和独立的上下文数据,以及线程栈空间。pthread_t 就是指向他们的首地址的一个地址。

     三.线程终止

    如果需要只终止某个线程而不终止整个进程,可以有三种方法:

    1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
    2. 线程可以调用pthread_ exit终止自己。
    3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

    1.pthread_exit

    功能:线程终止.
    原型:void pthread_exit(void *value_ptr);
    参数:value_ptr:value_ptr不要指向一个局部变量,返回线程结果。
    返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void *FuncRun1(void *argc)
    7. {
    8. int count = 0;
    9. while (1)
    10. {
    11. count++;
    12. cout << "I am thread-1-count:" << count << endl;
    13. sleep(1);
    14. // 三秒后线程1退出
    15. if (count == 3)
    16. {
    17. pthread_exit(NULL);
    18. }
    19. }
    20. }
    21. void *FuncRun2(void *argc)
    22. {
    23. int count = 0;
    24. while (1)
    25. {
    26. count++;
    27. cout << "I am thread-2-count:" << count << endl;
    28. sleep(1);
    29. // 三秒后线程2退出
    30. if (count == 3)
    31. {
    32. pthread_exit(NULL);
    33. }
    34. }
    35. }
    36. int main()
    37. {
    38. // 线程id
    39. pthread_t id1, id2;
    40. // 创建线程
    41. pthread_create(&id1, NULL, FuncRun1, NULL);
    42. pthread_create(&id2, NULL, FuncRun2, NULL);
    43. while (1)
    44. {
    45. cout << "I am main,my pid:" << getpid() << endl;
    46. sleep(1);
    47. }
    48. return 0;
    49. }

    测试结果:

    说明:

    1. 线程在退出后,主线程并没有受到影响,进程也有没受到影响。
    2. 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

    2.pthread_cancel

    功能:取消一个执行中的线程
    原型:int pthread_cancel(pthread_t thread);
    参数:thread:线程ID
    返回值:成功返回0;失败返回错误码

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void *FuncRun1(void *argc)
    7. {
    8. int count = 0;
    9. while (1)
    10. {
    11. count++;
    12. cout << "I am thread-1-count:" << count << endl;
    13. sleep(1);
    14. }
    15. }
    16. void *FuncRun2(void *argc)
    17. {
    18. int count = 0;
    19. while (1)
    20. {
    21. count++;
    22. cout << "I am thread-2-count:" << count << endl;
    23. sleep(1);
    24. }
    25. }
    26. int main()
    27. {
    28. // 线程id
    29. pthread_t id1, id2;
    30. // 创建线程
    31. pthread_create(&id1, NULL, FuncRun1, NULL);
    32. pthread_create(&id2, NULL, FuncRun2, NULL);
    33. int count = 0;
    34. while (1)
    35. {
    36. count++;
    37. sleep(1);
    38. if (count == 3)
    39. {
    40. // 三秒后终止线程
    41. pthread_cancel(id1);
    42. pthread_cancel(id2);
    43. }
    44. cout << "I am main" << endl;
    45. }
    46. return 0;
    47. }

    测试结果:

    四.线程等待 

    为什么需要线程等待?

    • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
    • 创建新的线程不会复用刚才退出线程的地址空间。

     功能:等待线程结束。
    原型:int pthread_join(pthread_t thread, void **value_ptr);
    参数:thread:线程ID。
    value_ptr:它指向一个指针,后者指向线程的返回值。
    返回值:成功返回0;失败返回错误码。

    调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下: 

    1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
    2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数,PTHREAD_ CANCELED。
    3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
    4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

     测试代码:

    1. void *FuncRun1(void *argc)
    2. {
    3. int *top = (int *)argc;
    4. int *sum = new int;
    5. for (int i = 1; i <= *top; i++)
    6. {
    7. *sum += i;
    8. }
    9. // 线程退出
    10. pthread_exit(sum);
    11. }
    12. void *FuncRun2(void *argc)
    13. {
    14. int *top = (int *)argc;
    15. int *sum = new int;
    16. for (int i = 1; i <= *top; i++)
    17. {
    18. *sum += i;
    19. }
    20. // 线程退出
    21. return sum;
    22. }
    23. void *FuncRun3(void *argc)
    24. {
    25. int *top = (int *)argc;
    26. int *sum = new int;
    27. for (int i = 1; i <= *top; i++)
    28. {
    29. *sum += i;
    30. sleep(1);
    31. }
    32. free(sum);
    33. // 线程退出
    34. }
    35. int main()
    36. {
    37. int top1 = 100;
    38. int top2 = 150;
    39. int top3 = 200;
    40. pthread_t id1;
    41. pthread_t id2;
    42. pthread_t id3;
    43. pthread_create(&id1, NULL, FuncRun1, &top1);
    44. pthread_create(&id2, NULL, FuncRun2, &top2);
    45. pthread_create(&id3, NULL, FuncRun3, &top3);
    46. pthread_cancel(id3);
    47. // 接受线程返回数据
    48. void *ret_ptr1;
    49. void *ret_ptr2;
    50. void *ret_ptr3;
    51. // 等待线程
    52. pthread_join(id1, &ret_ptr1);
    53. pthread_join(id2, &ret_ptr2);
    54. pthread_join(id3, &ret_ptr3);
    55. cout << "ret1:" << *((int *)ret_ptr1) << endl;
    56. free(ret_ptr1);
    57. cout << "ret2:" << *((int *)ret_ptr2) << endl;
    58. free(ret_ptr2);
    59. if (ret_ptr3 == PTHREAD_CANCELED)
    60. cout << "ret3:PTHREAD_CANCELED" << endl;
    61. return 0;
    62. }

    测试结果:

    五.线程分离

    1.  默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
    2. 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
    int pthread_detach(pthread_t thread);

    可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

    pthread_detach(pthread_self());

    joinable分离是冲突的,一个线程不能既是joinable又是分离的。

    测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void *FuncRun(void *argc)
    9. {
    10. // 线程分离
    11. pthread_detach(pthread_self());
    12. int count = 0;
    13. while (1)
    14. {
    15. count++;
    16. cout << "I am thread-count:" << count << endl;
    17. sleep(1);
    18. }
    19. }
    20. int main()
    21. {
    22. pthread_t tid;
    23. int n = pthread_create(&tid, NULL, FuncRun, NULL);
    24. if (n != 0)
    25. {
    26. cerr << "pthread_create:" << strerror(errno) << endl;
    27. }
    28. sleep(2);
    29. // 线程已经分离,再去线程等待,pthread_join会立即报错。
    30. if (pthread_join(tid, NULL) == 0)
    31. {
    32. printf("pthread wait success\n");
    33. }
    34. else
    35. {
    36. printf("pthread wait failed\n");
    37. }
    38. return 0;
    39. }

    测试结果:

    六.线程封装

    我们对线程操作有了一定的理解,但是线程各种操作还是有些繁琐,我们可以用类将线程封装,仅仅通过成员函数就可以完成对线程的控制。

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. typedef enum Status
    10. {
    11. NEW = 0,
    12. EXIT,
    13. RUNNING
    14. } Status;
    15. class Thread
    16. {
    17. private:
    18. typedef void *(*Func)(void *);
    19. public:
    20. // 构造函数
    21. Thread(int num, Func func, void *args)
    22. : _func(func), _args(args)
    23. {
    24. char __name[50] = {0};
    25. sprintf(__name, "thread-%d", num);
    26. _name = __name;
    27. _status = NEW;
    28. }
    29. // 如果类的回调函数是一个类的成员函数,那么这个成员函数需要是一个
    30. // 静态成员函数,因为普通成员函数有this指针的干扰
    31. static void *RunFunc(void *args)
    32. {
    33. Thread *ts = static_cast(args);
    34. return (*ts)();
    35. }
    36. void *operator()()
    37. {
    38. return _func(_args);
    39. }
    40. // 线程开始执行
    41. void run()
    42. {
    43. pthread_create(&_tid, NULL, RunFunc, this);
    44. _status = RUNNING;
    45. }
    46. // 线程等待
    47. void *join()
    48. {
    49. void **ret = (void **)new void *;
    50. pthread_join(_tid, ret);
    51. _status = EXIT;
    52. return *ret;
    53. }
    54. // 返回线程的名字
    55. string name()
    56. {
    57. return _name;
    58. }
    59. ~Thread()
    60. {
    61. }
    62. private:
    63. Func _func; // 回调的函数
    64. string _name; // 线程名称
    65. pthread_t _tid; // 线程id
    66. Status _status; // 线程状态
    67. void *_args; // 线程回调函数的参数
    68. };

  • 相关阅读:
    JVM学习----垃圾回收
    【气动学】基于Matlab模拟各类导弹跟踪
    基于SSM的宿舍管理系统(有报告)。Javaee项目。
    LeetCode_动态规划_中等_688.骑士在棋盘上的概率
    《近期BSN开发常见问题答疑(2022.9.23)》
    关于F大学教学主体偏离的核心矛盾分析
    如何在30分钟完成表格增删改查的前后端框架搭建
    C++异常
    【Linux/脚本/芯片学习】Perl学习
    java项目开发实例SSM框架实现的车位租赁管理系统|停车场计费系统
  • 原文地址:https://blog.csdn.net/qq_63943454/article/details/133715121