Linux 线程 (Ubuntu)

本文深入探讨Linux线程的概念,包括线程的创建、线程ID、线程并发与并行的区别、线程的终止以及如何通过pthread_create、pthread_join和pthread_cancel进行线程管理。线程作为调度的基本单位,共享进程资源,通过线程ID区分,主线程可创建子线程,而pthread_join用于等待和回收线程资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux的线程详解+代码引导

1.线程描述

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

  1. 线程是如何创建起来的?

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

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

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

主线程的重要性体现在两方面:

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

4.线程特点
1.线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。

2.同一进程中的多个线程将共享该进程中的全部系统资源。如虚拟地址空间,文件描述符和信号处理等等;
3. 同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage
4.线程一定存在于进程中

5.线程是参与系统调度的基本单位

6.可并发执行。同一进程的多个线程之间可并发执行

7.共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址; 此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。

5.并行,并发

a.并行指的是可以并排/并列执行多个任务,并行运行并不一定要同时开始运行、同时结束运行

b.并发强调的是一种时分复用,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。可见下图。

 简单理解:并发:交替做不同的事;并行:同时做不同的事。

6.同时运行

计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替/交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快了,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程。 简而言之,程序是交替做不同的事情,但是速度太快,一个线程可能只需要几微妙,而肉眼所见是同时运行。

2.线程ID

1.每个线程有对应的标识,称为线程 ID。

进程 ID 在整个系统中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。

2.线程ID的表示

进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数。而线程 ID 使用 pthread_t 数据类型来表示, 一个线程可通过库函数 pthread_self()获取自己的线程 ID

#include <pthread.h>

pthread_t pthread_self(void);

该函数调用总是成功,返回当前线程的线程 ID。

3.使用 pthread_equal()函数来检查两个线程 ID 是否相等,函数原型:

#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

两个线程 ID t1 和 t2 相等,则 pthread_equal()返回一个非零值;否则返回 0

无符号长整型(unsigned long int)来表示 pthread_t 数据类型,但是在其它系统当中,则不一定是无符号长整型,所以我们必须将 pthread_t 作为一种不透明的数据类型加以对待.所以pthread_equal()函数用于比较两个线程 ID 是否相等是有用的。

小知识 :

在一些应用程序中,以特定线程的线程 ID作为动态数据结构的标签,这某些应用场合颇为有用, 既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。

3.创建线程

1.启动程序时,创建的进程只是一个单线程的进程,称之为初始线程或主线程。

2.主线程(即为程序第一个进入main的线程)可以使用库函数 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 类型指针,当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。

attr:

pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区,pthread_attr_t 数据类型定义了线程的各种属性,关于线程属性将会在 11.8 小节介绍。如果将参数 attr 设置为 NULL,那么表示将线程的所有属性设置为默认值,以此创建新线程。

start_routine:

参数 start_routine 是一个函数指针,指向一个函数,新创建的线程从 start_routine()函数开始运行,该函数返回值类型为void *,并且该函数的参数只有一个void *,其实这个参数就是pthread_create()函数的第四个参数 arg。如果需要向 start_routine()传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为 arg 参数传入。

arg:

传递给 start_routine()函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量,意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数 arg 设置为 NULL,表示不需要传入参数给 start_routine()函数

返回值:

成功返回 0;失败时将返回一个错误号,并且参数 thread 指向的内容是不确定的。

使用示例使用 pthread_create()函数创建一个除主线程之外的新线程,示例代码如下所示:
以下代码Ubuntu环境下直接复制粘贴,gcc完事直接跑,但不要忘了链接上 -pthread或者-lpthread!!!

//示例代码 11.3.1 pthread_create()创建线程使用示例
#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());//获取进程id的同时获取线程id 对应相应的函数
	 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);
}

注意 pthread_create()在调用失败时通常会返回错误码,它并不像其它库函数或系统调用一样设置 errno,每个线程都提供了全局变量 errno 的副本,这只是为了与使用 errno 到的函数进行兼容,在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局变量,这样可以把错误的范围限制在引起出错的函数中

pthread_t 作为一种不透明的数据类型加以对待,但是在示例代码中需要打印线程 ID,所以要明确其数据类型,示例代码中使用了 printf()函数打印线程 ID 时,将其作为 unsigned long int 数据类型,在 Linux系统下,确实是使用 unsigned long int 来表示 pthread_t

