【Linux C | 多线程编程】线程的创建、线程ID、线程属性

😁博客主页😁:🚀https://blog.youkuaiyun.com/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
⏰发布时间⏰:2024-03-22 09:05:41

本文未经允许,不得转发!!!


在这里插入图片描述

🎄一、概述

Linux线程库接口包括线程的创建、 退出、 取消和分离, 以及连接已经终止的线程, 互斥量, 读写锁,线程的条件等待等。

POSIX 函数函数功能描述
pthread_create创建一个线程
pthread_exit退出线程
pthread_self获取线程ID
pthread_equal检查两个线程ID是否相等
pthread_join等待线程退出
pthread_detach设置线程状态为分离状态
pthread_cancel线程的取消
pthread_cleanup_push、pthread_cleanup_pop线程退出,清理函数注册和执行

在代码里使用到上述接口函数时,使用gcc编程过程中需要加-pthread选项。

本文将介绍线程创建相关的一些知识,从pthread_create开始,然后依次介绍该函数第一个参数相关的线程ID,以及第二个函数相关的线程属性。


在这里插入图片描述

🎄二、线程的创建 pthread_create

程序开始启动的时候, 产生的进程只有一个线程, 我们称之为主线程或初始线程。 对于单线程的进程而言, 只存在主线程一个线程。 如果想在主线程之外, 再创建一个或多个线程, 就需要 pthread_create 函数。

pthread_create 函数原型:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
   					void *(*start_routine) (void *), void *arg);

Compile and link with -pthread.
  • 函数描述:函数 pthread_create 在调用过程中启动一个新线程。新线程通过调用 start_routine 参数指向的函数开始执行;arg参数作为 start_routine 的唯一参数传递。
  • 函数参数:
    • thread:传出参数,thread参数是 pthread_t 类型的指针,线程创建成功的话,会将分配的线程ID填入该指针指向的地址。线程的后续操作将使用该值作为线程的唯一标识。
    • attr:传入参数,第二个参数是 pthread_attr_t 类型,通过该参数可以定制线程的属性,比如可以指定新建线程栈的大小、调度策略等。 如果创建线程无特殊的要求, 该值也可以是NULL, 表示采用默认属性。
    • start_routine:传入参数,第三个参数是线程需要执行的函数。创建线程,是为了让线程执行一定的任务。线程创建成功之后,该线程就会执行start_routine函数,该函数之于线程,就如同 main 函数之于主线程。
    • arg:第四个参数是新建线程执行的 start_routine 函数的入参。新建线程如果想要正常工作,则可能需要入参,那么主线程在调用 pthread_create 的时候,就可以将入参的指针放入第四个参数以传递给新建线程。如果多个入参,可以使用结构体指针。
  • 函数返回值:如果成功,则 pthread_create 返回0;如果不成功,则 pthread_create 返回一个非0的错误码。pthread_create函数有点不同, 它会将errno作为返回值, 而不是一个负值。
    • EAGAIN:系统资源不够,或者创建线程的个数超过系统对一个进程中线程总数的限制
    • EINVAL:第二个参数attr值不合法
    • EPERM:没有合适的权限来设置调度策略或参数

通过上面的描述可以看到,pthread_create 是一个"四针"函数,也就是说它四个参数都是指针。下面是这个函数的简单使用示例:

// 02_pthread_create
// 编译:gcc 02_pthread_create.c -l pthread
#include <stdio.h>
#include <pthread.h>
void *func(void *arg)
{
	int *parg = arg;
	printf("this thread arg is %d \n", *parg);
	return NULL;
}
int main()
{
	int arg=10;
	pthread_t threadId;
	pthread_create(&threadId, NULL, func, &arg);
	while(1); // 让主线程不退出
	return 0;
}

在这里插入图片描述

🎄三、线程ID

通过 pthread_create 成功创建线程后,第一个参数会返回所创建线程的线程ID,这个线程ID不同于使用系统调用函数syscall(SYS_gettid)获得的线程ID。syscall(SYS_gettid)的ID是进程调度的范畴;而这里返回的线程ID是操作系统调度器用来标识线程的。

