• Linux 多线程多进程


    一、线程初级

    1.1 线程基础函数

    对象操作Linux Pthread APIWindows SDK 库对应 API
    线程创建pthread_createCreateThread
    退出pthread_exitThreadExit
    等待pthread_joinWaitForSingleObject
    互斥锁创建pthread_mutex_initCreateMutex
    销毁pthread_mutex_destroyCloseHandle
    加锁pthread_mutex_lockWaitForSingleObject
    解锁pthread_mutex_unlockReleaseMutex
    条件创建pthread_cond_initCreateEvent
    销毁pthread_cond_destroyCloseHandle
    触发pthread_cond_signalSetEvent
    广播pthread_cond_broadcastSetEvent / ResetEvent
    等待pthread_cond_wait / pthread_cond_timedwaitSingleObjectAndWait

    1.2 示例

    创建调用线程的简单示例

    1. #include
    2. #include
    3. #include
    4. #include
    5. void* PrintMessage(void *ptr);
    6. int main()
    7. {
    8. pthread_t print_message_thread_tid;
    9. pthread_create(&print_message_thread_tid,NULL,&PrintMessage,NULL);
    10. pthread_join(print_message_thread_tid,NULL);
    11. }
    12. void* PrintMessage(void *ptr)
    13. {
    14. printf("This is PrintMessage function\n");
    15. }

    代码解释:

    其中,用到了pthread_t类型变量,pthread_create函数,pthread_join函数,线程函数PrintMessage。

    (1)pthread_t类型变量

    线程的TID,通过pthread_create创建线程后,返回一个线程tid,线程的标识符。

    扩充知识:注意线程PID、TID、进程PID的区别。

    进程PID:进程开启之后,在系统中是唯一的,不可重复的
    线程TID:创建一个线程之后,线程有一个标识符,此标识符只在该线程所属的进程上下文才有意义,为pthread_t数据类型。
    线程PID:Linux中的POSIX线程库实现的线程其实也是一个进程(LWP),只是该进程与主进程(启动线程的进程)共享一些资源而已,比如代码段,数据段等。在系统中是唯一的,不可重复的

    (2)pthread_create函数

    int   pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)

    第一个参数为指向线程标识符的指针,也就是线程对象的指针,tid

    第二个参数用来设置线程属性,通常直接设为NULL。多于一个参数时应当使用结构体传入

    第三个参数是线程运行函数的地址,函数指针

    第四个参数是线程要运行函数参数多于一个参数时应当使用结构体传入

    返回值:成功返回0,失败返回对应错误码

    重点介绍一下参数二和四。

    参数2:线程属性pthread_attr_t主要包括detach属性、policy属性、优先级、继承属性、堆栈地址、scope属性、堆栈大小。在pthread_create中,把第二个参数设置为NULL的话,将采用默认的属性配置。

    (1) detach属性:如果设置为PTHREAD_CREATE_DETACHED 则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。

    非分离状态。一个可结合的线程能够被其他线程收回其资源杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的

    分离状态。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

    设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

    栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。由系统自动分配。

    堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。需要程序员自己申请。delete 或 free 语句就能够正确的释放本内存空间。动态分配的内存在堆。


    (2)policy属性:schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_attr_setschedpolicy ()来改变。
    (3)优先级:schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。
    (4)继承属性:inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。通过pthread_attr_setinheritsched()设置,缺省为PTHREAD_EXPLICIT_SCHED
    (5)scope属性:scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。通过pthread_attr_setscope()设置结果不可测。也就是尽量不要传递变量进去。

    创建线程前先调用pthread_attr_init()进行线程属性初始化

    然后设置各个属性

    设置好属性后调用pthread_create拉起线程

    函数原型:   int pthread_attr_init (pthread_attr_t* attr);

    函数原型:   int pthread_attr_setscope (pthread_attr_t* attr, int scope);

    函数原型:    int pthread_attr_setdetachstate (pthread_attr_t* attr, int detachstate);

    函数原型:   int pthread_attr_setschedparam (pthread_attr_t* attr, struct sched_param* param);

    函数原型:  int pthread_attr_getschedparam (pthread_attr_t* attr, struct sched_param* param);

    参考链接:linux线程的创建和属性pthread_attr_t设置_芹泽的博客-CSDN博客

    通常解决方案:
    重新申请一块内存,存入需要传递的参数,再将这个地址作为arg传入。

    线程参数的传递,多值,单值

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void* PrintMessage(void *ptr);
    10. void PollPrintMessage();
    11. void* ParameterTransfer(void *ptr);
    12. void PollParameterTransfer();
    13. void* ParameterTransfer2(void *ptr);
    14. void PollParameterTransfer2();
    15. void PollDetachPthread();
    16. void* DetachPthread(void *ptr);
    17. struct Param {
    18. int a;
    19. int b;
    20. int c;
    21. };
    22. int main()
    23. {
    24. PollPrintMessage();
    25. PollParameterTransfer();
    26. PollParameterTransfer2();
    27. PollDetachPthread();
    28. }
    29. void PollPrintMessage()
    30. {
    31. pthread_t print_message_thread_tid;
    32. pthread_create(&print_message_thread_tid,NULL,&PrintMessage,NULL);
    33. pthread_join(print_message_thread_tid,NULL);
    34. }
    35. void* PrintMessage(void *ptr)
    36. {
    37. printf("This is PrintMessage function\n");
    38. }
    39. //传递单值
    40. void PollParameterTransfer()
    41. {
    42. int num = 23;
    43. pthread_t parameter_transfer_thread_tid;
    44. pthread_create(¶meter_transfer_thread_tid,NULL,&ParameterTransfer,&num);
    45. pthread_join(parameter_transfer_thread_tid,NULL);
    46. }
    47. void* ParameterTransfer(void *ptr)
    48. {
    49. int tmp = *(int *)ptr;
    50. printf("tmp = %d\n", tmp);
    51. }
    52. //传递两个或以上的参数
    53. void PollParameterTransfer2()
    54. {
    55. Param param;
    56. param.a=1;
    57. param.b=2;
    58. param.c=3;
    59. pthread_t parameter_transfer2_thread_tid;
    60. pthread_create(¶meter_transfer2_thread_tid,NULL,&ParameterTransfer2,¶m);
    61. pthread_join(parameter_transfer2_thread_tid,NULL);
    62. }
    63. void* ParameterTransfer2(void *ptr)
    64. {
    65. Param tmp;
    66. tmp.a=((Param*)ptr)->a;
    67. tmp.b=((Param*)ptr)->b;
    68. tmp.c=((Param*)ptr)->c;
    69. //= *(Param *)ptr;
    70. printf("tmp = %d\n",tmp.a);
    71. printf("tmp = %d\n",tmp.b);
    72. printf("tmp = %d\n",tmp.c);
    73. }
    74. void PollDetachPthread()
    75. {
    76. int ret;
    77. pthread_t detach_pthread_tid;
    78. pthread_attr_t attr;
    79. ret = pthread_attr_init(&attr);
    80. if (ret != 0) {
    81. fprintf(stderr, "pthread_init error:%s\n", strerror(ret));
    82. exit(1);
    83. }
    84. //设置线程分离
    85. pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    86. ret = pthread_create(&detach_pthread_tid, &attr, DetachPthread, NULL);
    87. if (ret != 0) {
    88. fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
    89. exit(1);
    90. }
    91. //这一步只是验证是否成功属性分离,若成功 则回收失败
    92. ret = pthread_join(detach_pthread_tid, NULL);
    93. if (ret != 0) {
    94. fprintf(stderr, "pthread_join error:%s\n", strerror(ret));
    95. exit(1);
    96. }
    97. pthread_exit((void *)1);
    98. }
    99. void* DetachPthread(void *ptr)
    100. {
    101. }

    传递多值,需要写成结构体的形式。

    pthread_attr_init()

    在默认情况下通过pthread_create函数创建的线程是非分离属性的,由pthread_create函数的第二个参数决定,在非分离的情况下,当一个线程结束的时候,它所占用的系统资源并没有完全真正的释放,也没有真正终止

    只有在pthread_join函数返回时,该线程才会释放自己的资源。
    或者是设置在分离属性的情况下,一个线程结束会立即释放它所占用的资源。

    (3)pthread_join()函数

    int pthread_join(pthread_t thread, void **retval);

    args:

    pthread_t thread: 被连接线程的线程号

    void **retval : 指向一个指向被连接线程的返回码的指针的指针

    return:

    线程连接的状态,0是成功,非0是失败

    当A线程调用线程B并 pthread_join() 时,A线程会处于阻塞状态,直到B线程结束后,A线程才会继续执行下去。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。这里有三点需要注意:

    1. 被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间。

    2. 一个线程只能被一个线程所连接。

    3. 被连接的线程必须是非分离的,否则连接会出错。
      所以可以看出pthread_join()有两种作用:1-用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。2-对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。

    线程函数的返回

    方式1:return返回

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void PollPthreadJoin();
    10. void* PthreadJoin(void *ptr);
    11. int main()
    12. {
    13. PollPthreadJoin();
    14. }
    15. void PollPthreadJoin()
    16. {
    17. pthread_t pthread_join_thread_tid;
    18. int* i =0;
    19. int argc =10;
    20. pthread_create(&pthread_join_thread_tid,NULL,&PthreadJoin,(void *)&argc);
    21. pthread_join(pthread_join_thread_tid,(void **)&i);
    22. printf("return value: %d\n",i[0]);
    23. }
    24. void* PthreadJoin(void *ptr)
    25. {
    26. //int i=10;
    27. int *ret = (int *)ptr;
    28. printf("This is PrintMessage function\n");
    29. return (void*)(ret);
    30. }

    方式1:pthread_exit()返回

    (4)pthread_exit()函数

    void pthread_exit( void * value_ptr );
    线程的终止可以是调用了pthread_exit或者该线程的例程结束。也就是说,一个线程可以隐式的退出,也可以显式的调用pthread_exit函数来退出。
    pthread_exit函数唯一的参数value_ptr是函数的返回代码,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr。

    该函数是主动结束线程,而join是等待线程的结束。

    (5)pthread_cancel和pthread_exit和pthread_join的区别(释放的资源上的差异)

            在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

    1.4 重要函数

    pthread_cancel(pthread_t tid)杀死一个线程,对应进程中的kill函数;成功返回0,失败返回错误码;
    注意:线程的取消并不是实时的,需要一个取消点,而这个取消点一般是一个系统调用函数,如果线程之中没有这个取消点,可以调用pthread_testcancel函数自行设置一个取消点;`

    1.5 重要知识

    PS:玩线程一定要注意资源释放

    线程退出的几种方式和资源回收【线程编程中避免内存泄漏】 - 走看看

    今天实战:今天实战项目里,线程的退出方式是return,但是如果默认线程的创建状态是非分离态的,那么你return之后,线程里面的资源是没有被释放的。所以我在线程函数一开始的地方就进行了线程的分离设置,就解决了资源占用问题。

    pthread_testcancel函数自行设置一个取消点

    设置对pthread_cannel的响应方式:pthread_setcannelstate

    设置对pthread_cannel的取消方式:pthread_setcanneltype

    立即取消、延迟取消

    可结合的线程的几种退出方式

    1. 子线程使用return退出,主线程中使用pthread_join回收线程

    2.子线程使用pthread_exit退出,主线程中使用pthread_join接收pthread_exit的返回值,并回收线程

    3.主线程中调用pthread_cancel,然后调用pthread_join回收线程

    (下面三点还没测试的)

    线程退出有多种方式,如return,pthread_exit,pthread_cancel等;线程分为可结合的(joinable)和 分离的(detached)两种。

    1. 子线程使用return退出,主线程中使用pthread_join回收线程

    2.子线程使用pthread_exit退出,主线程中使用pthread_join接收pthread_exit的返回值,并回收线程

    3.主线程中调用pthread_cancel,然后调用pthread_join回收线程

    线程一定要是可结合状态,然后return、pthrerad_exit、pthread_cannel 三种杀死线程的方法,最终才会让pthread_join释放掉除了堆区的资源(malloc).

    二、线程高级

    2.1 互斥锁

    在线程实际运行过程中,我们经常需要多个线程保持同步。

    这时可以用互斥锁来完成任务。互斥锁的使用过程中,主要有

    1. pthread_mutex_t lock; /* 互斥锁定义 */
    2. pthread_mutex_init(&lock, NULL); /* 动态初始化, 成功返回0,失败返回非0 */
    3. pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化 */
    4. pthread_mutex_lock(&lock); /* 阻塞的锁定互斥锁 */
    5. pthread_mutex_trylock(&thread_mutex);/* 非阻塞的锁定互斥锁,成功获得互斥锁返回0,如果未能获得互斥锁,立即返回一个错误码 */
    6. pthread_mutex_unlock(&lock); /* 解锁互斥锁 */
    7. pthread_mutex_destroy(&lock) /* 销毁互斥锁 */

    这几个函数以完成锁的初始化,锁的销毁,上锁和释放锁操作。

    2.1.1 锁的创建

    锁可以被动态或静态创建,可以用宏PTHREAD_MUTEX_INITIALIZER来静态的初始化锁:

    pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;

    另外锁可以用pthread_mutex_init函数动态的创建,函数原型如下:

    int pthread_mutex_init(pthread_mutex_t*mutex, const pthread_mutexattr_t * attr)

    2.1.2 锁的属性

    互斥锁属性可以由pthread_mutexattr_init(pthread_mutexattr_t *mattr)来初始化,然后可以调用其他的属性设置方法来设置其属性。

    互斥锁的范围:可以指定是该进程与其他进程的同步还是同一进程内不同的线程之间的同步。可以设置为PTHREAD_PROCESS_SHARE和PTHREAD_PROCESS_PRIVATE。默认是后者,表示进程内使用锁。可以使用

    int pthread_mutexattr_setpshared(pthread_mutexattr_t*mattr, int pshared)

    pthread_mutexattr_getpshared(pthread_mutexattr_t*mattr,int *pshared)

    用来设置与获取锁的范围;

    互斥锁的类型:有以下几个取值空间:

    PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。 PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。 PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。 PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

    可以用 pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type) pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type)

    获取或设置锁的类型。

    2.1.3 锁的释放

    调用pthread_mutex_destory之后,可以释放锁占用的资源,但这有一个前提上锁当前是没有被锁的状态。

    2.1.4 锁操作

    对锁的操作主要包括加锁 pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个。

    int pthread_mutex_lock(pthread_mutex_t*mutex) int pthread_mutex_unlock(pthread_mutex_t *mutex) int pthread_mutex_trylock(pthread_mutex_t *mutex) pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

    2.2 信号量

    1.什么是信号量
    信号量是一种特殊的变量,访问具有原子性。
    只允许对它进行两个操作:
    1)等待信号量
    信号量值为0时,程序等待;当信号量值大于0时,信号量减1,程序继续运行。
    2)发送信号量
    将信号量值加1。

    2.2.1 sem_init函数

    int sem_init(sem_t *sem,int pshared,unsigned int value);

    该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。
    pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.

    2.2.2 sem_wait函数

    该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:

    int sem_wait(sem_t *sem);  

    2.2.3 sem_post函数

    该函数用于以原子操作的方式将信号量的值加1。它的原型如下:

    int sem_post(sem_t *sem);  

    与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1

    2.2.4 sem_destroy函数

    该函数用于对用完的信号量的清理。它的原型如下:

    int sem_destroy(sem_t *sem); 

    成功时返回0,失败时返回-1.

    首先创建信号量,第二个参数为0,表示这个信号量是当前进程的局部信号量,初始值为0.

    三、进程初级

    四、进程高级

  • 相关阅读:
    【二分图染色】ARC 165 C
    【前端】ECMAScript6从入门到进阶
    【java篇】你真的了解“基本数据类型”吗?
    不就是Java吗之数组的定义和使用
    Perl语言入门:探索Perl语法规则的基本特点
    Vitis_米联客开发板MZU07_7EG上手_1
    JS中的【函数】与【方法】之“父慈子孝”
    [附源码]Python计算机毕业设计SSM教学团队管理系统(程序+LW)
    Java 全栈知识体系
    磁盘管理 及 nfs服务配置
  • 原文地址:https://blog.csdn.net/qq_42475191/article/details/126843016