在主线程和新线程中,分别通过 getpid()和 pthread_self()来获取进程 ID 和线程 ID

gcc -o testApp testApp.c -lpthread

两个线程的进程 ID 相同,说明新创建的线程与主线程本来就属于同一个进程,但是它们的线程 ID 不同。

4.终止线程

1.除了在线程 start 函数中执行 return 语句终止线程外,终止线程的方式还有多种,可以通过如下方式终止线程的运行:

  • 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
  • 线程调用 pthread_exit()函数;
  • 调用 pthread_cancel()取消线程(下面详细讲到);

如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意!

pthread_exit()函数将终止调用它的线程,其函数原型如下所示:

#include <pthread.h>

void pthread_exit(void *retval);

参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用 pthread_join()来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join()来获取的。参数 retval 所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效;也不应在线程栈中分配线程 start 函数的返回值。

调用 pthread_exit()相当于在线程的 start 函数中执行 return 语句,不同之处在于,可在线程 start 函数所调用的任意函数中调用 pthread_exit()来终止线程。如果主线程调用了 pthread_exit(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止

使用示例

//示例代码 11.4.1 pthread_exit()终止线程使用示例

#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);
}

新线程中调用 sleep()休眠,保证主线程先调用 pthread_exit()终止,休眠结束之后新线程也调用pthread_exit()终止。

正如上面介绍到,主线程调用 pthread_exit()终止之后,整个进程并没有结束,而新线程还在继续运行。

5 回收线程

父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中,也需要如此,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源。

//代码示例
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

thread:

pthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程;

retval:

如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消,则将 PTHREAD_CANCELED 放在retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL。

返回值:

成功返回 0;失败将返回错误码。

调用 pthread_join()函数将会以阻塞(等待)的形式等待指定的线程终止,如果该线程已经终止,则 pthread_join()立刻返回。如果多个线程同时尝试调用 pthread_join()等待指定线程的终止,那么结果将是不确定的。

若线程并未分离(detached,将在 11.6.1 小节介绍),则必须使用 pthread_join()来等待线程终止,回收线程资源;如果线程终止后,其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;线程积累过多会导致影响内存环境。僵尸线程除了浪费系统资源外,若僵尸线程积累过多,会导致应用程序无法创建新的线程。

如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收。

pthread_join()执行的功能类似于针对进程的 waitpid()调用,不过二者之间(阻塞等待)存在一些显著差别:

1.线程之间关系是对等的。进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止。

进程间层次关系不同,父进程如果使用 fork()创建了子进程,那么它也是唯一能够对子进程调用 wait()的进程,线程之间不存在这样的关系。

譬如,如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join()等待线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止。

2.不能以非阻塞的方式调用 pthread_join()。

对于进程,调用 waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。

使用示例

//示例代码 11.5.1 pthread_join()等待线程终止
#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);
}

主线程调用 pthread_create()创建新线程之后,新线程执行 new_thread_start()函数,而在主线程中调用pthread_join()阻塞等待新线程终止,新线程终止后,pthread_join()返回,将目标线程的退出码保存在*tret 所指向的内存中。

图 11.5.1 测试结果

6 取消线程

在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出。

  那么取消线程是什么意思?

程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消 线程。向指定的线程发送一个请求,要求其立刻终止、退出。简而言之,取消现场就好比送快递,到楼下了告诉你一声然后就走了。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。

取消一个线程

调用 pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:

#include <pthread.h>

int pthread_cancel(pthread_t thread);

参数 thread 指定需要取消的目标线程;成功返回 0,失败将返回错误码。

发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。所pthread_cancel()并不会等待线程终止,仅仅只是提出请求。所以往往我们为了保险起见会放置一个pthread_join来阻塞等待线程终止。

使用示例

//示例代码 11.6.1 pthread_cancel()取消线程使用示例
#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);
}

可以看出和上面代码相同,但是为了加深我们的印象还是要稳扎稳打。

主线程创建新线程,新线程 new_thread_start()函数直接运行 for 死循环;主线程休眠一段时间后,调用pthread_cancel()向新线程发送取消请求,接着再调用 pthread_join()等待新线程终止、获取其终止状态,将线程退出码打印出来。(代码思路如上)
由打印结果可知,当主线程发送取消请求之后,新线程便退出了,而且退出码为-1,也就是

PTHREAD_CANCELED。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

芯片烧毁大师

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

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

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

打赏作者

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

抵扣说明:

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

余额充值