pthread_t到底是个什么样的数据结构呢? 因为POSIX标准并没有限制pthread_t的数据类型, 所以该类型取决于具体实现。 对于Linux目前使用的NPTL实现而言, pthread_t类型的线程ID, 本质就是一个进程地址空间上的一个地址。

typedef unsigned long int pthread_t;

pthread_t类型在Linux系统中定义在 <bits/pthreadtypes.h>头文件中,在Ubuntu可以使用命令vi /usr/include/bits/pthreadtypes.h 来查看,其完整定义如上,是unsigned long int类型的。

✨2.1 线程ID相关函数

关于线程ID,线程库NPTL提供了pthread_selfpthread_equal 两个函数来操作线程ID,它们的函数原型如下:

#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t t1, pthread_t t2);

Compile and link with -pthread.

pthread_self函数用于在线程指向函数中获取自身线程ID,这个函数不会调用失败,返回值就是线程ID;

pthread_equal函数用于比较两个线程ID是否相等,返回值是0的时候, 表示两个线程是同一个线程, 非零值则表示不是同一个线程。注意,比较线程ID只有在同一个进程中才有意义。

🌰举例子:

// 03_pthreadID.c
// gcc 03_pthreadID.c -l pthread
#include <stdio.h>
#include <pthread.h>
void *func1(void *arg)
{
	int *parg = arg;
	printf("this thread arg is %d, my threadID is %lx \n", *parg, (unsigned long)pthread_self());
	while(1); // 让线程不退出
}
void *func2(void *arg)
{
	pthread_t *parg = arg;
	printf("other threadId is %lx, my threadID is %lx \n", (unsigned long)*parg, (unsigned long)pthread_self());
	while(1); // 让线程不退出
}
int main()
{
	int arg=10;
	pthread_t threadId_1;
	pthread_create(&threadId_1, NULL, func1, &arg);
	pthread_t threadId_2;
	pthread_create(&threadId_2, NULL, func2, &threadId_1);
	if(0 == pthread_equal(threadId_1,threadId_2))
		printf("same threads\n");
	else
		printf("different threads\n");
	while(1); // 让主线程不退出
	return 0;
}

✨2.2 线程ID复用

在满足下列条件时, 线程ID就有可能会被复用:
1) 线程退出。
2) 线程组的其他线程对该线程执行了pthread_join, 或者线程退出前将分离状态设置为已分离。
3) 再次调用pthread_create创建线程。

看例子:

// 04_pthreadID_reuse.c
// gcc 04_pthreadID_reuse.c -l pthread
#include <stdio.h>
#include <pthread.h>
void *func(void *arg)
{
	int *parg = arg;
	printf("this thread arg is %d, my threadID is %lx \n", *parg, (unsigned long)pthread_self());
	return NULL;
}

int main()
{
	int arg=10;
	pthread_t threadId;
	pthread_create(&threadId, NULL, func, &arg);
	pthread_join(threadId,NULL); // 等待线程退出
	pthread_create(&threadId, NULL, func, &arg);

	while(1); // 让主线程不退出
	return 0;
}

运行结果,可以看到两次的线程ID是一样的:
在这里插入图片描述


在这里插入图片描述

🎄四、线程属性

pthread_create 的第二个参数是线程属性,先看看 pthread_attr_t 结构体的定义:

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;


