什么叫做线程?
在一个程序里的一个执行路线叫做线程,更准确的说,线程是一个进程内部的控制序列。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
在Linux操作系统中,并没有真正意义上的进程,而是用线程去模拟进程,我们把其称之为轻量级进程(Lightweight Process,LWP),每个线程都有自己的线程ID(TID),一组寄存器,栈,erron,信号屏蔽字,调度优先级。
多线程(下面将多次与进程作对比):
之前在说进程的时候,当一个可执行程序加载到内存变成一个进程,操作系统会用一个PCB将这个进程描述起来,并分配一块内存,然后将这个进程组织到一个链表中,方便操作系统管理,这说的其实是一个进程中只有一个线程的特例。
那么如何创建多线程?
对比之前的单线程进程,只需要再创建多个PCB,使其共享同一个进程的资源(寄存器、堆栈、上下文)就实现了多进程,并且创建线程和销毁线程的操作代价大大减少。
之前说父进程创建子进程之后,操作系统要为子进程创建PCB,组织到链表中,堆栈等空间直接复制父进程数据供自己单独使用,代码采用写时拷贝,为了保证进程间的数据安全,进程与进程之间通信就变得困难,而在同一进程的多个线程看见得资源是完全相同的,也就是说,线程之间的通信成本大大减小。
进程之间相互独立,当子进程触发异常退出之后,并不会影响父进程,父进程需要做的就是获取子进程的退出码并回收子进程的资源,而多线程中只要有一个线程触发异常,在操作系统看来是这个进程产生了异常,由于进程是系统分配资源的实体,操作系统会回收分配给这个进程的空间,这样所有的线程就没有了运行的“场地”, 随即也都会退出,所以一个线程异常退出也会影响到其他线程。
线程的优点:
- 创建一个新线程的代价要比创建一个新进程少的多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用资源要比进程少很多
- 能充分利用处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算操作
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作
线程的缺点:
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
介绍几个函数:
函数功能:创建一个线程(pthread_create)
原型: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
所需头文件:#include <pthread.h>
参数:
thread: 线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是一个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0, 失败返回错误码
注意:
pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。所以在编译时的命令为:gcc tcreate.c -o tcreate -lpthread
函数功能:获取线程ID(gettid)
原型:#include <sys/types.h>
所需头文件: pid_t gettid(void)
注意:Linux提供了gittid系统调用来返回线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来供程序员使用,如果确实需要线程ID,可以采用如下方法:
#include<sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
pthread_create举例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/types.h>
#include<sys/syscall.h>
void* start_fun(void* arg) {
pid_t tid;
int i = 5;
while(i--){
sleep(1);
tid = syscall(SYS_gettid);
printf("I am new thread, mytid is %d\n",tid);
}
}
int main()
{
pthread_t tid;
if(pthread_create(&tid, NULL, start_fun, NULL) != 0) {
perror("pthread_create");
exit(1);
}
pid_t main_tid;
while(1) {
sleep(1);
main_tid = syscall(SYS_gettid);
printf("I am main thread, mytid is %d\n",main_tid);
}
return 0;
}
实验结果:
5秒之后,新线程执行完毕,随即退出,只剩下主进程
在一个线程正在运行时,如何查看线程的信息?
ps -eLf | head -1 && ps -eLf | grep tcreate | grep -v grep
- UID:用户ID
- PID:进程ID
- PPID:父进程ID
- LWP:线程ID
从上面可以看出,tcreate进程的ID为46995,下面有一个线程的ID也是46995,这不是巧合。线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。
强调一点,进程有父进程和子进程的概念,但是在线程组中,所有的线程都是对等的关系。
线程库NPTL提供了pthread_self函数,可以获得线程自身的ID
#include <pthread.h>
pthread_t pthread_self(void);
返回值到底是什么类型呢?这个取决于实现,对于Linux目前的NPTL实现而言,pthread_t类型的线程ID,本质上就是一个进程空间上的一个地址。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数retun。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_exit终止自己。
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
功能:线程终止原型原型:void pthread_exit(void *value_ptr);参数:value_ptr:valueptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)需要注意,pthread_exit 或者return; 区回的指针所指向的内存单元必须是全后的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
功能::取消一个执行中的线程原型:int pthread_cancel (pthread_t thread) ;参数:thread: 线程ID返回值:成功返回0; 失败返回错误码
如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常数
PTHREAD_ CANCELED。
线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然存在进程的地址空间内
- 新创建的线程不会复用退出线程的空间
int pthread_join (pthread_t thread, void **value_ptr );
thread: 线程ID
value_ptr: 它指向一个指针,后者指向线程的返回值返回值: 成功返回0; 失败返回错误码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void* thread1(void* arg){
printf("thread1 is run ...\n");
int *p = malloc(sizeof(int));
*p = 1;
return (void* )p;
}
void* thread2(void* arg){
printf("thread2 is run ...\n");
int *p = malloc(sizeof(int));
*p = 2;
pthread_exit((void* )p);
}
void* thread3(void* arg) {
while(1) {
printf("thread3 is run ...\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
void* ret;
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("thread1 id is %X, return code %d\n", tid, *(int *)ret);
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("thread2 id is %X, return code %d\n", tid, *(int *)ret);
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if(ret == PTHREAD_CANCELED){
printf("thread3 id is %X, return code: PTHREAN_CANCELED", tid);
}
else {
printf("thread3 id is %X, return code: NULL", tid);
}
return 0;
}
实验结果:
线程分离:
默认情况下,新创建的线程是非分离状态(joinable)的,线程退出后,需要别的线程对其进⾏pthread_join操作,否则⽆法释放资源,从⽽造成系统泄漏。
如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对标线程进⾏分离,也可以是线程⾃⼰分离,也就是说参数thread可以是线程组内所有的线程ID:
注意:⼀个线程不能既是joinable⼜是分离的。
举例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void* thread_run(void* reg) {
pthread_detach(pthread_self());
char* str = (char*)reg;
printf("%s\n", str);
return NULL;
}
int main()
{
pthread_t tid;
if(pthread_create(&tid, NULL, thread_run, "new thread is run ...") != 0 ) {
printf("pthread_create is error\n");
exit(1);
}
sleep(1);
if(pthread_join(tid, NULL) == 0) {
printf("线程等待成功!\n");
return 0;
}
else {
printf("线程等待失败!\n");
return 1;
}
return 0;
}
实验结果:
线程等待失败,表示分离成功。