Linux多线程

线程的概念

线程是指程序中的一条执行路径。在一个进程中,至少有一个线程,称为主线程,通过主线程可以派生出其他子线程。

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 }
posted @ 2024-03-10 22:21  西兰花战士  阅读(69)  评论(0编辑  收藏  举报