int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);   
功能:初始化/销毁线程的属性结构体
  • detachstate:分离状态

    • PTHREAD_CREATE_JOINABLE(默认值):线程执行完函数后不会自行释放资源;
    • PTHREAD_CANCEL_DEFERRED:线程执行完函数后,会自行终止并释放占用的资源。

    系统提供两个函数获取、设置分离状态。另外,pthread_detach函数也可以设置线程分离。

    int pthread_attr_getdetachstate(const pthread_attr_t * attr,int * detachstate);
    int pthread_attr_setdetachstate(pthread_attr_t *sttr,int detachstate);
    
  • schedpolicy:调度策略

    • SCHED_OTHER(默认值):普通策略(分时调度算法),按照优先级调度
    • SCHED_FIFO:先进先出。一个FIFO会持续执行,直到线程阻塞、结束、有更高优先级的线程就绪
    • SCHED_RR:轮转策略。给每个线程分配执行时间(时间片),当一个线程的时间片耗尽时,下一个线程执行

    其中,SCHED_OTHER 调度算法不支持为线程设置优先级,而另外两种调度算法支持。获取、设置的函数如下:

    int pthread_attr_getschedpolicy(const pthread_attr_t *, int * policy)
    int pthread_attr_setschedpolicy(pthread_attr_*, int policy)
    
  • schedparam:调度参数
    用于设置线程的优先级(默认值为 0),该属性仅当线程的 schedpolicy 属性为 SCHED_FIFO 或者 SCHED_RR 时才能发挥作用。

    int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
    int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
    struct sched_param param; // sched_param 只有一个字段 sched_priority 
    param.sched_priority = 99;
    
  • inheritsched:继承性

    • PTHREAD_INHERIT_SCHED 调度属性(schedpolicy、schedparam)继承自创建者的
    • PTHREAD_EXPLICIT_SCHED 使用attr创建的线程,从attr指定的值中获取其调度属性(schedpolicy、schedparam)。

    获取、设置函数如下:

    int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);
    int pthread_attr_getinheritsched(pthread_attr_t *attr,int *inheritsched);
    
  • scope:作用域(竞争范围)

    • PTHREAD_SCOPE_SYSTEM:在系统范围内竞争资源
    • PTHREAD_SCOPE_PROCESS:在进程范围内竞争资源

    线程执行过程中,可以只和同进程内的其它线程争夺 CPU 资源,也可以和系统中所有的其它线程争夺 CPU 资源,scope 属性用于指定目标线程和哪些线程抢夺 CPU 资源。获取、设置函数如下:

    int pthread_attr_setscope(pthread_attr_t *attr, int scope);
    int pthread_attr_getscope(pthread_attr_t *attr, int *scope);
    
  • guardsize:线程栈末尾的警戒缓冲区大小
    每个线程中,栈内存的后面都紧挨着一块空闲的内存空间,我们通常称这块内存为警戒缓冲区,它的功能是:一旦我们使用的栈空间超出了额定值,警戒缓冲区可以确保线程不会因“栈溢出”立刻执行崩溃。

    int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
    int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);
    
  • stackaddr_set:线程的栈设置

    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);
    
  • stackaddr:线程栈的位置

    int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
    int pthread_attr_getstackaddr(pthread_attr_t *attr, void **stackaddr);
    
  • stacksize:线程栈的大小

    int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
    int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
    

🌰举例子:

// 05_dispaly_attr.c  这是man手册的一个展示线程属性的例子,可以仔细研究以下
// gcc 05_dispaly_attr.c -l pthread
#define _GNU_SOURCE     /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define handle_error_en(en, msg) \
	   do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

static void
display_pthread_attr(pthread_attr_t *attr, char *prefix)
{
   int s, i;
   size_t v;
   void *stkaddr;
   struct sched_param sp;

   s = pthread_attr_getdetachstate(attr, &i);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getdetachstate");
   printf("%sDetach state        = %s\n", prefix,
		   (i == PTHREAD_CREATE_DETACHED) ? "PTHREAD_CREATE_DETACHED" :
		   (i == PTHREAD_CREATE_JOINABLE) ? "PTHREAD_CREATE_JOINABLE" :
		   "???");

   s = pthread_attr_getscope(attr, &i);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getscope");
   printf("%sScope               = %s\n", prefix,
		   (i == PTHREAD_SCOPE_SYSTEM)  ? "PTHREAD_SCOPE_SYSTEM" :
		   (i == PTHREAD_SCOPE_PROCESS) ? "PTHREAD_SCOPE_PROCESS" :
		   "???");

   s = pthread_attr_getinheritsched(attr, &i);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getinheritsched");
   printf("%sInherit scheduler   = %s\n", prefix,
		   (i == PTHREAD_INHERIT_SCHED)  ? "PTHREAD_INHERIT_SCHED" :
		   (i == PTHREAD_EXPLICIT_SCHED) ? "PTHREAD_EXPLICIT_SCHED" :
		   "???");

   s = pthread_attr_getschedpolicy(attr, &i);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getschedpolicy");
   printf("%sScheduling policy   = %s\n", prefix,
		   (i == SCHED_OTHER) ? "SCHED_OTHER" :
		   (i == SCHED_FIFO)  ? "SCHED_FIFO" :
		   (i == SCHED_RR)    ? "SCHED_RR" :
		   "???");

   s = pthread_attr_getschedparam(attr, &sp);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getschedparam");
   printf("%sScheduling priority = %d\n", prefix, sp.sched_priority);

   s = pthread_attr_getguardsize(attr, &v);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getguardsize");
   printf("%sGuard size          = %ld bytes\n", prefix, v);

   s = pthread_attr_getstack(attr, &stkaddr, &v);
   if (s != 0)
	   handle_error_en(s, "pthread_attr_getstack");
   printf("%sStack address       = %p\n", prefix, stkaddr);
   printf("%sStack size          = 0x%lx bytes\n", prefix, v);
}

