【前言】
我们知道,进程在各⾃自独⽴立的地址空间中运⾏行,进程之间共享数据需要⽤map或者进程间通 信机制,接下来我们学习如何在⼀一个进程的地址空间中执⾏行多个线程。(多个线程共享一个进程内存空间,通过页表映射,可以看到一块物理内存,那这有会引入什么问题呢?临界资源的保护,同步、互斥机制)有些情况需要在⼀一个进
程中同时执 ⾏多个控制流程,这时候线程就派上了⽤用场。
一、什么是线程?
线程就是进程的若干个执行流,一个进程在某一时刻只能做一件事情,引入线程后,就可以做多样事情,比如说,我用迅雷下载电影,迅雷一边可以与我互动,一边可以继续下载我要的资源。能给我们带来很多好处。在linux当中其实不存在线程的,linux使用进程模拟线程,站在CPU的角度线程都是进程,所以我们把线程叫做轻量级进程(lwp)。
二、进程与线程的区别?
进程程是操作系统分配资源的单位,线程是调度的基本单位。进程是独占资源的,而线程是可以共享进程的资源的。
在一个已有进程中创建一个线程比创建一个进程所需要的时间少的多。
终止一个线程比终止一个进程所需的时间少的多
同一个进程内线程的切换比进程间切换时间少得多
线程的缺点:
不够稳定(由进程的独立性可知)
相反,进程的优点:独立性强,不过彼此间实现进程间通信比较困难。
三、进程与线程的联系
由于同⼀一进程的多个线程共享同⼀一地址空间,因 此Text Segment、Data Segment都是共享的,如果定义⼀一个函数,在各线程中都可以调⽤用,如
果定义⼀一 个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环 境:
1. ⽂文件描述符表
2. 每种信号的处理⽅方式(SIG_IGN、SIG_DFL或者⾃自定义的信号处理函数)
3. 当前⼯工作⽬目录
4. ⽤用户id和组id
但有些资源是每个线程各有⼀一份的:
1. 线程id
2. 上下⽂文,包括各种寄存器的值、程序计数器和栈指针
3. 栈空间
4. errno变量
5. 信号屏蔽字
6. 调度优先级
我们将要学习的线程库函数是由POSIX标准定义的,称为POSIX thread或者pthread。在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项。
四、线程的控制 -创建、等待、 终止
1.创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功返回0,失败返回错误码。可以通过char *strerror(int errnum)函数提取错误信息。
thread:用来保存线程的id。属于一个输出型参数,将线程的id保存起来。
attr:用来修改线程的属性。如果为NULL的时候表示创建的线程是默认属性。
start_routine:start_routine是一个函数指针,它指向一个返回值为void*,参数为void* 函数。该线程创建好了以后就会执行这个函数。当start_routine返回之后该线程就退出了。
arg:是start_routine所指向函数的参数。
在⼀一个线程中调⽤用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下 执行,⽽而新的线程所执⾏行的代码由我们传给pthread_create的函数针start_routine决 定。start_routine函数接收⼀一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为voi d*,这个指针按什么类型解释由调⽤用者⾃自⼰己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调⽤用者⾃自⼰己定义。start_routine返回时,这个线程就退出了,其它线程 可以 调⽤用pthread_join得到start_routine的返回值,类似于⽗父进程调⽤用wait(2)得到⼦子进程的退出 状态。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯⼀一的,调⽤用getpid(2)可以获得当前进程的id,是⼀一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯⼀一的,在不同的系统中thread_t这个类型有不同的实现,它可能是⼀一个整数值,也可能是⼀一个结构体,也可能是⼀一个 地址,所以不能简单地当成整数⽤用printf打印,调⽤用pthread_self(3)可以获得当前线程的id。
2.线程的终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适⽤用,从main函数return相当于调用exit。
- 一个线程可以调用pthread_cancel终⽌止同一进程中的另一个线程。
- 线程可以调用pthread_exit终⽌⾃己。
另外,若线程调用exit,整个进程就会结束,起作用就相当于main函数中调用return .
我们主要学习一下pthread_cancel和pthread_exit函数。
void pthread_exit(void *retval);
int pthread_cancel(pthread_t thread);//可以自己取消自己,也可以被别人取消。
retval是void *类型,和线程函数返回值的用法⼀一样,其它线程可以调用pthread_join获得这个指针,得到这个返回值。(pthread_exit用来终止线程自身,参数是返回的错误码,想要获得这个错误码,可以通过pthread_join来获得。)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是⽤用malloc分 配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
3线程等待
在进程当中我们提到过,如果我们当子进程结束以后,这个时候需要让父进程得到信息,然后由父进程去进行资源的回收,以及后续的处理,所以我们要确保子进程先结束,然后父进程再结束,否则会出现僵死状态,当值内存泄漏的问题。所以进程当中我们提到了wait和waitpid函数。
线程当中同样有上述的问题,当你的新线程结束,你的主线程也是需要等待,然后回收新线程的资源及其他信息。这样就能确保内存不泄露。所以这里使用一个函数pthread_join函数来进行等待。
int pthread_join(pthread_t thread, void **retval);
4.获取线程id
pthread_t pthread_self();
在一个线程中调用这个函数会返回该线程的id。
五、线程的可分离与可结合属性
在任何⼀一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。
一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。
相反,
一个分离的线程是不能被其他线程回收或杀死的,它的存储器 资源在它终⽌止时由系统自动释放。
默认情况下,线程被创建成可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调⽤用pthread_join;要么通过调⽤用pthread_detach函数被分离。
如果⼀一个可结合线程结束运⾏行但没有被join,则它的状态类似于进程中的Zombie Process,即还有⼀一部分资源没有被回收,所以创建线程者应该调⽤用pthread_join来等待线程运⾏行结束,并可得到线程的退出代码,回收其资源。 由于调⽤用pthread_join后,如果该线程没有运⾏行结束,调⽤用者会被阻塞,
在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的连接请求),这时可以在⼦子线程中加⼊入代码 pthread_detach(pthread_self())
或者⽗父线程调⽤用
pthread_detach(thread_id)(⾮非阻塞,可⽴立即返回)
这将该⼦子线程的状态设置为分离的(detached),如此⼀一来,该线程运⾏行结束后会⾃自动释放所有资源。
六、线程控制代码的实现
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<errno.h>
//talk three kinds of return
void* thread1(void*val)
{
printf("thread1 1 is returning ...\n");
return (void*)1;
}
void* thread2(void*val)
{
printf("thread2 is exiting...\n");
pthread_exit((void*)2);
}
void* thread3(void*val)
{
while(1)
{
printf("thread3 is running,wait for being canceled...\n");
sleep(1);
}
return NULL;
}
void* thread_run(void* _val)
{
pthread_detach(pthread_self());
printf("%s\n",(char*)_val);
return NULL;
}
int main()
{
pthread_t tid;
void* tret;//get return stat
//thred1
pthread_create(&tid,NULL,thread1,NULL);
pthread_join(tid,&tret);
printf("thread return ,thread id is %u,return code is %d\n",tid,(int)tret);
//thread2
pthread_create(&tid,NULL,thread2,NULL);
pthread_join(tid,&tret);
printf("thread return ,thread id is %u,exit code is %d\n",tid,(int)tret);
//thread3
pthread_create(&tid,NULL,thread3,NULL);
pthread_cancel(tid);
pthread_join(tid,&tret);
printf("thread return ,thread id is %u,cancel code is %d\n",tid,(int)tret);
//detach
int tret1=pthread_create(&tid,NULL,thread_run,"thread_run is running");
if(tret1!=0)
{
printf("create thread error!.info is %s\n",strerror(tret));
}
//wait
int ret=0;
sleep(1);
if(0==pthread_join(tid,NULL))
{
printf("pthread wait sucess1\n");
}
else
{
printf("pthread_ wait failed\n");
ret=1;
}
return ret;
}
Makefile文件
test:test.c
gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f test