IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 线程(Linux应用编程篇) -> 正文阅读

[Java知识库]线程(Linux应用编程篇)

与线程相关的就是并发

一、线程初识

  • 线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。

  • 当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以 main()做为入口开始运行的,所以main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务

  • 任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程

  • 既然有单线程进程,那自然就存在多线程进程,所谓多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用pthread_create创建一个新的线程),那么创建的新线程就是主线程的子线程

  • 主线程的作用

    • 其它新的线程(也就是子线程)是由主线程创建的
    • 主线程通常会在最后结束运行,执行各种清理工作,譬如回收各个子线程
  • 线程的特点

    • 线程不单独存在、而是包含在进程中;
    • 线程是参与系统调度的基本单位
    • 并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果
    • 共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等
  • 线程与进程的区别
    多线程通常对于一些中小型应用程序,而多进程通常会用在一些大型应用程序项目中

    • 进程间切换开销大
    • 进程间通信较为麻烦,。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦,
    • 同一进程的多个线程间切换开销比较小
    • 同一进程的多个线程间通信容易
    • 线程创建的速度远大于进程创建的速度
    • 多线程在多核处理器上更有优势
    • 在实际的应用当中多线程远比多进程应用更为广泛,
    • 多线程编程难度高,在多线程环境下需要考虑很多的问题,例如线程安全问题、信号处理的问题等,编写与调试一个多线程程序比单线程程序困难得多
  • 线程ID

    • 像每个进程都有一个进程 ID一样,每个线程也有其对应的标识,称为线程 ID。
    • 进程ID在整个系统中是唯一的,但线程 ID不同,线程 ID 只有在它所属的进程上下文中才有意义
    • 进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数,而线程 ID 使用 pthread_t 数据类型来表示,一个线程可通过库函数 pthread_self()来获取自己的线程 ID
      /*
      @ 	头文件 #include <pthread.h> 
      @	该函数调用总是成功,返回当前线程的线程ID。
      */
      pthread_t pthread_self(void); 
      
      
      /*
      @	头文件:#include <pthread.h> 
      @	如果两个线程 ID  t1 和 t2 相等,则 pthread_equal()返回一个非零值;否则返回 0。
      */
      int pthread_equal(pthread_t t1, pthread_t t2); 
      

二、线程相关的操作函数

1、创建线程pthread_create()
  • 启动程序时,创建的进程只是一个单线程的进程,称之为初始线程或主线程,
  • 主线程可以使用库函数 pthread_create()负责创建一个新的线程, 创建出来的新线程被称为主线程的子线程
  • 线程创建成功,新线程就会加入到系统调度队列中,获取到CPU之后就会立马从 start_routine()函数开始运行该线程的任务;调用 pthread_create()函数后,通常我们无法确定系统接着会调度哪一个线程来使用CPU 资源,同步技术解决这个问题。
/*
@ 	头文件:#include <pthread.h> 
@	thread: pthread_t类型指针, 当pthread_create()成功返回时,新创建的线程的线程ID会保存在参数thread
所指向的内存中,后续的线程相关函数会使用该标识来引用此线程
@	attr:pthread_attr_t类型指针,指向pthread_attr_t类型的缓冲区,pthread_attr_t数据类型定义了线程的各种属性
@	start_routine:参数 start_routine 是一个函数指针,指向一个函数,新创建的线程从 start_routine()函数开始运行,该函数返回值类型为void *,并且该函数的参数只有一个void *,
@	arg:传递给 start_routine()函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数 arg设置为NULL,表示不需要传入参数给start_routine()函数
@	返回值:成功返回0;失败时将返回一个错误号,并且参数 thread指向的内容是不确定的。
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 

例子
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     printf("新线程: 进程ID<%d>   线程ID<%lu>\n", getpid(), pthread_self()); 
     return (void *)0; 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     int ret; 
 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "Error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     printf("主线程: 进程ID<%d>   线程ID<%lu>\n", getpid(), pthread_self()); 
     sleep(1); 
     exit(0); 
}
2、终止线程pthread_exit()
  • 终止线程的方式:
    • 线程的start函数执行 return语句并返回指定值,返回值就是线程的退出码;
    • 线程调用pthread_exit()函数;**********
    • 调用pthread_cancel()取消线程
  • 进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止
  • pthread_exit()函数将终止调用它的线程
/*
@	头文件:#include <pthread.h> 
@	参数retval的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用 pthread_join()来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join()来获取的
*/ 
void pthread_exit(void *retval); 


