Linux的线程详解+代码引导
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。