static void *
thread_start(void *arg)
{
   int s;
   pthread_attr_t gattr;

   /* pthread_getattr_np() is a non-standard GNU extension that
	  retrieves the attributes of the thread specified in its
	  first argument */

   s = pthread_getattr_np(pthread_self(), &gattr);
   if (s != 0)
	   handle_error_en(s, "pthread_getattr_np");

   printf("Thread attributes:\n");
   display_pthread_attr(&gattr, "\t");

   exit(EXIT_SUCCESS);         /* Terminate all threads */
}

int main(int argc, char *argv[])
{
   pthread_t thr;
   pthread_attr_t attr;
   pthread_attr_t *attrp;      /* NULL or &attr */
   int s;

   attrp = NULL;

   /* If a command-line argument was supplied, use it to set the
	  stack-size attribute and set a few other thread attributes,
	  and set attrp pointing to thread attributes object */

   if (argc > 1) {
	   int stack_size;
	   void *sp;

	   attrp = &attr;

	   s = pthread_attr_init(&attr);
	   if (s != 0)
		   handle_error_en(s, "pthread_attr_init");

	   s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	   if (s != 0)
		   handle_error_en(s, "pthread_attr_setdetachstate");

	   s = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
	   if (s != 0)
		   handle_error_en(s, "pthread_attr_setinheritsched");

	   stack_size = strtoul(argv[1], NULL, 0);

	   s = posix_memalign(&sp, sysconf(_SC_PAGESIZE), stack_size);
	   if (s != 0)
		   handle_error_en(s, "posix_memalign");

	   printf("posix_memalign() allocated at %p\n", sp);

	   s = pthread_attr_setstack(&attr, sp, stack_size);
	   if (s != 0)
		   handle_error_en(s, "pthread_attr_setstack");
   }

   s = pthread_create(&thr, attrp, &thread_start, NULL);
   if (s != 0)
	   handle_error_en(s, "pthread_create");

   if (attrp != NULL) {
	   s = pthread_attr_destroy(attrp);
	   if (s != 0)
		   handle_error_en(s, "pthread_attr_destroy");
   }

   pause();    /* Terminates when other thread calls exit() */
}

运行结果,打印一些默认值:
在这里插入图片描述


在这里插入图片描述

🎄五、总结

本文介绍了线程创建相关的内容,包括pthread_create函数的详细介绍和使用例子,然后依次介绍该函数第一个参数相关的线程ID知识以及第二个参数相关的线程属性知识。读完完整地了解线程的创建。

补充:
进程的地址空间:
1、Linux系统中,/proc/sys/vm/legacy_va_layout文件的值会影响进程地址空间的布局。默认值是0,表示mmap区域的基地址在栈的下面, mmap区域从高地址向低地址扩展;若值为1, 那么mmap的基地址mmap_base变小(约在128T的三分之一处),mmap区域从低地址向高地址扩展。

2、使用命令 pmap PIDcat /proc/PID/maps 可以查看进程的地址空间:
在这里插入图片描述

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

参考资料:
https://blog.youkuaiyun.com/qq_41854911/article/details/118719001
《Linux环境编程:从应用到内核》

评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wkd_007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值