//==============================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     printf("新线程start\n"); 
     sleep(1); 
     printf("新线程end\n"); 
     pthread_exit(NULL); 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     int ret; 
 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "Error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     printf("主线程end\n"); 
     pthread_exit(NULL); 
     exit(0); 
} 
3、回收线程 pthread_join()
  • 在父、子进程当中,父进程可通过wait()函数(或其变体waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;
  • 而在线程当中,也需要如此,通过调用pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源
  • 调用pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则pthread_join()立刻返回。如果多个线程同时尝试调用 pthread_join()等待指定线程的终止,那么结果将是不确定的
  • 若线程并未分离,则必须使用 pthread_join()来等待线程终止,回收
    线程资源;如果线程终止后,其它线程没有调用pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程
  • 当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收。
  • 线程回收的特点pthread_join
    • 线程之间关系是对等的。进程中的任意线程均可调用pthread_join()函数来等待另一个线程的终止,如果线程A 创建了线程B,线程B再创建线程 C,那么线程A 可以调用 pthread_join()等待线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止;这与进程间层次关系不同,父进程如果使用 fork()创建了子进程,那么它也是唯一能够对子进程调用 wait()的进程,线程之间不存在这样的关系。
    • 不能以非阻塞的方式调用pthread_join()。对于进程,调用waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。
/*
@	#include <pthread.h> 
@	thread:pthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程;
@	retval:如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程 start函数中执行 return语句对应的返回值)复制到*retval所指向的内存区域;如果目标线程被 pthread_cancel()取消,则将 PTHREAD_CANCELED 放在*retval中。如果对目标线程的终止状态不感兴趣,则可将参数 retval设置为 NULL
@	返回值:成功返回0;失败将返回错误码。
*/
int pthread_join(pthread_t thread, void **retval); 

//=========================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     printf("新线程start\n"); 
     sleep(2); 
     printf("新线程end\n"); 
     pthread_exit((void *)10); 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     void *tret; 
     int ret; 
 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     ret = pthread_join(tid, &tret); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     printf("新线程终止, code=%ld\n", (long)tret); 
 
     exit(0); 
} 
4、取消线程pthread_cancel()
  • 在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start函数执行 return语句退出
  • 需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出
/*
@	#include <pthread.h> 
@	参数thread 指定需要取消的目标线程
@	成功返回0,失败将返回错误码。
@	发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出
*/
int pthread_cancel(pthread_t thread); 

//==================================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     printf("新线程--running\n"); 
     for ( ; ; ) 
         sleep(1); 
     return (void *)0; 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     void *tret; 
     int ret; 
 
     /* 创建新线程  */ 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     sleep(1); 
 
     /* 向新线程发送取消请求  */ 
     ret = pthread_cancel(tid); 
     if (ret) { 
         fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     /* 等待新线程终止  */ 
     ret = pthread_join(tid, &tret); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     printf("新线程终止, code=%ld\n", (long)tret); 
 
     exit(0); 
} 
  • 取消状态以及类型,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者控制如何被取消
/*
@	头文件:#include <pthread.h> 
@	,pthread_setcancelstate()函数会将调用线程的取消性状态设置
为参数 state 中给定的值,并将线程之前的取消性状态保存在参数 oldstate 指向的缓冲区中,如果对之前的状态不感兴趣,Linux允许将参数 oldstate设置为NULL;pthread_setcancelstate()调用成功将返回0,失败返回非 0值的错误码。
@	pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,
	PTHREAD_CANCEL_ENABLE:线程可以取消
	PTHREAD_CANCEL_DISABLE:线程不可被取消
*/
int pthread_setcancelstate(int state, int *oldstate); 

