在Linux kernel中是不存在线程的,或者说没有真正意义上的线程,Linux下的线程都是用进程来模拟的,线程的实现就是多个共享数据信息的进程,我们称之为“轻量级进程”。
多线程是指操作系统在单个进程内支持多个并发执行路径的能力,线程属于进程的的多个执行流。linux多线程设计包括多任务程序的设计,并发程序设计,网络程序设计,数据共享等。
Linux系统下的多线程遵循POSIX线程接口,称为POSIX thread 或者 pthread。在linux下线程函数位于libpthread共享库中,需要加上头文件pthread.h,连接时需要使用-lpthread。
Linux下pthread的实现是通过系统调用clone ( ) 来实现的,clone是linux所特有的系统调用,它的使用方式类似于fork。
由于同一个进程的多个线程共享同一地址空间,因此Text Segment、Data Segment 都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问,除此之外,各线程共享以下进程资源和环境:
(1)文件描述符(fd)
(2)每种信号的处理方式(SIG_IGT、SIG_DEL或者自定义的信号函数)
(3)当前的工作目录
(4)用户id和组id
但是,有一些资源是各个线程私有一份的:
(1)线程id
(2)上下文,包括各种寄存器的值、程序计数器和栈指针
(3)栈空间
(4)errno变量
(5)信号屏蔽字
(6)调度优先级
线程由内核自动调度,并且内核通过一个整数ID来识别线程。同基于I/O多路复用一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的整个内容,包括代码、数据、堆、共享库和打开的文件。
多线程的执行模型在某些方面和多进程是相似的,如下图所示:
每个进程开始生命周期都是单一线程,整个线程称之为主线程(main thread)。在某一时刻,主线程创建一个对等线程(peer thread),从这个时间点开始,两个线程就并发地执行。最后,因为主线程执行一个慢速系统调用(read 或 sleep)等,控制就会通过上下文切换传递到对等线程,对等线程会执行一段时间,然后传递回主线程。
线程执行时不同于进程。因为一个线程的上下文要比一个进程的上下文小得多,线程的上下文切换要比进程的上下文切换快的多;另一方面,线程不是按照严格的父子层次来组织的,和一个进程相关的线程组成一个对等(线程)池(pool),独立于其他线程创建的线程。
主线程和其他线程的区别仅在于它总是进程中第一个执行的线程,对等(线程)池概念的主要影响是:一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。另外,每个对等线程都能读写相同的共享数据。
线程通过调用 pthread_create 函数来创建其他线程,它的声明如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数[*thread]:输出型参数,它包含新创建线程的ID。新线程可以通过调用 pthread_self 函数来获得它自己的线程ID,但仅在进程内部有效。
pthread_t pthread_self(void);
参数[*attr]:线程的属性(通常设置为NULL)。
参数[*start_routine]:函数指针指向所要创建的线程。
参数[*arg]:属于输入型参数,传递给该函数的指针。
返回值:成功返回0,失败返回错误码。
下面我们创建一个线程来验证一下:
#include
#include
#include
void *thread_run(void *arg)
{
printf("peer thread pid is %d,tid is %u\n",(int)getpid(),(unsigned long long)pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_run, NULL);
if(ret != 0)
{
printf("create thread error:%s\n",strerror(ret));
exit(0);
}
printf("main thread is:%d,tid is:%u\n",(int)getpid(),(unsigned long long)pthread_self());
sleep(1);
return 0;
}
运行结果如下:
从执行结果可以看出,主线程与新创建线程的 pid 都是2802,即主线程与从线程从属于同一个进程,也进一步说明,在Linux下,没有真正意义上的线程。
需要注意的是 pthread_self 所获取到的是相对于进程的线程控制块的首地址,只是用来描述统一进程当中的不同的线程。而真正的内核当中的线程ID,对于多线程进程来说,每个tid实际上是不一样的。
线程终止有三种方式:
(1)使用return可以终止一个线程,但是如果是在main函数中调用 return 的话,相当于进程退出,那么所有线程都会退出,相当于调用 exit 或者 _exit 函数。在任意一个线程中调用 exit 或 _exit 都会使所有线程退出。
#include
#include
#include
void *thread_run1(void *arg)
{
printf("thread1 running...\n");
return;
}
int main()
{
pthread_t tid;
void *retCode;
pthread_create(&tid, NULL, thread_run1, NULL);
int ret_return = pthread_join(tid, &retCode);
printf("join_ret : %d\n",ret_return);
printf("thread return,thread id is : %u,return code is : %d\n",(unsigned long)tid,(int*)retCode);
return 0;
}
运行结果:
(2)void pthread_exit(void *retval)
在线程中调用 pthread_exit ( ) 可以使线程自己退出。退出时的信息可以通过参数传出去,被等待它的线程获取。
#include
#include
#include
void *thread_run2(void* arg)
{
printf("thread2 running...\n");
pthread_exit((void*)2);
}
int main()
{
pthread_t tid;
void *retCode;
//pthread_exit
pthread_create(&tid, NULL, thread_run2, NULL);
pthread_join(tid,&retCode);
printf("thread exit,thread id is:%u,return code is:%d\n",(unsigned long)tid,(int*)retCode);
return 0;
}
运行结果:
注意:pthread_exit 或 return 返回的指针指向的内存单元必须是全局的或者用 malloc 分配出来的,不能在线程函数的栈上分配,因为当线程得到整个返回指针时,线程已经退出。
(3)int pthread_cancel ( pthread_t tid )
在一个线程中调用 pthread_cancel 函数可以终止另一个线程。假设A线程是被 pthread_cancel 异常终止的,则 pthread_join 获取的线程A的退出码就是 PTHREAD_CANCEL,这个宏的值是-1,可在 pthread.h 头文件中找到。
#include
#include
#include
void *thread_run3(void* arg)
{
while(1)
{
printf("pthread3 running...wait cancel...\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
void* retCode;
//pthread_cancel
pthread_create(&tid, NULL, thread_run3, NULL);
sleep(3);
pthread_cancel(tid);
int ret_cancel = pthread_join(tid,&retCode);
printf("ret cancel:%d\n",ret_cancel);
printf("thread return,thread id is:%u,return code is :%d\n",(unsigned long)tid,(int*)retCode);
return 0;
}
运行结果:
返回值:成功返回0,失败返回错误码。
在之前进程中,子进程结束时,需要父进程去回收资源,以及后续的处理,即确保子进程先于父进程退出,否则会出现僵尸进程,造成内存泄露等问题,进程可以调用 wait 和waitpid 函数来防止僵尸进程的出现,那么线程亦是如此。
线程与进程又异曲同工之秒,或者可以直接认为它就是进程,同样,新线程的释放要先于主线程,因此,我们引入pthread_join函数。
int pthread_join(pthread_t thread, void **retval);
参数[thread]:要等待的目标线程的id。
参数[**retval]:要等待的目标线程的退出码。线程在运行时,只能运行完且返回一个退出码,如果一个线程出错,则整个进程挂掉(原因在于进程是操作系统执行的基本单位)。对 void* 也不能解引用,因为解引用之后,无法知道其大小,因此需要使用 void** 二级指针。
当然,线程等待属于阻塞式等待,即新线程不退出,主线程就一直等待,主线程退出,则进程退出。
和Unix的 wait 函数不同,pthread_join 函数只能等待一个指定的线程终止,没有办法让 pthread_join 等待任意一个进程终止。
注意:线程可以被等待,也可以使用终止来结束新线程:
(1)使用return (void*),如果在主线程处使用就等同与exit。
(2)使用 pthread_exit(void* retval),等同于从线程退出。
(3)使用 pthread_cancel 取消这个线程,即线程允许被取消,其退出码为-1。
在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。
一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是没有被释放的。
相反,一个分离的线程是不能被其他线程回收或者杀死的,它的存储器资源在它终止时由系统自动释放。一个可分离的线程是不能被等待的。
默认情况下,线程被创建为可结合的。为了避免存储器泄露,每个可结合线程都应该要么被其他线程显示回收,要么通过调用 pthread_detach 函数将线程分离。
如果一个可结合的线程运行结束没有被join,则它的状态类似于僵尸进程,也就是还有一部分资源没有被回收。
调用pthread_join之后,如果该线程没有运行结束,则调用者会被阻塞,当主线程退出后,就相当于进程退出了,这时候不管是可结合的或者可分离的进程都会被回收结束掉。
int pthread_detach(pthread_t thread);
pthread_detach总是返回0。分离分为新线程的分离和主线程的分离,下面就两者的分离作以阐述:
(1)新线程的分离:新线程分离后,主线程可能不知道新线程的分离,因此主线程可能会一直去join等待。
#include
#include
#include
void *pthread_run(void *arg)
{
pthread_detach(pthread_self());
printf("new thread->pid:%d,tid:%ld\n",getpid(),pthread_self());
return;
}
int main()
{
pthread_t tid;
int err = pthread_create(&tid, NULL, pthread_run, NULL);
if(err)
{
printf("%s\n",strerror(err));
return -1;
}
int ret = 0;
sleep(2);
if(pthread_join(tid,NULL) == 0)
{
printf("wait sucess\n");
ret = 0;
}
else
{
printf("wait failure\n");
ret = 1;
}
return ret;
}
运行结果:
等待失败在所难免的,新线程的分离对于主线程来说是看不到的,主线程会一直去等待新线程,殊不知新线程已经被操作系统释放。
(2)主线程的分离:此时主线程已经确认与新线程分离,将不再等待新线程。
#include
#include
#include
void *thread_run(void *arg)
{
printf("%s\n",(char*)arg);
return;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, "thread run...");
sleep(2);
pthread_detach(tid);
sleep(2);
printf("join_ret:%d\n",pthread_join(tid,NULL));
return 0;
}
运行结果:
主线程已经知道自己与新线程分离,因此将不再回收新线程,而是主线程执行完就直接退出。
(1)线程是在进程内部运行的,实际上是在进程的地址空间上运行的。
(2)线程是进程的一个分支,创建线程的成本较低。
(3)线程是操作系统调度的基本单位,进程是系统资源分配的基本单位。
(4)线程属于轻量级的进程。