什么是线程
先来举一个我们生活中的实例,我们都使用过一个强大的软件—迅雷。那你必然知道迅雷有一个边下边播的功能,我们在下载的时候还能同时进行观看。这就是一个多线程实例。
线程是进程内部的执行分支。
- 打开迅雷软件—–向系统内核索要资源,启动“迅雷”进程,。
- 开始下载一个电影—–从索要的资源中调度分配一部分资源,启动下载线程。
- 开始播放电影—–再索要的资源中调度分配一部分资源次从,启动播放线程。
由上我们就不难理解,进程是资源分配的基本单位,线程是调度的基本单位。
线程有主次之分,主线程负责分配调度,新线程负责执行,每个进程只有一个主线程,新线程可以有多个。当主线程被杀死或者运行结束时,整个进程随之结束,当然那么多的线程也会结束。
我们可以建一个模型来理解上述文字:
当主线程启动程序关闭时,整个迅雷软件就会关闭,下载,播放线程当然也就关闭。我们不可能在还下载完电影就关闭下载软件吧?
所以这里要非常注意一点,我们在进行多线程编程时,要让主线程等待新线程运行结束后再结束。
线程属性
在Linux在没有实质意义上的线程,Linux是通过进程来模拟线程,而这些模拟出的线程又叫轻量级进程。我们知道,每个进程都有一个进程控制块PCB,操作系统通过控制PCB来控制进程,进程通过PCB与地址空间,页表的合作来访问物理内存,如下图:
当我们创建多个线程时,就会拥有过个PCB,这些PCB(包括主线程)共享同一份地址空间,也就是说这些线程共享同一份进程资源和环境。如下图:
这些数据包括:文件描述符表,每种信号的处理方式,当前工作目录,用户ID和组ID等。
但是有些资源是每个线程独有一份的:
线程ID,上下文,各种寄存器的值,程序计数器,栈指针,栈空间,error变量,信号屏蔽字,调度优先级等。尤其注意这里的线程ID和栈空间。
线程操作函数
因为Linux上的线程函数位于libpthread共享库中,因此在编译时要加上-lpthread。例如:
gcc -o test test.c -lpthread
具体原因可参照博客:
http://blog.csdn.net/llzk_/article/details/55519242
创建线程
在上文中我提到过一个名字叫线程ID,每个线程都有一个自己的线程ID,就像每个线进程都有一个进程ID一样,进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
线程ID是由pid_t数据类型来表示,是一个非负整数。线程ID是由数据类型 pthread_t来表示。
函数pthread_create用来创建一个线程。
int pthread_create(pthread_t *thread,const pthread_attr_t* attr,void*(*start routine)(void*),void *arg)
参数:
- thread,为一个pthread_t类型的指针,指向一个内存单元。新创建的线程的线程ID会被设置在thread指向的内存单元。
- attr,用于定制各种不同的线程属性。
- start routine,为一个函数指针,新创建的线程从start routine函数的地址开始运行。该函数的参数是一个无类型指针参数。这个参数后面我们会提到。
- arg,为一个无类型指针,当我们需要为第三个参数函数指针传参时,那么便需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。可以理解为此arg参数就是上面函数指针的参数。
头文件:
#include<pthread.h>
返回值:成功时返回0,失败时返回错误码。以前学的系统函数都是成功返回0,失败返回-1,而错误码保存在全局变量error中,pthread库的函数都是通过返回值返回错误码,虽然每个线程也都有一个error,但这是为了兼容其他函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。
示例代码:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
//新线程,每秒打印一次,共打印五次。
void* thread_run(void *arg)
{
int count = 0;
while(count++ < 5)
{
sleep(1);
printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
printf("thread is over...\n");
return NULL;
}
int main()
{
//主线程
printf("phread\n");
pthread_t tid;
int ret = pthread_create(&tid,NULL,thread_run,pthread_self());
if(ret != 0)
{
printf("pthread_creat error\n");
return -1;
}
else
{
sleep(1);
printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
int exitCode;
pthread_join(tid,(void**)&exitCode);//用来等待新线程的结束
printf("main is over...%d\n",exitCode);
return 0;
}
上面代码中用到了一个pthread_join函数,此函数的功能就是等待一个线程的结束。此例中让主线程等待新线程的结束。第一个参数为新线程的线程ID,第二个参数用来接收新线程的退出码。
执行结果应为主线程打印1次,新线程打印五次:
线程终止
线程终止有三个方式。
简单的从启动例程中返回,返回值是线程的退出码。
这种方式是最简单的方式,上面例子thread_run函数直接return就是这种方式。线程可以被同一进程中的其他线程调用pthread_cancle终止。
例如在主线程中终止新线程,仍为上面的例子。pthread_cancle的参数为要结束线程的线程ID。
代码:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
void* thread_run(void *arg)
{
int count = 0;
while(count++ < 5)
{
sleep(1);
printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
printf("thread is over...\n");
return NULL;
}
int main()
{
printf("phread\n");
pthread_t tid;
int ret = pthread_create(&tid,NULL,thread_run,pthread_self());
if(ret != 0)
{
printf("pthread_creat error\n");
return -1;
}
else
{
sleep(1);
printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
pthread_cancel(tid);//在主线程中结束子线程
int exitCode;
pthread_join(tid,(void**)&exitCode);
printf("main is over...%d\n",exitCode);
return 0;
}
执行结果应为主线程打印一次后直接结束,新线程退出码为-1。如果一个线程被其他线程调用pthread_cancel异常终止掉,它返回的退出码将是常数PTHREAD_CANCELED。这个宏被定义在pthread.h中,值为-1。
- 线程可以调用pthread_exit自己终止自己。
pthread_exit的参数为退出码。注意,pthread_exit的参数或者新线程return的指针所指向的内存单元必须是全局的或者由malloc分配的,因为当其他线程pthread_join得到这个返回指针时新线程函数已经退出了。
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
void* thread_run(void *arg)
{
int count = 0;
pthread_t mid = *(pthread_t*)arg;
while(count++ < 5)
{
sleep(1);
printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
printf("thread is over...\n");
pthread_exit((void*)10);
}
int main()
{
printf("phread\n");
pthread_t tid;
int ret = pthread_create(&tid,NULL,thread_run,pthread_self());
if(ret != 0)
{
printf("pthread_creat error\n");
return -1;
}
else
{
sleep(1);
printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
int exitCode;
pthread_join(tid,(void**)&exitCode);
printf("main is over...%d\n",exitCode);
return 0;
}
执行结果为:程序正常执行,但新线程退出码为10。
一般情况下,线程终止后,资源不会被立即释放,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止,这时系统擦才会释放它所占用的资源。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用额度所有资源,而不保留终止状态,这时主线程也不必一直阻塞等待,可以去做其他的工作。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。对一个detach的线程调用pthread_join或者pthread_detach都可以把该线程置为detach状态,也就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
这里要补充一个有关线程的概念:
分离线程
线程是可分离的(detached)或者结合的(joinable),一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,他的存储器资源(eg:栈)是不可释放的。相反一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。
默认情况下,线程被创建成可结合的。为了避免村吃起的泄露,每个可结合线程都应该被显示的回收,即调用pthread_join。若没有被join,则会造成资源,内存的泄露。
因为调用pthread_join后,如果应该被join的新线程没有运行结束,调用者会被阻塞,我们有时候并不希望如此。这时可在新线程中加入代码pthread_detach(pthread_self())或者主线程调用pthread_detach(thread_id)来将新线程设置成可分离的,如此一来,新线程运行结束后会自动释放所有资源。
话说回来。除去以上三种线程终止方式外,当任意一个线程调用exit或者_exit时,则整个进程的所有线程都终止。
我们能不能用新线程结束主线程呢?
答案是可以的,但是新线程如何获取主线程的线程ID呢?
这时就用到了pthread_create函数的第四个参数。将主线程的线程ID作为第四个参数传入。在新线程中就可以以参数msg获取到。具体实现看代码:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
void* thread_run(void *arg)
{
int count = 0;
pthread_t mid = *(pthread_t*)arg;//直接解引用msg获取主线程ID
pthread_cancel(mid);
while(count++ < 5)
{
sleep(1);
printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
printf("thread is over...\n");
return NULL;
}
int main()
{
printf("phread\n");
pthread_t tid;
pthread_t mid = pthread_self();//调用self函数获取主线程ID
int ret = pthread_create(&tid,NULL,thread_run,(void*)&mid);//将主线程ID作为第四个参数传入
if(ret != 0)
{
printf("pthread_creat error\n");
return -1;
}
else
{
sleep(1);
printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
}
int exitCode;
pthread_join(tid,(void**)&exitCode);
printf("main is over...%d\n",exitCode);
return 0;
}
运行结果为:主线程不运行,新线程运行5次后结束。