//====================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     /* 设置为不可被取消  */ 
     pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); 
 
     for ( ; ; ) { 
         printf("新线程--running\n"); 
         sleep(2); 
     } 
     return (void *)0; 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     void *tret; 
     int ret; 
 
     /* 创建新线程  */ 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     sleep(1); 
 
     /* 向新线程发送取消请求  */ 
     ret = pthread_cancel(tid); 
     if (ret) { 
         fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     /* 等待新线程终止  */ 
     ret = pthread_join(tid, &tret); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     printf("新线程终止, code=%ld\n", (long)tret); 
 
     exit(0); 
} 



/*
@	pthread_setcanceltype()函数 
@	如果线程的取消性状态为PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用 pthread_setcanceltype()函数来设置,它的参数 type 指定了需要设置的类型,而线程之前的取消性类型则会保存在参数 oldtype 所指向的缓冲区中,如果对之前的类型不敢兴趣,Linux下允许将参数 oldtype 设置为 NULL。同样 pthread_setcanceltype()函数调用成功将返回 0,失败返回非 0 值的错误码
@	pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作
	PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point,将在 12.6.3 小节介绍)为止
	PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)
取消线程
*/
int pthread_setcanceltype(int type, int *oldtype); 
  • 取消点, 若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用那什么是取消点呢?所谓取消点其实就是一系列函数, 当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,但
    没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生

5、分离线程pthread_detach()

  • 默认情况下,当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用pthread_detach()将指定线程进行分离,也就是分离线程
  • 处于分离状态的线程,当其终止后,能够自动回收线程资源
/*
@	#include <pthread.h>
@	参数 thread指定需要分离的线程
@	函数 pthread_detach()调用成功将返回 0;失败将返回一个错误码
@	一个线程既可以将另一个线程分离,同时也可以将自己分离
	pthread_detach(pthread_self()); 
@	一旦线程处于分离状态,就不能再使用pthread_join()来获取其终止状态
@	处于分离状态的线程,当其终止后,能够自动回收线程资源
*/
int pthread_detach(pthread_t thread); 

//====================================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     int ret; 
 
     /* 自行分离  */ 
     ret = pthread_detach(pthread_self()); 
     if (ret) {
         fprintf(stderr, "pthread_detach error: %s\n", strerror(ret)); 
         return NULL; 
     } 
 
     printf("新线程start\n"); 
     sleep(2);    //休眠2秒钟 
     printf("新线程end\n"); 
     pthread_exit(NULL); 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     int ret; 
 
     /* 创建新线程  */ 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     sleep(1);    //休眠1秒钟 
 
     /* 等待新线程终止  */ 
     ret = pthread_join(tid, NULL); 
     if (ret) 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
 
     pthread_exit(NULL); 
} 

三、线程有关问题

1、注册线程清理处理函数
  • 使用atexit()函数注册进程终止处理函数,当进程调用 exit()退出时就会执行进程终止处理函数;其实,当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数,我们把这个称为线程清理函数(thread cleanup handler)
  • 与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构,也就是说它们的执行顺序与注册(添加)顺序相反,当执行完所有清理函数后,线程终止
  • 线程通过函数pthread_cleanup_push()和pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加和移除清理函数
/*
@	#include <pthread.h> 
@	第一个参数 routine 是一个函数指针,指向一个需要添加的清理函数
@	routine()函数无返回值,只有一个void *类型参数
@	第二个参数 arg,当调用清理函数 routine()时,将arg 作为routine()函数的参数
*/
void pthread_cleanup_push(void (*routine)(void *), void *arg); 

/*
@	#include <pthread.h> 
@	既然有添加,自然就会伴随着删除,就好比对应入栈和出栈,调用函数pthread_cleanup_pop()可以将清理函数栈中最顶层(也就是最后添加的函数,最后入栈)的函数移除
@	当线程执行以下动作时,清理函数栈中的清理函数才会被执行:
	线程调用pthread_exit()退出时;
	线程响应取消请求时;
	用非0参数调用pthread_cleanup_pop() 
@
*/
void pthread_cleanup_pop(int execute); 


