目录
概述
1 线程概念
1.1 线程的特性
1.2 线程的运行状态
2 线程API
2.1 pthread的数据类型
2.2 pthread函数的返回值
2.3 POSIX线程接口
2.3.1 创建线程函数pthread_create
2.3.2 终止线程
2.3.3 线程ID
2.3.4 连接已终止线程
2.3.5 线程分离
3 线程VS进程
4 线程使用的案例
4.1 使用pthread_exit退出线程
4.1.1 功能介绍
4.1.2 编写代码
4.1.3 验证
4.2 修改线程属性
4.2.1 功能介绍
4.2.2 编写代码
4.2.3 验证
5 参考文献
概述
本文详细介绍线程的相关知识,并介绍和线程相关的pthread的API,对其中重要的函数做了详细的说明,还比较了线程和进程的优缺点。最后,使用pthread API编写了两个案例,以更好的认识和理解这些接口函数。
1 线程概念
每一个进程有一个地址空间和一个控制线程。同一个地址空间准并行(对于单核CPU来说,本质上还是串行执行的)运行多个控制线程,这些线程就像分离的进程。所以,线程也可以被称作mini进程。下图可形象的表示进程和线程的关系:
1.1 线程的特性
1)线程依附于进程存在,当该进程消亡后,运行在该进程下的线程也全部消亡;
2)同一个进程下的所有线程,共享同一个地址空间和所有可用数据;
3)线程是轻量级别的运行程序(和进程比较),它们比进程更容易创建和销毁;
4)在存在大量计算和大量IO处理的系统中,多线程允许这些活动重叠进行,从而加快应用程序的执行速度;
5)进程用于把资源集中在一起,线程则是在CPU上被调度执行的实体;
6)同一个进程中,允许运行多个线程。
1.2 线程的运行状态
一个线程可以处在若干中状态中的任何一个:运行态,阻塞态,就绪态或者终止态
运行态:正在运行的线程,拥有CPU的资源。并且活跃执行。
阻塞态:线程被阻塞,以便等待其他事件或者资源来释放它的
就绪态:线程可以被调度,只要轮到它就马上被执行
终止态:线程停止运行,并且不再被唤醒
2 线程API
2.1 pthread的数据类型
数据类型 | 描述 |
---|---|
pthread_t | 线程ID |
pthread_mutex_t | 互斥对象 |
pthread_mutexattr_t | 互斥属性对象 |
pthread_cond_t | 条件变量 |
pthread_condattr_t | 条件变量的属性对象 |
pthread__key_t | 线程特有数据的键 |
pthread_once_t | 一次初始化控制上下文 |
pthread_attr_t | 线程的属性对象 |
2.2 pthread函数的返回值
pthread函数返回0,表示成功,返回一个正值表示失败。这只是为了与使用 errno 到的函数进行兼容,在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局变量,这样可以把错误的范围限制在引起出错的函数中 。
2.3 POSIX线程接口
为了实现可移植的线程程序,IEEE标准定义了线程的标准。这里介绍一些使用线程常用到的一些接口函数
函数名 | 描述 |
---|---|
pthread_create | 创建一个新线程 |
pthread_exit | 结束调用的线程 |
pthread_join | 等待一个特定的线程退出 |
pthread_yield | 释放CPU来运行另外一个线程 |
pthread_attr_init | 创建并初始化一个线程的属性结构 |
pthread_attr_destroy | 删除一个线程的属性结构 |
2.3.1 创建线程函数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 类型指针, 当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程 |
attr | pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区, pthread_attr_t 数据类型定义了线程的各种属性,如果将参数 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()函数。 |
2.3.2 终止线程
可以通过如下方式终止线程的运行:
1)线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
2)线程调用 pthread_exit()函数;
3)调用 pthread_cancel()取消线程;
4)如果进程中的任意线程调用 exit(),那么将会导致所有的进程立即终止
#include <pthread.h> void pthread_exit(void *retval);
2.3.3 线程ID
进程内部的每一个线程都有唯一的便是,称为线程ID。 线程 ID 使用 pthread_t 数据类型来表示,一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数声明如下所示:
#include <pthread.h> pthread_t pthread_self(void);
线程 ID 在应用程序中非常有用,原因如下: 1) 不同的线程函数,利用线程 ID 来标识要操作的目标线程, 这些函数包括pthread_cancel()、 pthread_detach()、 pthread_join()、 pthread_kill()
2) 在一些应用程序中,以特定线程的线程 ID 作为动态数据结构的标签,这某些应用场合颇为有用,既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。
2.3.4 连接已终止线程
函数pthread_join() 等待由pthread_t标识的线程终止,如果该线程已经终止,则其立即返回。 通过参数 thread(线程 ID) 指定需要等待的线程;
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
参数介绍
参数 | 描述 |
---|---|
thread | 通过参数 thread(线程 ID) 指定需要等待的线程 |
retval | 如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过 pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消, 则将 PTHREAD_CANCELED 放在retval 中。 如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL。 |
pthread_join()终止线程的特点:
1)调用 pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则 pthread_join() 立刻返回。 如果多个线程同时尝试调用 pthread_join()等待指定线程的终止,那么结果将是不确定的。
2) 若线程并未分离,则必须使用 pthread_join()来等待线程终止,回收线程资源;如果线程终止后,其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。
pthread_join()执行的功能类似于针对进程的 waitpid()调用,不过二者之间存在一些显著差别: 1)线程之间关系是对等的。进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止。譬如,如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join()等待线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止;这与进程间层次关系不同,父进程如果使用 fork()创建了子进程,那么它也是唯一能够对子进程调用 wait()的进程,线程之间不存在这样的关系。
2)不能以非阻塞的方式调用 pthread_join()。对于进程,调用 waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。
2.3.5 线程分离
当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关心线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用 pthread_detach()将指定线程进行分离,也就是分离线程。其函数声明如下所示:
#include <pthread.h> int pthread_detach(pthread_t thread);
调用pthread_detach()后:
1)一旦线程处于分离状态,不能在使用pthread_join()来获取状态,也无法返回可连接状态;
2)其他线程调用exit(),或者主线程执行return。即使已经分离的线程也会收到影响,此时不管线程处于那种状态,进程下的所有线程都会终止。
3 线程VS进程
在应用程序中,使用多线程还是多进程呢?这要根据应用的场景进行选择。现在,先来看看多线程的优缺点吧:
线程的优点
1)线程间共享数据简单
2)创建线程要远快于进程
线程的缺点
1)多线程编程,要确保以安全的方式调用函数,多进程无需考虑这个
2)某个线程的bug可能会影响其他线程,因为它们共享相同的地址空间和其他属性
3)处在同一个进程之下的所有线程都在竞争宿主下有限的地址空间;
4)多线程中处理信号,需要特别小心;
5)在多线程程序中,所有线程运行在同一个程序下。多进程可以运行在不同的程序下。
4 线程使用的案例
4.1 使用pthread_exit退出线程
4.1.1 功能介绍
创建了两个线程,其中第一个线程是在程序运行到中途时调用 pthread_exit函数退出,第二个线程正常运行退出。在主线程中收集这两个线程的退出信息,并释放资源。
4.1.2 编写代码
创建test_thread.c文件,编写如下代码
/***************************************************************
Copyright 2024-2029. All rights reserved.
文件名 : test_thread.c
作者 : tangmingfei2013@126.com
版本 : V1.0
描述 : pthread API test
其他 : 无
日志 : 初版V1.0 2024/03/04
***************************************************************/
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
/* 线程 -1 */
void thread1(void)
{
int i = 0;
for( i=0; i < 6; i++){
printf("This is pthread1.\n");
if( i == 2 ){
pthread_exit(0);
}
sleep(1);
}
}
/* 线程 -2 */
void thread2(void)
{
int i = 0;
for( i = 0; i < 3; i++ ){
printf("This is pthread2.\n");
}
pthread_exit(0);
}
int main(void)
{
pthread_t id1,id2;
int ret;
/* 创建线程 -1 */
ret = pthread_create(&id1,NULL,(void *) thread1,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
/* 创建线程 -2 */
ret = pthread_create(&id2,NULL,(void *) thread2,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
/*等待线程结束*/
pthread_join(id1,NULL);
pthread_join(id2,NULL);
exit (0);
}
Makefile 文件:
CFLAGS= -Wall -lpthread -O2
CC=/home/ctools/gcc-linaro-4.9.4-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc
STRIP=/home/ctools/gcc-linaro-4.9.4-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip
test_thread: test_thread.o
$(CC) $(CFLAGS) -o test_thread test_thread.o
$(STRIP) -s test_thread
clean:
rm -f test_thread test_thread.o
4.1.3 验证
编译代码,让后在板卡上运行,得到运行结果:
4.2 修改线程属性
4.2.1 功能介绍
第一个线程设置为分离属性,并将第二个线程设置为始终运行状态,这样就可以在第二个线程运行过程中查看内存值的变化。
4.2.2 编写代码
创建test_thread.c,然后编写如下代码
/***************************************************************
Copyright 2024-2029. All rights reserved.
文件名 : test_thread.c
作者 : tangmingfei2013@126.com
版本 : V1.0
描述 : pthread API test
其他 : 无
日志 : 初版V1.0 2024/03/04
***************************************************************/
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
/* 线程 -1 */
void thread1(void)
{
int i = 0;
for( i=0; i < 6; i++){
printf("This is pthread1.\n");
if( i == 2 ){
pthread_exit(0);
}
sleep(1);
}
}
/* 线程 -2 */
void thread2(void)
{
int i = 0;
for( i = 0; i < 3; i++ ){
printf("This is pthread2.\n");
}
pthread_exit(0);
}
int main(void)
{
pthread_t id1,id2;
int ret;
pthread_attr_t attr;
/*初始化线程*/
pthread_attr_init(&attr);
/*设置线程绑定属性*/
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
/*设置线程分离属性*/
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
/*创建线程*/
ret=pthread_create(&id1,&attr,(void *) thread1,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
ret=pthread_create(&id2,NULL,(void *) thread2,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
pthread_join(id2,NULL);
return (0);
}
4.2.3 验证
编译代码,让后在板卡上运行,得到运行结果:
通过比较运行前后的内存,可知在线程1执行完成后,会立即是否内存
5 参考文献
-
《现代操作系统》
-
《linux/unix系统编程手册》