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
创建线程
#include <pthread>
int pthread_create(pthread_t *tread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
const pthread_attr_t *attr: 线程属性设置, 如使用默认属性,则传NULL
void *(*start_routine)(void *):函数指针,指向新线程应该加载执行的函数模块
void *arg:指定线程将要家在调用的那个函数的参数
返回值:成功返回0,失败返回错误号。
gcc编译的时候要加上 -lpthread
获取调用线程tid(进程内部有效)
pthread_t pthread_self(void);
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
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\n", s->id, s->name, (int)pthread_self());
//打印结构体的内容,和本线程的tid
sleep(1);
}
}
int main(void)
{
pthread_t tid;
struct STU student = {12, "xiaoming"};
int err;
err = pthread_create(&tid, NULL, thread_do, (void *)&student);//创建线程,线程的执行函数是thread_do,向函数传的是一个结构体的地址
if(err != 0) {
fprintf(stderr, "can't create thread: %s\n", strerror(err));//习惯性定义,在线程中使用fprintf
}
while (1) {
printf("main thread pid = %x, child tid = %x\n", (int)pthread_self(), (int)tid);
//打印本线程的id和子线程的tid
sleep(1);
}
return 0;
}
当pthread_create里面的tid没有返回,而tread_do函数已经执行时,此刻,tread_do里的pthread_self得到的是线程id,而tid还没有被复制,里面是垃圾值, 例如把tid定义成全局变量,并且在线程函数调用的时候,可能会出问题。
调用线程退出函数
如果使用exit时,任何线程里exit导致进程退出,其他线程工作没有结束就退出了,主控线程退出时不能return或exit,就不能关闭文件描述符,刷新缓冲区。
return 是只结束当前线程
#include <pthread.h>
void pthread_exit(void *retval);
void *retval:线程退出时传递出来的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。
_exit是exit的底层函数,使用_exit退出函数,不会刷新缓冲区,是和read、write一个层面的。
回收线程,当线程没有结束时,调用该函数的线程将挂起等待
#include <pthread.h>
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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
struct STU {
int id;
char name[20];
};
void *do_thread_1(void *arg)
{
printf("I am %lx\n", pthread_self());
sleep(3);
pthread_exit((void *)5); //返回5
}
void *do_thread_2(void *arg)
{
struct STU *p = (struct STU*)arg;
p->id = 20;
strcpy(p->name, "xiaoming");
printf("I am %lx\n", 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\n");
pthread_join(tid, (void**)&res);
printf("child thread return %d\n", (int)res);
pthread_create(&tid, NULL, do_thread_2, (void*)&s);
printf("wait for child thread_2\n");
pthread_join(tid, (void **)&p);//因为这个返回的地址是二级指针,所以用&p接收
printf("child thread return %d, %s\n", p->id, p->name);
return 0;
}
在进程内的某个线程可以干掉同进程里的另一个线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
被取消的线程,退出值,第一在Linux的pthread库中常数PTHREAD_CANCELED的值是-1.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thr_fn1(void *arg)
{
printf("tread 1 returning\n");
return (void*)1;
}
void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
void *thr_fn3(void *arg)
{
while(1) {
//printf("thread 3 writing\n");
//sleep(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\n", (int)tret); //return
pthread_create(&tid, NULL, thr_fn2, NULL);
pthread_join(tid, &tret);
printf("tread 2 exit code %d\n", (int)tret); //pthread_exit
pthread_create(&tid, NULL, thr_fn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
if(tret == PTHREAD_CANCELED) //pthread_cancel
printf("I am cancel\n");
printf("tread 3 exit code %d\n", (int)tret);
return 0;
}
- 如果把thr_fn3函数的while里面的两句话全部注释掉,在运行,会发现线程thr_fn3不会挂掉,这是因为第三个线程没有进入内核进行操作,所以没有触发cancel,这里可以类比信号,此时cancel这个操作还没有响应到第三个线程上,只有第三个线程进入内核的时候才会响应这个时间,线程才会退出,
printf->write->system_write->内核
,像n++
这种操作就没有进入内核- 如果线程内本就没有进入内核的操作,但还是需要使用pthread_cancel这个函数来干掉他,那就让线程调用
pthread_testcancel()
,让内核去检测是否需要取消当前线程
分离线程
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值:成功0, 失败返回错误号。
一般情况下,线程终止后,其终止状态一直保留到其他线程地调用pthread_join获取他的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它所占用的所有资源,而不保留终止状态,这种线程也叫游离线程。不能对一个已经处在detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能在调用pthread_join了
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *thr_fn(void *arg)
{
int n = 3;
while (n--) {
printf("thread count %d\n", n);
sleep(1);
}
return (void *)1;
}
int main(void)
{
pthread_t tid;
void *tret;
int err;
pthread_create(&tid, NULL, thr_fn, NULL);
//pthread_detach(tid); //加上这句和不加上这句看看有什么差别
while(1) {
err = pthread_join(tid, &tret);
if(err != 0)
fprintf(stderr, "thread %s\n", strerror(err));
else
fprintf(stderr, "thread exit code %d\n", (int)tret);
sleep(1);
}
return 0;
}
比较两个线程的tid是否相等
#include <pthread.h>
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;
初始化线程属性
int pthread_attr_init(pthread_attr_t *attr); //初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr); //销毁线程属性所占的资源
#include <pthread.h>
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 <pthread.h>
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 <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 0x10000
void *do_thread(void *arg)
{
printf("hello\n");
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\n", stackaddr);
printf("stacksize = %x\n", (int)stacksize);
pthread_attr_getdetachstate(&attr, &detachstate); //获取默认属性中的detach状态
if (detachstate == PTHREAD_CREATE_DETACHED)
printf("thread detached\n");
else if (detachstate == PTHREAD_CREATE_JOINABLE) {
printf("thread join\n");
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //如果不是detach,就把他设置成detach
} else
printf("thread unknown\n");
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);//创建attr这种属性的线程
if (err != 0) {
printf("%s\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}
pthread_attr_destroy(&attr); //释放属性空间
return 0;
}
1.主线程退出其他线程不退出,主线程应调用pthread_exit
2.避免僵线程
3.malloc和mmap申请的内存可以被其他线程释放
4.如果线程终止时没有释放加锁的互斥量,则该互斥量不能再被使用
5.应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
6.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。