//=======================================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
static void cleanup(void *arg) 
{ 
     printf("cleanup: %s\n", (char *)arg); 
} 
 
static void *new_thread_start(void *arg) 
{ 
     printf("新线程--start run\n"); 
     pthread_cleanup_push(cleanup, "第1次调用"); 
     pthread_cleanup_push(cleanup, "第2次调用"); 
     pthread_cleanup_push(cleanup, "第3次调用"); 
 
     sleep(2); 
     pthread_exit((void *)0);     //线程终止 
 
     /* 为了与pthread_cleanup_push 配对,不添加程序编译会通不过  */ 
     pthread_cleanup_pop(0); 
     pthread_cleanup_pop(0); 
     pthread_cleanup_pop(0); 
} 
 
int main(void) 
{ 
     pthread_t tid; 
     void *tret; 
     int ret; 
 
     /* 创建新线程  */ 
     ret = pthread_create(&tid, NULL, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     /* 等待新线程终止  */ 
     ret = pthread_join(tid, &tret); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
     printf("新线程终止, code=%ld\n", (long)tret); 
 
     exit(0); 
} 

  • 将新线程中调用的pthread_exit()替换为 return,在进行测试,发现并不会执行清理函数
2、线程属性
  • 调用 pthread_create()创建线程,可对新建线程的各种属性进行设置。在 Linux 下,使用pthread_attr_t数据类型定义线程的所有属性
  • 调用pthread_create()创建线程时,参数 attr设置为NULL,表示使用属性的默认值创建线程
  • 如果不使用默认值,参数 attr 必须要指向一个 pthread_attr_t 对象,而不能使用 NULL。当定义 pthread_attr_t 对象之后,需要使用 pthread_attr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用pthread_attr_destroy()函数将其销毁
/*
@	#include <pthread.h> 
@	参数 attr指向一个pthread_attr_t对象,即需要进行初始化的线程属性对象
@	在调用成功时返回 0,失败将返回一个非 0 值的错误码
@	调用 pthread_attr_init()函数会将指定的 pthread_attr_t 对象中定义的各种线程属性初始化为它们各自对应的默认值
*/ 
int pthread_attr_init(pthread_attr_t *attr); 
int pthread_attr_destroy(pthread_attr_t *attr); 
  • pthread_attr_t数据结构中包含的属性比较多,可能比较关注属性包括:线程栈
    的位置和大小、线程调度策略和优先级,以及线程的分离状态属性等。
  • **线程栈属性 **
    每个线程都有自己的栈空间,pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小,pthread_attr_getstack()可以获取这些信息,函数 pthread_attr_setstack()对栈起始地址和栈大小进行设置,
/*
@	#include <pthread.h> 
@	attr:参数attr指向线程属性对象。 
@	stackaddr:调用pthread_attr_getstack()可获取栈起始地址,并将起始地址信息保存在*stackaddr中; 
@	stacksize:调用pthread_attr_getstack()可获取栈大小,并将栈大小信息保存在参数 stacksize所指向的内存中;
@	返回值:成功返回0,失败将返回一个非 0值的错误码。
*/
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 


/*
@	#include <pthread.h> 
@	attr:参数attr指向线程属性对象。
@	stackaddr:设置栈起始地址为指定值。
@	stacksize:设置栈大小为指定值;
@	返回值:成功返回0,失败将返回一个非 0值的错误码。 
*/

int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 

//==如果想单独获取或设置栈大小、栈起始地址,可以使用下面这些函数: 
/*
@	#include <pthread.h> 
*/
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize); 
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); 
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr); 

//============================================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
 
static void *new_thread_start(void *arg) 
{ 
     puts("Hello World!"); 
     return (void *)0; 
} 
 
