线程的概念
线程是指程序中的一条执行路径。在一个进程中,至少有一个线程,称为主线程,通过主线程可以派生出其他子线程。
Linux系统内核只提供了轻量级进程(light-weight process)的支持,并未实现线程模型。Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。进程是最小的资源分配单位,而线程是最小的CPU执行单位。它们之间的关系如下:
一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表等一些数据结构初始化。后续派生出来的线程,并不会创建地址空间,而只是创建进程控制块(task_struct),它们共用一个地址空间。
每创建一个 task_struct,就对应一条执行流,CPU根据调度程序依次执行。
线程间共享的资源:
- 文件描述符表
- 当前工作目录
- 用户ID和组ID
- 内存地址空间
线程间非共享资源:
- 线程id
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量,虽然子线程也可以访问errno变量,但会发生竞争,因此不建议在子线程中使用。
- 信号屏蔽字
- 调度优先级
线程的创建和使用
在Linux中,线程用pthread_t 类型描述。该类型是一个不透明类型,不同的线程库可能会以不同的方式实现它。在 POSIX 标准中,pthread_t 类型实质就是一个整数,可以将其看做是线程 ID。它只在当前进程中保证是唯一。开发人员不必了解内部实现,执行通过系统提供的API完成相应功能即可。
创建线程-pthread_create()函数
pthread_create()函数用于创建一个线程,该函数定义如下:
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数说明
- thread:一个 pthread_t 类型的指针,用于存储新线程的标识符。在成功创建新线程后,该标识符会被填充。
- attr:一个指向 pthread_attr_t 类型的结构体的指针,用于设置新线程的属性。可以传入 NULL,表示使用默认属性。
- start_routine:一个函数指针,指向新线程的入口函数。新线程将从该函数开始执行。
- arg:传递给新线程入口函数的参数。
返回值
- 如果函数执行成功,函数返回0。
- 如果函数执行失败,函数返回一个非零的错误码。
参数start_routine 是一个函数指针,它接收一个参数,是通过 pthread_create() 函数的 arg 参数传递给它。该参数的类型为 void *,这个指针按什么类型解释由调用者自己定义。start_routine 函数执行完返回时,这个线程就就结束了。
获取线程标识-pthread_self()函数
pthread_self() 函数用于返回当前线程的线程标识符(ID)。它返回调用线程的唯一标识符,可以用于在程序中区分不同的线程。该函数定义如下:
#include <pthread.h> pthread_t pthread_self(void);
返回值
- 返回当前线程的 pthread_t 类型的标识符。
线程错误值
由于线程函数出错时,并不会设置errno,而是直接返回错误值,如果需要获取错误信息,则需要使用 strerror() 函数。由于 strerror() 函数是一个不可重入函数,当多个线程同时往标准错误输出信息时,会造成竞争冒险。strerror() 函数打印信息会紊乱,这个使用需要使用 strerror_r() 函数来打印出错信息。该函数定义如下:
#include <string.h> char *strerror(int errnum); //不可重入 int strerror_r(int errnum, char* buf, size_t buflen); char* strerror_r(int errnum, char* buf, size_t buflen);
参数说明
- errnum:错误码。
- buf:用于存储错误信息的缓冲区。
- buflen:缓冲区的大小。
返回值
- 当返回值为char*时,函数返回一个指向错误信息字符串的指针,该字符串通常包含有关错误的简短描述。
- 当返回值为int时,返回值为 0 表示成功,如果在复制错误信息时发生错误,则返回一个非零值。
线程退出
子线程的退出有两种方式,一是调用 return 从线程入口函数中结束。入口函数的返回值类型是void*,可以通过返回值返回线程的执行结果。第二种方式则是调用pthread_exit() 函数,调用该函数,当前进程会立刻终止执行。该函数定义如下:
#include <pthread.h> void pthread_exit(void *retval);
参数说明
- errnum:参数是一个指向线程的返回值的指针,该返回值会被传递给线程的创建者。
需要注意和 exit() 函数的区别,在任何线程中调用 exit() 函数都会导致当前进程退出。因此,在退出子线程时,一定不要使用 exit() 函数。
当线程函数运行结束或调用 pthread_exit() 函数结束线程,线程会停止运行。但不会释放线程所占用的堆栈和线程描述符,需要主线程进行资源回收。
线程回收
linux线程执行有两种状态,joinable 状态和 unjoinable 状态。
- joinable 状态:当线程退出时,线程资源不会自动释放,需要主线程回收。
- unjoinable 状态:线程资源会在线程结束时,自动被回收。
joinable 状态-pthread_join() 函数
pthread_join() 函数用于等待子线程结束,获取该线程的返回值,并回收线程资源。该函数定义如下:
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
参数说明
- thread:要等待终止的线程的标识符。
- retval:一个指向指针的指针,用于存储线程的返回值。若不需要线程的返回值,可以传入NULL。
返回值
- 如果线程已经正常终止,返回值为 0。
- 如果发生错误,返回值为非 0。
注意事项
- 当调用 pthread_join() 函数时,调用线程会一直阻塞,直到目标线程终止为止。一旦目标线程终止,调用线程将会恢复执行,并通过 retval 参数获取目标线程的返回值。
- 如果目标线程在调用 pthread_join() 函数之前已经终止并且没有被其他线程 join ,那么调用 pthread_join() 函数会立即返回 0。
- 对同一个线程多次调用 pthread_join() 函数,除了第一次会获取线程的返回值外,后续的调用会返回错误码 ESRCH(表示没有找到指定的线程)。
示例-获取返回值
1 #include<stdio.h> 2 #include<pthread.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 7 void showError(const char* str, int nErr) 8 { 9 char pBuf[1024] = {0}; 10 strerror_r(nErr, pBuf, sizeof(pBuf)); 11 printf("%s : %s\n", str, pBuf); 12 } 13 14 void* threadEntry(void* arg) 15 { 16 int* pVal = (int*)malloc(sizeof(int)); 17 printf("address : %p\n", pVal); 18 *pVal = 10; 19 //pthread_exit((void*)pVal); 20 return (void*)pVal; 21 } 22 23 int main(int agc, char** argv) 24 { 25 pthread_t th1; 26 int nRet = pthread_create(&th1, NULL, threadEntry, NULL); 27 if (nRet != 0) 28 { 29 showError("pthread_create", nRet); 30 exit(-1); 31 } 32 33 void* pthread_return_value; 34 pthread_join(th1, &pthread_return_value); 35 printf("address : %p\n", pthread_return_value); 36 printf("Thread return value : %d\n", *(int*)pthread_return_value); 37 return 0; 38 }
注意,Linux 多线程通过 libpthread 线程函数库来实现。在编译多线程程序的时候,需要链接libpthread,比如:
gcc main.c -lpthread
输出:
address : 0x7fbfb4000b70 address : 0x7fbfb4000b70 Thread return value : 10
unjoinable 状态-pthread_detach()函数
pthread_detach() 函数用于将一个线程的资源标记为可被系统自动回收,而不需要显式地调用 pthread_join() 等待线程结束。该函数定义如下:
#include <pthread.h> int pthread_detach(pthread_t thread);
参数说明
- thread:要被标记线程。
返回值
- 如果函数执行成功,返回0。
- 如果函数执行失败,返回非0值。
当某个线程被标记为可被回收后,其资源将在其运行结束之后立即被操作系统回收。这意味着线程结束时不会保留任何退出状态,主线程也无法获取线程返回值。
取消线程
pthread_cancel()函数
pthread_cancel() 函数是用来请求取消另一个线程的执行。一旦一个线程被取消了,它就会立即停止执行。该函数定义如下:
#include <pthread.h> int pthread_cancel(pthread_t thread);
参数说明
- thread:要被取消的线程。
返回值
- 如果函数执行成功,返回0。
- 如果函数执行失败,返回非0值。
示例
1 #include<stdio.h> 2 #include<pthread.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 7 void* threadEntry(void* arg) 8 { 9 int i = 0; 10 while(1) 11 { 12 printf("child thread : %d\n", i++); 13 sleep(1); 14 } 15 return NULL; 16 } 17 18 int main(int argc, char** argv) 19 { 20 pthread_t th1; 21 pthread_create(&th1, NULL, threadEntry, NULL); 22 sleep(3); 23 int ret = pthread_cancel(th1); 24 if(ret != 0) 25 { 26 printf("failed to cancel thread, error : %d\n", ret); 27 return -1; 28 } 29 printf("cancel thread success!\n"); 30 pause(); 31 return 0; 32 }
输出:
child thread : 0 child thread : 1 child thread : 2 cancel thread success!
实际上,取消只是向线程发送一个请求,系统并不会马上关闭被取消线程,是将该请求其记录在PCB中,只有在被取消线程下次发生系统调用时,才会真正结束线程。如果线程在执行期间没有发生系统调用,该线程不会结束:
1 #include<stdio.h> 2 #include<pthread.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 7 void* threadEntry(void* arg) 8 { 9 int i = 0; 10 while(1) 11 { 12 //printf("child thread : %d\n", i++); 13 //sleep(1); 14 } 15 return NULL; 16 } 17 18 int main(int argc, char** argv) 19 { 20 pthread_t th1; 21 pthread_create(&th1, NULL, threadEntry, NULL); 22 sleep(3); 23 printf("send cacel to thread\n"); 24 int ret = pthread_cancel(th1); 25 if(ret != 0) 26 { 27 printf("failed to cancel thread, error : %d\n", ret); 28 return -1; 29 } 30 printf("cancel thread success!\n"); 31 pause(); 32 return 0; 33 }
输出:
$ ./a.out send cacel to thread cancel thread success!
在启动一个终端,输入 ps - eLf 命令:
$ ps -eLf | grep "./a.out" dxq 30078 18071 30078 0 2 13:29 pts/1 00:00:00 ./a.out dxq 30078 18071 30079 99 2 13:29 pts/1 00:01:19 ./a.out
在while循环中没有执行任何系统调用相关的API,子线程并没有退出。为了避免出现上述问题,系统提供了 pthread_testcancel() 函数,用来检测线程是否被取消。
pthread_testcancel() 函数
pthread_testcancel() 函数用于检查当前线程是否收到了取消请求。当确认收到取消请求时,立即取消线程的执行。相比较其他系统调用,该函数开销更小。该函数定义如下:
#include<pthread.h> void pthread_testcancel(void);
将上面 threadEntry() 函数修改:
1 void* threadEntry(void* arg) 2 { 3 int i = 0; 4 while(1) 5 { 6 pthread_testcancel(); 7 } 8 return NULL; 9 }
再次启动程序,通过ps命令查看,子线程已经被回收。
线程比较-pthread_equal() 函数
pthread_equal() 函数用于比较两个线程标识符,判断它们是否引用同一个线程。该函数定义如下:
#include<pthread.h> int pthread_equal(pthread_t t1, pthread_t t2);
参数说明
- t1:线程1。
- t2:线程2。
返回值
- 如果两个线程相等,返回非0值。
- 如果两个线程不相等,返回0值。
线程属性
之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
typedef struct { int detachstate; //线程的分离状态 int schedpolicy; //线程调度策略 struct sched_param schedparam; //线程的调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; //线程的栈设置 void* stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小 }pthread_attr_t
注:目前线程属性在内核中不是直接这么定义的,这边只做简单描述。
上面的属性中,真正拿来操作一般是 线程的分离状态、线程调度策略、线程栈的大小。其他项基本上不会去修改。
线程属性初始化-pthread_attr_init() 函数
pthread_attr_init() 函数用来初始化线程的属性,该函数定义如下:
#include <pthread.h> int pthread_attr_init(pthread_attr_t *attr);
参数说明
- attr:指向 pthread_attr_t 数据类型的指针,用来保存线程属性。
返回值
- 如果初始化成功,返回值为 0。
- 如果初始化失败,返回错误代码。
销毁线程属性-pthread_attr_destroy() 函数
pthread_attr_destroy() 函数用来销毁线程属性。该函数定义如下:
#include <pthread.h> int pthread_attr_destroy(pthread_attr_t *attr);
参数说明
- attr:指向 pthread_attr_t 数据类型的指针,表示要销毁的线程属性对象。
返回值
- 如果初始化成功,返回值为 0。
- 如果初始化失败,返回错误代码。
获取线程属性-pthread_getattr_np() 函数
pthread_getattr_np() 函数是一个 Linux 特有的函数,用于获取指定线程的线程属性。该函数定义如下:
#include <pthread.h> int pthread_getattr_np(pthread_t thread, pthread_attr_t *attr);
参数说明
- thread:要获取线程属性的线程的线程 ID。
- attr:用于存储线程属性的 pthread_attr_t 结构体对象的指针。
返回值
- 如果初始化成功,返回值为 0。
- 如果初始化失败,返回错误代码。
线程的分离状态
线程的状态分为分离状态和非分离状态。
- 非分离状态:线程的默认属性是非分离状态,在这种状态下,主线程需要等待子线程运行结束。只有 pthread_join() 函数返回时,子线程才算结束,进而释放线程资源。
- 分离状态:分离线程没有被其他的线程所等待,当线程运行结束,操作系统马上回收系统资源。
可以通过线程属性设置线程分离状态:
#include <pthread.h> int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
参数说明
- attr:指向 pthread_attr_t 数据类型的指针,表示已被初始化的线程属性。
- detachstate:线程分离状态的参数值,有如下取值:
- PTHREAD_CREATE_DETACHED(1):表示分离状态。
- PTHREAD_CREATE_JOINABLE(0):表示非分离状态。
返回值
- 如果初始化成功,返回值为 0。
- 如果初始化失败,返回错误代码。
示例:
1 #include <pthread.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <unistd.h> 6 7 void *threadEntry(void *arg) { 8 int i = 0; 9 while (1) 10 { 11 printf("child thread : %d\n", ++i); 12 sleep(1); 13 } 14 return NULL; 15 } 16 17 int main(int argc, char **argv) { 18 pthread_t th1; 19 pthread_attr_t attr; 20 pthread_attr_init(&attr); 21 pthread_attr_setdetachstate(&attr, 1); 22 pthread_create(&th1, &attr, threadEntry, NULL); 23 int i = 0; 24 while(1) 25 { 26 printf("main thread : %d\n", ++i); 27 sleep(1); 28 } 29 pthread_attr_destroy(&attr); 30 pause(); 31 return 0; 32 }
输出:
main thread : 1 child thread : 1 child thread : 2 main thread : 2 child thread : 3 main thread : 3 ...
在实际工作中,更推荐使用属性的方式设置分离态。当线程运行时间很短时,可能在刚创建完线程,线程就已经执行完毕了。如果此时再去调用 pthread_detach() 函数,则会导致线程资源没有被回收,造成资源泄露。
线程的栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用。当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
设置栈大小
pthread_attr_getstacksize() 函数和pthread_attr_setstacksize() 函数提供了相关设置,函数定义如下:
#include <pthread.h> int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
参数说明
- attr:指向 pthread_attr_t 数据类型的指针,表示已被初始化的线程属性。
- stacksize:栈大小,默认大小为8M。
返回值
- 如果初始化成功,返回值为 0。
- 如果初始化失败,返回错误代码。
示例,获取栈默认大小:
1 #include <pthread.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <unistd.h> 6 7 void *threadEntry(void *arg) 8 { 9 size_t stackSize = 0; 10 pthread_attr_t attr; 11 pthread_getattr_np(pthread_self(), &attr); 12 pthread_attr_getstacksize(&attr, &stackSize); 13 printf("stack size : %ld\n", stackSize); 14 return NULL; 15 } 16 17 int main(int argc, char **argv) 18 { 19 pthread_t th1; 20 pthread_create(&th1, NULL, threadEntry, NULL); 21 pthread_join(th1, NULL); 22 pause(); 23 return 0; 24 }
输出:
stack size : 8388608
栈大小在 ulimit 中也有设置,默认大小为8M,可以通过如下命令查看:
$ ulimit -s 8192
同时,由于线程被视作轻量级进程,即使虚拟内存足够大,线程的个数也不能超过用户最大进程数。可以通过 ulimit -u 查看最大个数:
$ ulimit -u 7823
设置栈空间
除上述对栈设置的函数外,还有以下两个函数可以获取和设置线程栈属性,当进程栈地址空间不够用时,指定新建线程使用 malloc 分配的空间作为自己的栈空间。函数定义如下:
#include <pthread.h> //设置栈空间 int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); //获取栈空间 int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
参数说明
- attr:指向线程属性对象的指针。
- stackaddr:栈空间地址。如果在设置栈空间时,传入NULL,则表示由系统自动分配。
- stacksize:栈大小。
返回值
- 如果初始化成功,返回值为 0。
- 如果初始化失败,返回错误代码。
示例,设置线程栈空间并求阶乘:
1 #include <pthread.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <unistd.h> 6 const int stackSize = 4 * 1024 * 1024; 7 8 long long calc(int n) 9 { 10 if(n == 1) 11 return 1; 12 13 return n * calc(n - 1); 14 } 15 16 void *threadEntry(void *arg) 17 { 18 size_t stackSize = 0; 19 pthread_attr_t attr; 20 pthread_getattr_np(pthread_self(), &attr); 21 pthread_attr_getstacksize(&attr, &stackSize); 22 printf("stack size : %ld\n", stackSize); 23 24 int n = *(int*)arg; 25 long long* val = (long long*)malloc(sizeof(long long)); 26 *val = calc(n); 27 return (void*)val; 28 } 29 30 int main(int argc, char** argv) 31 { 32 pthread_t th; 33 pthread_attr_t attr; 34 pthread_attr_init(&attr); 35 pthread_attr_setstack(&attr, malloc(stackSize), stackSize); 36 int arg; 37 scanf("%d", &arg); 38 pthread_create(&th, &attr, threadEntry, &arg); 39 40 void* result; 41 pthread_join(th, &result); 42 printf("%d! = %lld\n", arg, *(long long*)result); 43 44 free(result); 45 pthread_attr_destroy(&attr); 46 return 0; 47 }标签:include,attr,int,Linux,线程,pthread,多线程,函数 From: https://www.cnblogs.com/BroccoliFighter/p/18064891