首页 > 系统相关 >Linux多线程

Linux多线程

时间:2024-03-10 22:35:38浏览次数:30  
标签:include attr int Linux 线程 pthread 多线程 函数

线程的概念

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

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

相关文章

  • linux系统必备软件
    linux系统必备软件需要配置好epel源必须安装的工具treevimwgetbash-completionbash-completion-extraslrzsznet-toolssysstatiotopiftophtopunzipncnmaptelnetbcpsmischttpd-toolsbind-utilsnethogsexpect命令作用tree以树形显示目......
  • 分布式锁——JVM锁、MySQL锁解决多线程下并发争抢资源
    分布式锁——JVM锁、MySQL锁解决库存超卖问题引入库存扣案例需求背景电商项目中,用户购买商品后,会对商品的库存进行扣减。需求实现根据用户购买商品及商品数量,对商品的库存进行指定数量的扣减publicStringdeductStock(LonggoodsId,Integercount){//1.查询商品......
  • 多进程、多线程知识再整理
    #threading模块'''cpython全局解释器锁导致同时只能有一个线程执行python,利用多cpu执行cpu密集型任务使用多进程,密集io型可以使用多线程并发classthreading.Thread(group=None,target=None,name=None,args=(),kwargs={},*,daemon=NoneThread类代表一个在独立控制线......
  • Linux防火墙命令
    //端口可批量操作////开启防火墙systemctlstartfirewalld//查看防火墙开放的端口firewall-cmd--list-ports//开放端口(开放后需要要重启防火墙才生效)firewall-cmd--zone=public--add-port=端口/tcp--permanent//重启防火墙firewall-cmd--reload//停止防火墙sy......
  • Linux脚本分享
    宝塔官方自动挂载硬盘脚本说明:本工具默认将数据盘挂载到/www目录若您的磁盘已分区,且未挂载,工具会自动将分区挂载到/www若您的磁盘是新磁盘,工具会自动分区并格式化成xfs/ext4文件系统已安装宝塔或数据盘有数据务必先做快照或数据备份挂载后建议重启服务器检查是否挂载成......
  • 2.1 Linux 网络相关概念和修改IP地址的方法
    2.1Linux网络相关概念和修改IP地址的方法2.1.1网卡的命名规则Centos6的网卡命名方式:它会根据情况有所改变而非唯一且固定,在Centos6之前,网络接口使用连续号码命名:如eth0、eth1等,当增加或删除网卡时,名称可能会发生改变Centos7采用dmidec......
  • 手撕Java多线程(四)线程之间的协作
    线程之间的协作当多个线程可以一起去解决某个问题时,如果某些部分必须在其他部分之前完成,那么就需要对线程进行协调。join()在线程中调用另一个线程的join()方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。对于以下代码,虽然b线程先启动,但是因为在b线程中调用了a线程的join......
  • linux查看资源使用情况
    linux查看资源使用情况top-c#查看资源使用情况top输出如下内容top-14:54:21up95days,20:03,3users,loadaverage:2072.21,1241.33,1244.76Tasks:1071total,459running,610sleeping,2stopped,0zombie%Cpu(s):12.4us,36.1sy,0.0ni,51......
  • Linux系统初始化+安装docker
    Linux初始化脚本#!/bin/bash#在master节点和worker节点都要执行#安装docker#参考文档如下#https://docs.docker.com/install/linux/docker-ce/centos/#https://docs.docker.com/install/linux/linux-postinstall/#卸载旧版本yumremove-ydocker\docke......
  • 13_Linux第一个程序HelloWorld
    Linux第一个程序HelloWorld1.什么是gcc?gcc全称(guncompilercollection)既编译套件,gcc可以支持多种计算机体系结构,比如X86,MIPI,ARM。Ubuntu默认自带gcc可以使用gcc-v命令来查看Ubuntu的gcc2.gcc基本用法gcc选项文件名举例:gcchello.c-ohello-o参数......