int main(int argc, char *argv[]) 
{ 
     pthread_attr_t attr; 
     size_t stacksize; 
     pthread_t tid; 
     int ret; 
 
     /* 对attr对象进行初始化  */ 
     pthread_attr_init(&attr); 
 
     /* 设置栈大小为4K */ 
     pthread_attr_setstacksize(&attr, 4096); 
 
     /* 创建新线程  */ 
     ret = pthread_create(&tid, &attr, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     /* 等待新线程终止  */ 
     ret = pthread_join(tid, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); 
         exit(-1); 
              } 
 
     /* 销毁attr对象  */ 
     pthread_attr_destroy(&attr); 
     exit(0); 
} 
  • 分离状态属性
    • 如果对现已创建的某个线程的终止状态不感兴趣,可以使用
      pthread_detach()函数将其分离,那么该线程在退出时,操作系统会自动回收它所占用的资源
    • 如果我们在创建线程时就确定要将该线程分离, 可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始运行就处于分离状态。调用函数 pthread_attr_setdetachstate()设置 detachstate 线程属性,调用
      pthread_attr_getdetachstate()获取 detachstate线程属性
/*
@	#include <pthread.h> 
@	参数attr指向 pthread_attr_t对象
@	参数detachstate取值如下: 
	PTHREAD_CREATE_DETACHED: 新建线程一开始运行便处于分离状态, 以分离状态启动线程
	PTHREAD_CREATE_JOINABLE:这是 detachstate 线程属性的默认值,正常启动线程,可以被其它线程获取终止状态信息。
*/
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 

/*
@	获取 detachstate 线程属性,将 detachstate 线程属性保存在参数detachstate 所指定的内存中
*/
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 

//=======================================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <string.h> 
#include <unistd.h> 
 
static void *new_thread_start(void *arg) 
{ 
     puts("Hello World!"); 
     return (void *)0; 
} 
 
int main(int argc, char *argv[]) 
{ 
     pthread_attr_t attr; 
     pthread_t tid; 
     int ret; 
 
     /* 对attr对象进行初始化  */ 
     pthread_attr_init(&attr); 
 
     /* 设置以分离状态启动线程  */ 
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 
 
     /* 创建新线程  */ 
     ret = pthread_create(&tid, &attr, new_thread_start, NULL); 
     if (ret) { 
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); 
         exit(-1); 
     } 
 
     sleep(1); 
 
     /* 销毁attr对象  */ 
     pthread_attr_destroy(&attr); 
     exit(0); 
} 
3、线程安全==重要

** 线程栈 **

  • 进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈
  • 譬如主线程调用 pthread_create()创建了一个新的线程,那么这个新的线程有它自己独立的栈地址空间、而主线程也有它自己独立的栈地址空间。
  • 既然每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量)都是分配在自己的线程栈中的,它们不会相互干扰
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
 
static void *new_thread(void *arg) 
{ 
     int number = *((int *)arg); 
     unsigned long int tid = pthread_self(); 
     printf("当前为<%d>号线程, 线程ID<%lu>\n", number, tid); 
     return (void *)0; 
} 
 
static int nums[5] = {0, 1, 2, 3, 4}; 
 
int main(int argc, char *argv[]) 
{ 
     pthread_t tid[5]; 
     int j; 
 
     /* 创建5个线程  */ 
     for (j = 0; j < 5; j++) 
         pthread_create(&tid[j], NULL, new_thread, &nums[j]); 
 
     /* 等待线程结束  */ 
     for (j = 0; j < 5; j++) 
         pthread_join(tid[j], NULL);//回收线程 
 
     exit(0); 
}

可重入函数

  • 要解释可重入(Reentrant)函数为何物,首先需要区分单线程程序和多线程程序。
  • 单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;而对于
    多线程程序而言,同一进程却存在多条独立、并发的执行流进程中执行流的数量除了与线程有关之外,与信号处理也有关联。因为信号是异步的,进程可能会在其运行过程中的任何时间点收到信号,进而跳转、执行信号处理函数,从而在一个单线程进程 (包含信号处理)中形成了两条(即主程序和信号处理函数)独立的执行流
  • 如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数
  • 重入指的是同一个函数被不同执行流调用,前一个执行流还没有执行完该函数、另一个执行流又开始调用该函数了,其实就是同一个函数被多个执行流并发/并行调用,在宏观角度上理解指的就是被多个执行流同时调用。
  • 信号与可重入问题
