线程概念
线程和进程的关系
1.轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone。
2.从内核里看进程和线程是一样的,都有各自不同的PCB(但进程id号是一样的),但是PCB指向内存资源的三级页表是相同的(共用了地址空间)。
3.进程可以蜕变成线程(一个a.out进程,运行时分离出一个线程,那原来的a.out也就蜕变成线程了)。
4.线程就是寄存器和栈。
5.在linux下,线程是最小的执行单位(调度单位);进程是最小的分配资源单位(0~4G地址空间,时间片)。
系统是给进程分配0~4G地址空间,如果同一个进程分出线程,PCB指向的三级页表是相通的,
ps -Lf pid 查看这个进程里面有多少线程 LWP轻量级进程id
ps -eLf 查看所有
线程间共享资源
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间
TEXT(代码段)、data(已初始化全局变量)、bss(未初始化全局变量)、堆、共享库
栈不共享
栈的管理方式:CPU中有个ESP栈指针寄存器,占四个字节,用来保存地址,通过地址的位移来操作栈空间
PC程序计数器,也是四个字节,用来保存地址,这个保存的是CPU给到代码段的那个位置取指令的地址
PCP里面有内核栈,在保存处理器现场的时候,用来保存每个线程分别的ESP和PC,使得线程之间可以正常切换。
线程间非共享资源
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级
线程优缺点
优点
提高程序的并发性
开销小,不用重新分配内存
通信和共享数据方便
线程切换比进程块,不用重新映射0~4G的地址空间
缺点
线程不稳定(库函数实现)
线程调试比较困难(gdb支持不好)
线程无法使用unix经典事件 例如信号。
查看manpage关于pthread的函数
man -k pthread
安装pthread相关manpage
sudo apt-get install manpages-posix manpages-posix-dev
线程原语
pthread_create
创建线程
#include
int pthread_create(pthread_t *tread, const pthread_attr_t *attr,
void *, void *arg);
pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
const pthread_attr_t *attr: 线程属性设置, 如使用默认属性,则传NULL
void *:函数指针,指向新线程应该加载执行的函数模块
void *arg:指定线程将要家在调用的那个函数的参数
返回值:成功返回0 ,失败返回错误号。
gcc编译的时候要加上 -lpthread
pthread_self
获取调用线程tid(进程内部有效)
pthread_t pthread_self(void );
例子:
#include
#include
#include
#include
#include
#include
struct STU {
int id;
char name[20 ];
};
void *thread_do(void *arg)
{
struct STU *s = (struct STU *)arg;
while (1 ) {
printf ("id = %d, name = %s, tid = %x
" , s->id, s->name, (int )pthread_self());
sleep(1 );
}
}
int main(void )
{
pthread_t tid;
struct STU student = {12 , "xiaoming" };
int err;
err = pthread_create(&tid, NULL, thread_do, (void *)&student);
if (err != 0 ) {
fprintf (stderr, "can't create thread: %s
" , strerror(err));
}
while (1 ) {
printf ("main thread pid = %x, child tid = %x
" , (int )pthread_self(), (int )tid);
sleep(1 );
}
return 0 ;
}
当pthread_create里面的tid没有返回,而tread_do函数已经执行时,此刻,tread_do里的pthread_self得到的是线程id,而tid还没有被复制,里面是垃圾值, 例如把tid定义成全局变量,并且在线程函数调用的时候,可能会出问题。
pthread_exit
调用线程退出函数
如果使用exit时,任何线程里exit导致进程退出,其他线程工作没有结束就退出了,主控线程退出时不能return或exit,就不能关闭文件描述符,刷新缓冲区。
return 是只结束当前线程
#include
void pthread_exit(void *retval);
void *retval:线程退出时传递出来的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。
_exit是exit的底层函数,使用_exit退出函数,不会刷新缓冲区,是和read、write一个层面的。
pthread_jojin
回收线程,当线程没有结束时,调用该函数的线程将挂起等待
#include
int pthread_join(pthread_t thread, void **retval);
pthread_t thread:回收线程的tid
void **retval:接收退出线程传递出的返回值
返回值:成功返回0 ,失败返回错误号
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方式终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数
例子:
#include
#include
#include
#include
#include
struct STU {
int id;
char name[20 ];
};
void *do_thread_1(void *arg)
{
printf ("I am %lx
" , pthread_self());
sleep(3 );
pthread_exit((void *)5 );
}
void *do_thread_2(void *arg)
{
struct STU *p = (struct STU*)arg;
p->id = 20 ;
strcpy (p->name, "xiaoming" );
printf ("I am %lx
" , pthread_self());
sleep(3 );
pthread_exit((void *)p);
}
int main(void )
{
pthread_t tid;
int *res;
struct STU s = {100 , "zhangsan" };
struct STU *p;
pthread_create(&tid, NULL, do_thread_1, NULL);
printf ("wait for child thread_1
" );
pthread_join(tid, (void **)&res);
printf ("child thread return %d
" , (int )res);
pthread_create(&tid, NULL, do_thread_2, (void *)&s);
printf ("wait for child thread_2
" );
pthread_join(tid, (void **)&p);
printf ("child thread return %d, %s
" , p->id, p->name);
return 0 ;
}
pthread_cancel
在进程内的某个线程可以干掉同进程里的另一个线程。
#include
int pthread_cancel(pthread_t thread);
被取消的线程,退出值,第一在Linux的pthread库中常数PTHREAD_CANCELED的值是-1.
#include
#include
#include
#include
void *thr_fn1(void *arg)
{
printf ("tread 1 returning
" );
return (void *)1 ;
}
void *thr_fn2(void *arg)
{
printf ("thread 2 exiting
" );
pthread_exit((void *)2 );
}
void *thr_fn3(void *arg)
{
while (1 ) {
}
}
int main(void )
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fn1, NULL);
pthread_join(tid, &tret);
printf ("tread 1 exit code %d
" , (int )tret);
pthread_create(&tid, NULL, thr_fn2, NULL);
pthread_join(tid, &tret);
printf ("tread 2 exit code %d
" , (int )tret);
pthread_create(&tid, NULL, thr_fn3, NULL);
sleep(3 );
pthread_cancel(tid);
pthread_join(tid, &tret);
if (tret == PTHREAD_CANCELED)
printf ("I am cancel
" );
printf ("tread 3 exit code %d
" , (int )tret);
return 0 ;
}
如果把thr_fn3函数的while里面的两句话全部注释掉,在运行,会发现线程thr_fn3不会挂掉,这是因为第三个线程没有进入内核进行操作,所以没有触发cancel,这里可以类比信号,此时cancel这个操作还没有响应到第三个线程上,只有第三个线程进入内核的时候才会响应这个时间,线程才会退出,printf->write->system_write->内核
,像n++
这种操作就没有进入内核
如果线程内本就没有进入内核的操作,但还是需要使用pthread_cancel这个函数来干掉他,那就让线程调用pthread_testcancel()
,让内核去检测是否需要取消当前线程
pthread_detach
分离线程
#include
int pthread_detach(pthread_t tid);
返回值:成功0 , 失败返回错误号。
一般情况下,线程终止后,其终止状态一直保留到其他线程地调用pthread_join获取他的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它所占用的所有资源,而不保留终止状态,这种线程也叫游离线程。不能对一个已经处在detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能在调用pthread_join了
#include
#include
#include
#include
#include
void *thr_fn(void *arg)
{
int n = 3 ;
while (n--) {
printf ("thread count %d
" , n);
sleep(1 );
}
return (void *)1 ;
}
int main(void )
{
pthread_t tid;
void *tret;
int err;
pthread_create(&tid, NULL, thr_fn, NULL);
while (1 ) {
err = pthread_join(tid, &tret);
if (err != 0 )
fprintf (stderr, "thread %s
" , strerror(err));
else
fprintf (stderr, "thread exit code %d
" , (int )tret);
sleep(1 );
}
return 0 ;
}
pthread_equal
比较两个线程的tid是否相等
#include
int phtread_equal(pthrad_t t1, pthread_t t2);
返回值,相同返回一个非零值,不相等返回0
线程属性
默认属性已经可以解决绝大多是问题。如果我们队程序i的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程的栈大小来降低内存的使用,增加最大线程个数。
早期的线程属性定义,方便理解。
typedef struct {
int etachstate;
int schedpolicy;
structsched_param schedparam;
int inheritsched;
int scope ;
size_t guardsize;
int stachaddr_set;
void * stachaddr;
size_t stacksize;
}pthread_attr_t;
pthread_attr_init
初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
线程的分离状态
int pthread_attr_setdetachstate(pthread_attr_t *attr , int detachstate); // 设置线程属性,分离or 非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr , int *detachstate ); // 获取线程属性,分离or 非分离
pthread_attr_t *attr :被已经初始化的线程属性
int detachstate:可选为PTHREAD_CREATE_DETACHED(分离线程)和PTHREAD_CREATE_JOINABLE(非分离线程)
如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号,为了避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等一会。
线程的栈地址和栈大小
#include
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t stacksize);
attr 指向一个线程属性的指针
stackaddr 返回获取的栈地址
stacksize 返回获取的栈大小
返回值,若成功返回0 ,否则返回错误的编号
例子
#include
#include
#include
#include
#define SIZE 0x10000
void *do_thread(void *arg)
{
printf ("hello
" );
while (1 )
sleep(1 );
}
int main(void )
{
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);
printf ("stackadd = %p
" , stackaddr);
printf ("stacksize = %x
" , (int )stacksize);
pthread_attr_getdetachstate(&attr, &detachstate);
if (detachstate == PTHREAD_CREATE_DETACHED)
printf ("thread detached
" );
else if (detachstate == PTHREAD_CREATE_JOINABLE) {
printf ("thread join
" );
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
} else
printf ("thread unknown
" );
while (1 ) {
stackaddr = calloc (SIZE, 1 );
if (stackaddr == NULL) {
perror("calloc" );
exit (1 );
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid, &attr, do_thread, NULL);
if (err != 0 ) {
printf ("%s
" , strerror(err));
exit (1 );
}
printf ("%d
" , i++);
}
pthread_attr_destroy(&attr);
return 0 ;
}
注意
1.主线程退出其他线程不退出,主线程应调用pthread_exit
2.避免僵线程
3.malloc和mmap申请的内存可以被其他线程释放
4.如果线程终止时没有释放加锁的互斥量,则该互斥量不能再被使用
5.应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
6.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。