与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么,只是维护应用程序所需的各种资源,而线程则是真正的执行实体。所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。
进程,直观点说,是保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位。而线程存在于进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。
总结:进程是操作系统分配资源的最小单位;线程是操作系统调度的最小单位
当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone()
系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合 POSIX 标准的要求。
要改进 LinuxThreads,非常明显需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL。
NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。
查看当前pthread库版本:
gsmrlab@amsiv:~$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.27
查看指定进程的 LWP(light weight process) 号:
ps -Lf pid
linux 下安装 posix 手册
查看线程相关函数列表,需要先下载 posix 手册:
sudo apt-get install manpages-posix-dev
【说明】manpages-posix-dev 包含 POSIX 的 header files 和 library calls 的用法
查看 posix 线程函数列表:
man -k pthread
查看函数的具体用法:
man [函数名]
线程与进程的联系
类 Unix 系统中,早期是没有“线程”概念的,80 年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切:
clone()
系统调用:实际上,无论是创建进程的 fork()
,还是创建线程的 pthread_create()
,底层实现都是调用同一个内核函数 clone()
。 线程与进程的区别
当然进程与线程也有不同,线程和进程主要区别如下:
fork()
来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork()
调用在时间上的开销依然不菲。线程共享资源
线程与进程的联系
【注意】线程间的共享资源与非共享资源是针对同一进程中的线程而言的,如果两个线程不在同一进程中,就谈不上共享资源与非共享资源。
线程函数的程序在 pthread 库中,pthread 库不是Linux系统默认的库,链接时需要使用静态库 libpthread.a,故编译时需要指定链接线程库:要加上参数 -lpthread(当然直接-pthread也可)。
就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。
进程号用 pid_t 数据类型表示,是一个非负整数;线程号则用 pthread_t 数据类型来表示,linux 使用无符号长整数表示。有的系统在实现 pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现中,不能把它做为整数处理。
获取线程号的函数如下:
#include pthread_t pthread_self(void);
功能:获取线程号。
参数:无
返回值:调用线程的线程 ID 。
【注意】本函数总是会执行成功。
判断两线程是否相等的函数如下:
#include int pthread_equal(pthread_t t1, pthread_t t2);
功能:判断线程号 t1 和 t2 是否相等。为了方便移植,尽量使用函数来比较线程 ID。
参数:t1,t2:待判断的线程号。
返回值:相等: 非 0不相等:0
参考程序:
#include
#include
#include #include int main() {pthread_t thread_id = -1;// 返回调用线程的线程IDthread_id = pthread_self(); //本函数总是会执行成功,所以不需要进行错误判断printf("Thread ID = %lu \n", thread_id);if (0 != pthread_equal(thread_id, pthread_self())) {printf("Equal!\n");} else {printf("Not equal!\n");}return 0;
}
一般情况下,main 函数所在的线程我们称之为主线程(或者叫做 main 线程),其余创建的线程称之为子线程。
程序中默认只有一个进程,fork()
函数调用之后,程序中会存在两个进程;程序中默认只有一个线程,pthread_create()
函数调用,该进程中会存在两个线程。
pthread_create 函数用于创建线程,详细如下所示:
#include int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg );
功能:创建一个线程。
参数:thread:线程标识符地址(传出参数:线程创建成功后,子线程的线程ID被写到该变量中)。attr:线程属性结构体地址,通常设置为 NULL。start_routine:线程函数的入口地址。arg:传给线程函数的参数。
返回值:成功:0失败:非0,即返回错误号。这个错误号和之前errno不太一样。获取错误号的信息: char * strerror(int errnum);
在一个线程中调用 pthread_create()
创建新的线程后,当前线程从 pthread_create()
返回继续往下执行,而新的线程所执行的代码由我们传给 pthread_create 的函数指针 start_routine 决定。
【注意】由于 pthread_create 的错误码不保存在 errno 中,因此不能直接用 perror()
打印错误信息,可以先用 strerror()
把错误码转换成错误信息再打印。
参考程序:
#include
#include
#include #include // 回调函数
void *thread_fun(void * arg) {sleep(1);int num = *((int *)arg);printf("int the new thread: num = %d\n", num);return NULL;
}int main()
{pthread_t tid = -1;int test = 100;// 返回错误号int ret = pthread_create(&tid, NULL, thread_fun, (void *)&test);if (ret != 0) {printf("error number: %d\n", ret);// 根据错误号打印错误信息printf("error information: %s\n", strerror(ret));}while (1);return 0;
}
创建线程会申请系统资源,在线程调用结束之后,需要将这些资源进行回收,pthread_join 函数可以实现资源的回收。
为什么要进行连接?父进程创建了多个子进程,当这些子进程运行结束后,父进程就需要负责回收这些进程的资源。同理父线程也需要回收子线程的资源,否则会产生僵尸线程。与进程不同,子线程可以被任何其他的线程回收,一般情况下都是父线程回收子线程。所以需要连接子线程并对其资源进程回收。
#include int pthread_join(pthread_t thread, void **retval);
功能:阻塞等待线程结束,并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:thread:被等待的线程号。retval:用来存储线程退出状态的指针的地址。thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:1.如果thread线程通过return返回,retval 所指向的单元里存放的是 thread 线程函数的返回值。2. 如果 thread 线程被别的线程调用 pthread_cancel 异常终止掉,retval 所指向的单元里存放的是常数PTHREAD_CANCELED。3. 如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。返回值:成功:0失败:非 0
参考程序:
#include
#include
#include
#include
#include void *thead(void *arg) {static int num = 123; //静态变量sleep(3);printf("after 3 seceonds, thread will return\n");return # // 注意返回的变量,不能是局部变量
}int main()
{pthread_t tid = -1;int ret = -1;void *value = NULL;// 创建线程ret = pthread_create(&tid, NULL, thead, NULL);if (0 != ret) {printf("pthread_creat failed......\n");return 1;}// 阻塞等待线程号为 tid 的线程结束(如果此线程结束就回收其资源) // &value保存线程退出的返回值pthread_join(tid, &value);printf("main pthrad will exit,value = %d\n", *((int *)value));return 0;
}
调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。
使用 pthread_join 函数回收资源会导致调用 pthread_join 函数的线程产生阻塞等待,但是很多时候我们并不希望调用线程被阻塞,这时候就需要使用线程分离。所以线程分离不需要让父线程或者其他线程手动调用 pthread_join 去连接当前的线程来回收资源,设置分离后自动回收资源。
一般情况下,线程终止后,其终止状态一直保留到其它线程调用 pthread_join 获取它的状态为止。但是线程也可以被置为 detach 状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。
【注意】
#include int pthread_detach(pthread_t thread);
功能:使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,回收线程的函数不会阻塞。
参数:thread:线程号。
返回值:成功:0失败:非0
#include
#include
#include
#include
#include void *thead(void *arg) {static int num = 123; //静态变量sleep(3);printf("after 3 seceonds, thread will return\n");return #
}int main() {pthread_t tid = -1;int ret = -1;void *value = NULL;// 创建线程ret = pthread_create(&tid, NULL, thead, NULL);if (0 != ret) {printf("pthread_creat failed......\n");return 1;}// 线程资源由系统回收,主线程不会阻塞pthread_detach(tid);printf("main pthrad will exit\n");while (1) {sleep(1);}return 0;
}
在进程中我们可以调用 exit()
函数或 _exit()
函数(详细参考:[[09_Linux 进程基础#3、进程退出|进程退出]])来结束进程,类似的,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
#include void pthread_exit(void *retval);
功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。参数:retval:存储线程退出状态的指针。
返回值:无
【补充】主线程退出其他线程不退出,主线程应调用 pthread_exit 来退出,这样主线程退出后不会影响到子线程的运行。
参考程序:
#include
#include
#include
#include
#include void *thread(void *arg) {static int num = 123; //静态变量int i = 0;while (1) {printf("I am runing\n");sleep(1);i++;if (i == 5) {pthread_exit((void *)&num); // 相当于 return #// pthread_exit(NULL); // 相当于 return NULL;}}return NULL;
}int main(int argc, char *argv[]) {int ret = 0;pthread_t tid;void *value = NULL;pthread_create(&tid, NULL, thread, NULL);pthread_join(tid, &value);printf("value = %d\n", *(int *)value);return 0;
}
在进程中我们可以调用 exit()
函数或 _exit()
函数(详细参考:[[09_Linux 进程基础#3、进程退出|进程退出]])来结束进程,类似的,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
#include int pthread_cancel(pthread_t thread);
功能:杀死(取消)线程
参数:thread : 目标线程ID。
返回值:成功:0失败:出错编号
【注意】
参考程序:
#include
#include
#include
#include
#include
void *thread_cancel(void *arg) {while (1) {sleep(1);printf("The child thread still exists\n");}return NULL;
}int main() {pthread_t tid = -1;pthread_create(&tid, NULL, thread_cancel, NULL); //创建线程sleep(3); //3秒后pthread_cancel(tid); //取消tid线程:主线程杀死tid线程printf("The child thread already over\n");pthread_join(tid, NULL);return 0;
}
linux 下线程的属性是可以根据实际项目需要进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。
但如果我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
线程属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数必须在 pthread_create 函数之前调用。之后须用 pthread_attr_destroy 函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。这些属性都使用结构体来体现——线程属性类型:pthread_attr_t,结构体详情如下:
typedef struct
{int etachstate; //线程的分离状态int schedpolicy; //线程调度策略struct sched_param schedparam; //线程的调度参数int inheritsched; //线程的继承性int scope; //线程的作用域size_t guardsize; //线程栈末尾的警戒缓冲区大小int stackaddr_set; //线程的栈设置void* stackaddr; //线程栈的位置size_t stacksize; //线程栈的大小
} pthread_attr_t;
结构体主要成员:
#include int pthread_attr_init(pthread_attr_t *attr);
功能:初始化线程属性函数,注意:应先初始化线程属性,再 pthread_create 创建线程
参数:attr:线程属性结构体
返回值:成功:0失败:错误号int pthread_attr_destroy(pthread_attr_t *attr);
功能:销毁线程属性所占用的资源函数
参数:attr:线程属性结构体
返回值:成功:0失败:错误号
线程的分离状态决定一个线程以什么样的方式来终止自己。
pthread_join()
函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。#include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
功能:设置线程分离状态
参数:attr:已初始化的线程属性detachstate: 分离状态PTHREAD_CREATE_DETACHED(分离线程)PTHREAD_CREATE_JOINABLE(非分离线程)
返回值:成功:0失败:非0int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
功能:获取线程分离状态
参数:attr:已初始化的线程属性detachstate: 分离状态PTHREAD_CREATE_DETACHED(分离线程)PTHREAD _CREATE_JOINABLE(非分离线程)
返回值:成功:0失败:非0
【注意】
#include
#include
#include
#include
#include
void *thread_cancel(void *arg) {while (1) {sleep(1);printf("The child thread still exists\n");}return NULL;
}int main() {pthread_t tid = -1;int ret = -1;pthread_attr_t attr;ret = pthread_attr_init(&attr);if (0 != ret) {printf("pthread_att_init failed...\n");return 1;}ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);if (0 != ret) {printf("pthread_attr_setdetachstate failed...\n");return 1;}// 创建一个线程,使用初始化好的属性ret = pthread_create(&tid, &attr, thread_cancel, NULL); //创建线程if (0 != ret) {printf("pthread_create failed...\n");return 1;}ret = pthread_attr_destroy(&attr);if (0 != ret) {printf("pthread_attr_destroy failed...\n");return 1;}return 0;
}
POSIX.1 定义了两个常量来检测系统是否支持栈属性:
_POSIX_THREAD_ATTR_STACKADDR
_POSIX_THREAD_ATTR_STACKSIZE
_SC_THREAD_ATTR_STACKADDR
_SC_THREAD_ATTR_STACKSIZE
当进程栈地址空间不够用时,指定新建线程使用由 malloc 分配的空间作为自己的栈空间;通过 pthread_attr_setstack 和 pthread_attr_getstack 两个函数分别设置和获取线程的栈地址。
#include int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
功能:设置线程的栈地址
参数:attr:指向一个线程属性的指针stackaddr:内存首地址stacksize:返回线程的堆栈大小
返回值:成功:0失败:错误号int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
功能:获取线程的栈地址
参数:attr:指向一个线程属性的指针stackaddr:返回获取的栈地址stacksize:返回获取的栈大小
返回值:成功:0失败:错误号
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
#include int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
功能:设置线程的栈大小
参数:attr:指向一个线程属性的指针stacksize:线程的堆栈大小
返回值:成功:0失败:错误号int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
功能:获取线程的栈大小
参数: attr:指向一个线程属性的指针stacksize:返回线程的堆栈大小
返回值:成功:0失败:错误号
#include
#include
#include
#include
#include #define SIZE 0x100000void *th_fun(void *arg) {while (1) {sleep(1);}
}int main() {pthread_t tid;int err, detachstate, i = 1;pthread_attr_t attr;size_t stacksize;void *stackaddr;pthread_attr_init(&attr); //线程属性初始化pthread_attr_getstack(&attr, &stackaddr, &stacksize); //获取线程的栈地址pthread_attr_getdetachstate(&attr, &detachstate); //获取线程分离状态if (detachstate == PTHREAD_CREATE_DETACHED) {printf("thread detached\n");}else if (detachstate == PTHREAD_CREATE_JOINABLE) {printf("thread join\n");}else {printf("thread unknown\n");}pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置分离状态while (1) {stackaddr = malloc(SIZE);if (stackaddr == NULL) {perror("malloc");exit(1);}stacksize = SIZE;pthread_attr_setstack(&attr, stackaddr, stacksize); //设置线程的栈地址err = pthread_create(&tid, &attr, th_fun, NULL); //创建线程if (err != 0) {printf("%s\n", strerror(err));exit(1);}printf("%d\n", i++);}pthread_attr_destroy(&attr); //销毁线程属性所占用的资源函数return 0;
}