#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h> 
 
static void func(void) 
{ 
     /*...... */ 
} 
 
static void sig_handler(int sig) 
{ 
     func(); 
} 
 
int main(int argc, char *argv[]) 
{ 
     sig_t ret = NULL; 
 
     ret = signal(SIGINT, (sig_t)sig_handler); 
     if (SIG_ERR == ret) { 
         perror("signal error"); 
         exit(-1); 
     } 
 
     /* 死循环  */ 
     for ( ; ; ) 
         func(); 
 
     exit(0); 
  • 当main()函数正在执行 func()函数代码,此时进程收到了 SIGINT信号,便会打断当前正常执行流程、跳转到 sig_handler()函数执行,进而调用 func、执行 func()函数代码;这里就出现了主程序与信号处理函数并发调用 func()的情况
    在这里插入图片描述
  • 上面的代码是单线程的进程。在信号处理函数中,执行完 func()之后,信号处理函数退出、返回到主程序流程,也就是被信号打断的位置处继续运行。如果每次出现这种情况执行 func()函数都能产生正确的结果,那么 func()函数就是一个可重入函数
  • 以上举例说明了函数被多个执行流同时调用的两种情况:
    • 在一个含有信号处理的程序当中, 主程序正执行函数 func(),此时进程接收到信号, 主程序被打断,跳转到信号处理函数中执行,信号处理函数中也调用了 func()。
    • 在多线程环境下,多个线程并发调用同一个函数
  • 需要注意不可重入函数的问题,如果多条
    执行流同时调用一个不可重入函数则可能会得不到预期的结果、甚至有可能导致程序崩溃
  • 可重入的分类
    • 绝对的可重入函数:所谓绝对,指的是该函数不管如何调用,都刚断言它是可重入的,都能得到预期的结果。
    • 带条件的可重入函数:指的是在满足某个/某些条件的情况下,可以断言该函数是可重入的,不管怎么调用都能得到预期的结果。

线程安全函数

  • 一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。
    在这里插入图片描述
  • 譬如下面这个函数是一个不可重入函数,同样也是一个线程不安全函数
static int glob = 0; 
 
static void func(int loops) 
{ 
  int local; 
  int j; 
 
  for (j = 0; j < loops; j++) { 
    local = glob; 
    local++; 
    glob = local; 
  } 
} 
  • 如果对该函数进行修改,使用线程同步技术(譬如互斥锁)对共享变量 glob 的访问进行保护,在读写该变量之前先上锁、读写完成之后在解锁。这样,该函数就变成了一个线程安全函数,但是它依然不是可重入函数,因为该函数更改了外部全局变量的值
  • 可重入函数只是单纯从语言语法角度分析它的可重入性质,不涉及到一些具体的实现机制,譬如线程同步技术,这是判断可重入函数和线程安全函数的区别,因为你单从概念上去分析的话,其实可以发现可重入函数和线程安全函数好像说的是同一个东西,“一个函数被多个线程同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数”,多个线程指的就是多个执行流(不包括信号处理函数执行流),所以从这里看跟可重入函数的概念是很相似的
  • 判断一个函数是否为线程安全函数的方法是, 该函数被多个线程同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个线程安全函数。判读一个函数是否为可重入函数的方法是,从语言语法角度分析,该函数被多个执行流同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个可重入函数。

一次性初始化

  • 在多线程编程环境下,有些代码段只需要执行一次,譬如一些初始化相关的代码段,通常比较容易想到的就是将其放在 main()主函数进行初始化,这样也就是意味着该段代码只在主线程中被调用,只执行过一次
#include <pthread.h> 
/*
@	在多线程编程环境下,尽管 pthread_once()调用会出现在多个线程中,但该函数会保证 init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。
@	once_control:这是一个 pthread_once_t类型指针,在调用 pthread_once()函数之前,我们需要定义了一个 pthread_once_t类型的静态变量,调用pthread_once()时参数once_control指向该变量
@	init_routine:一个函数指针,参数 init_routine 所指向的函数就是要求只能被执行一次的代码段,pthread_once()函数内部会调用init_routine(),即使pthread_once()函数会被多次执行,但它能保证init_routine()仅被执行一次。
@	返回值:调用成功返回 0;失败则返回错误编码以指示错误原因。
@	如果在一个线程调用 pthread_once()时,另外一个线程也调用了 pthread_once,则该线程将会被阻塞等待,直到第一个完成初始化后返回。换言之,当调用 pthread_once成功返回时,调用总是能够肯定所有的状态已经初始化完成了。
*/
pthread_once_t once_control = PTHREAD_ONCE_INIT; 
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

//=====================================================================
#include <stdio.h> 
#include <stdlib.h> 
#include <pthread.h> 
 
static pthread_once_t once = PTHREAD_ONCE_INIT; 
 
static void initialize_once(void) 
{ 
     printf("initialize_once被执行: 线程ID<%lu>\n", pthread_self()); 
} 
 
static void func(void) 
{ 
     pthread_once(&once, initialize_once);//执行一次性初始化函数 
     printf("函数func执行完毕.\n"); 
} 
 
static void *thread_start(void *arg) 
{ 
     printf("线程%d被创建:  线程ID<%lu>\n", *((int *)arg), pthread_self()); 
     func();      //调用函数 func 
     pthread_exit(NULL); //线程终止 
} 
 
static int nums[5] = {0, 1, 2, 3, 4}; 
 
int main(void) 
{ 
     pthread_t tid[5]; 
     int j; 
 
     /* 创建5个线程  */ 
     for (j = 0; j < 5; j++) 
         pthread_create(&tid[j], NULL, thread_start, &nums[j]); 
 
     /* 等待线程结束  */ 
     for (j = 0; j < 5; j++) 
         pthread_join(tid[j], NULL);//回收线程
 
     exit(0); 
} 

** 线程特有数据 **

  • 线程特有数据也称为线程私有数据, 简单点说,就是为每个调用线程分别维护一份变量的副本(copy) ,每个线程通过特有数据键(key)访问时,这个特有数据键都会获取到本线程绑定的变量副本。这样就可以避免变量成为多个线程间的共享数据。
    ** 线程局部存储**
  • 通常情况下,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量;而线程局部存储在定义全局或静态变量时,使用__thread 修饰符修饰变量,此时,每个线程都会拥有一份对该变量的拷贝。线程局部存储中的变量将一直存在,直至线程终止,届时会自动释放这一存储
  • 线程局部存储的主要优点在于,比线程特有数据的使用要简单。要创建线程局部变量,只需简单地在全局或静态变量的声明中包含__thread修饰符即可
  • 关于线程局部变量的声明和使用,需要注意以下几点:
    • 如果变量声明中使用了关键字 static或 extern,那么关键字__thread必须紧随其后
    • 与一般的全局或静态变量申明一眼,线程局部变量在申明时可设置一个初始值
    • 可以使用C 语言取值操作符(&)来获取线程局部变量的地址
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 
 
static __thread char buf[100]; 
 
static void *thread_start(void *arg) 
{ 
     strcpy(buf, "Child Thread\n"); 
     printf("子线程: buf (%p) = %s", buf, buf); 
     pthread_exit(NULL); 
} 
 
int main(int argc, char *argv[]) 
{ 
     pthread_t tid; 
     int ret; 
 
     strcpy(buf, "Main Thread\n"); 
 
     /* 创建子线程  */ 
     if (ret = pthread_create(&tid, NULL, thread_start, NULL)) { 
         fprintf(stderr, "pthread_create error: %d\n", ret); 
         exit(-1); 
     } 
 
     /* 等待回收子线程  */ 
     if (ret = pthread_join(tid, NULL)) { 
         fprintf(stderr, "pthread_join error: %d\n", ret); 
         exit(-1); 
     } 
 
     printf("主线程: buf (%p) = %s", buf, buf); 
     exit(0); 
}

参考资料:正点原子linuxC应用编程

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 17:27:33  更:2022-04-18 17:29:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:53:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码