线程:Linux下, 线程是轻量级的进程,又称为LWP(light weight process),线程是进程中的一条执行流程。
- 同一个进程下的线程拥有的PCB是独立的,然而它们共享相同的地址空间。PCB中指向内存资源的三级页表是相同的。
- 线程之间可以并发的执行。
- 线程之间共享内存地址空间(未初始化的全局变量和静态变量(bss),data段,text段,heap空间,共享区等),文件描述符表,当前工作目录,用户ID和组ID,当前的工作目录。
- 不共享的资源:用户栈空间,线程id,信号屏蔽字,errno变量,处理器现场和栈指针,调度优先级
线程和进程的区别:
- 进程是资源分配(分配内存地址空间,文件描述符等)的单位,而线程是CPU调度的基本单位,如果一个进程中包含3个线程,CPU调度的时候是线程。
- 线程相对于进程来说开销小,数据共享方便(只有栈和寄存器是独有的,其他资源都是共享的),提高程序的并发性。
查看线程号
可以从LWP那一列看到 ,对应的线程号。这里的线程号和下文提到的线程ID是不一样的,线程号是用来给CPU使用的。
ps -Lf 进程id
线程常用的操作:
1.获取线程id号, pthread_t本质上是无符号长整型数据(%lu),线程ID。线程ID是用来标识同一个进程下面的不同的线程的。不同的进程之间,线程ID号是可以相同。
pthread_t pthread_self(void);
2.创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数介绍:
1.thread:线程的id号,它是传出参数,是在pthread_create函数内部获取到的,传入的应该是它的地址。
2.attr:线程的属性,一般设置为NULL即可,是常量。
3.start_routine:函数指针,指向创建的线程函数的地址。函数的返回值为void*,对应的参数也为void*
4.线程函数的参数:因此它的参数也为void*类型,如果不需要的话,设置为NULL即可。
传入参数和传出参数:传入参数指的在函数调用的时候,该值已经被定义好,作为参数给函数使用。传出参数一般给的都是地址,该值在传入的时候没有确定下来,而是在函数内部被确定下来,从函数中带出值。例子永远是最好的解释:这里的num1是传入参数,num2是传出参数哦。
int func(int num1, int *num2)
{
*num2 = 5;
return num1 + *num2;
}
int num1=4;
int num2
int result = func(num1, &num2);
对应的代码如下所示:需要注意的是这里必须要让主线程进行休眠,如果主线程不进行休眠的话, 直接return之后,进程的地址空间就会被销毁掉,由于线程之间的地址空间是共享的,所以线程也会被销毁掉,就无法看到打印的东西哦。
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include <errno.h>
#include <pthread.h>
void* tfun(void *arg)
{
printf("thread pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
int ret = pthread_create(&tid,NULL,tfun,NULL);
if(ret !=0)
{
perror("pthread_create_error");
}
sleep(1);
return 0;
}
循环创建多个线程:
代码如下所示:
void* tfun(void *arg)
{
long int i=(long int)(arg);
sleep(i);
printf("thread pid=%d,i=%d\n,tid=%lu\n",getpid(),i,pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
long int i;
for( i=0;i<5;++i)
{
int ret = pthread_create(&tid,NULL,tfun,(void*)i);
if(ret !=0)
{
perror("pthread_create_error");
}
}
printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(i);
return 0;
}
打印结果如下所示:
间隔一秒就会打印一次,由于线程是并发执行的,虽然线程创建的顺序是按照0,1,2,3,4的顺序,但是执行对应的线程函数时,是需要抢占CPU的。先被创建出来的,有可能先拿到CPU的使用权。宏观上来看,几个线程同时都会执行对应的回调函数,后面创建的线程睡眠的时间更长,比如进程1:sleep(1),经过1秒之后才打印。进程2,sleep(2),经过2秒之后打印。
错误的写法:
假设我们传递的是i的地址,那么对应的arg指向的其实是主线程中i的地址。需要注意的是,for循环中的i的变化,运行在用户态,当子线程去获取主线程中i元素的时候,对应的i值已经发生了改变。即回调函数中的对arg进行解引用的时候。
主要的原因是因为:使用pthread_create创建线程的时候,内部调用的是clone函数,需要不停的进行用户态和内核态的切换,因此消耗的时间是比较大的,而i值的改变速度又很快。
void* tfun(void *arg)
{
long int i=*((int*)arg);
sleep(i);
printf("thread pid=%d,i=%d\n,tid=%lu\n",getpid(),i,pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
long int i;
for( i=0;i<5;++i)
{
int ret = pthread_create(&tid,NULL,tfun,&i);
if(ret !=0)
{
perror("pthread_create_error");
}
}
printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(i);
return 0;
}
如何验证线程之间共享全局变量?
设置一个全局变量,在主线程中首先打印该值,在子线程中修改该全局变量的值,之后在主线程中查看该值,发现已经被修改啦!
3.进程的退出
void pthread_exit(void *retval)
- return:返回到调用者到那里去。main函数的调用者就是当前的out文件,对于线程来说:返回到pthread_create那里,pthread_create由主线程调用的。
- exit(0):退出当前的进程。
- pthread_exit(NULL),退出当前线程。
总体来说,调用线程的时候,采用第三种方法,可以用来退出单个的线程。
4. 阻塞等待当前线程的退出,获取线程退出的状态
注意:任意线程得到其他线程的pid都可以回收,没有父线程回收子线程的说法。等价于进程中的waitpid函数,该函数是父进程回收子线程。
参考以下文章:pthread_join函数
int pthread_join(pthread_t thread, void **retval);
args:
pthread_t thread: 被连接线程的线程号
void **retval : 指向一个指向被连接线程的返回值的指针的指针:二级指针,因为线程的返回值为void*,因此存储它的返回状态使用void**。类似于wait函数,存储进程的退出状态使用的是int型指针,退出时返回的是pid。
return:
线程连接的状态,0是成功,非0是失败
代码如下:需要注意的是创建的线程函数,它的返回值不是局部变量,如下所示:ret是在堆空间上分配的内存,因此当线程函数调用结束,通过join函数中的retval可以获取到对应的值,而如果不是malloc,而是在tfn中创建一个局部变量exit_t ret,ret.a=300,ret.b=400
,得到的结果将是错误的,因为局部变量一旦脱离作用域将被销毁。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
typedef struct {
int a;
int b;
} exit_t;
void *tfn(void *arg)
{
exit_t *ret;
ret = malloc(sizeof(exit_t));
ret->a = 100;
ret->b = 300;
//return (void*)ret.
pthread_exit((void *)ret);
}
int main(void)
{
pthread_t tid;
exit_t *retval;
pthread_create(&tid, NULL, tfn, NULL);
/*调用pthread_join可以获取线程的退出状态*/
pthread_join(tid, (void **)&retval); //wait(&status);
printf("a = %d, b = %d \n", retval->a, retval->b);
return 0;
}
循环回收多个线程:需要注意的是回收子线程的时候,不能创建完立刻就回收,这样打印出来的线程id号都是一样的,因此操作系统回收第一个之后,会给第二个分配和第一个的一样的线程id号。
void* tfun(void *arg)
{
long int i=(long int)(arg);
sleep(i);
printf("thread pid=%d,i=%d\n,tid=%lu\n",getpid(),i,pthread_self());
return NULL;
}
int main()
{
pthread_t tid[5];
long int i;
int ret;
for( i=0;i<5;++i)
{
ret = pthread_create(&tid[i],NULL,tfun,(void*)i);
if(ret !=0)
{
perror("pthread_create_error");
}
}
for( i=0;i<5;++i)
{
ret = pthread_join(tid[i],NULL);
if(ret !=0)
{
fprintf(stderr,"pthread_join_error:%s\n",strerror(ret));
exit(1);
}
}
printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(i);
return 0;
}
5.杀死线程
pthread_cancel函数,相当于进程中的kill函数。
int pthread_cancel(pthread_t thread);成功返回0,失败返回错误号
有以下几点需要注意:
1.线程的取消(杀死)需要等待一个取消点(一般来说进入系统调用时(sleep,open,write,read等函数时,会有机会调用该函数)。
2.如果没有这些系统调用函数,可以手动添加取消点,例如pthread_testcancel函数。
3.被取消的线程,退出时的返回值为-1
总结一下,终止进程的三种方式:
1.从线程主函数处return,它将返回到函数调用的位置处,类似于main函数中调用f(),返回到f函数的位置处。但是这种方法不适合于主线程,因为一旦return或者exit,就会退出整个进程。
2.使用pthread_exit函数,退出当前的线程(推荐使用)
3.一个线程调用pthread_cancel函数来终止同一进程中的其他线程。
6.分离线程
pthread_detach函数,用来分离线程,使得当前的线程和主控线程断开关系,当线程结束之后,其退出的状态将由自己自动释放,而不需要使用pthread_join回收资源。
函数原型如下:
int pthread_detach(pthread_t thread); 成功:0;失败:错误号
需要注意的是,一旦线程被设置为detach之后,再使用pthread_join会返回错误值,代码如下:
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include <errno.h>
#include <pthread.h>
void* tfun(void *arg)
{
long int i=(long int)(arg);
sleep(i);
printf("thread pid=%d,i=%ld\n,tid=%lu\n",getpid(),i,pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
long int i=0;
int ret;
ret = pthread_create(&tid,NULL,tfun,(void*)i);
sleep(1);//加上sleep函数防止子线程还没有运行,就已经分离了。
pthread_detach(tid);
if(ret !=0)
{
perror("pthread_create_error");
}
ret = pthread_join(tid,NULL);
if(ret !=0)
{
fprintf(stderr,"pthread_join_error:%s\n",strerror(ret));
exit(1);
}
printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(i);
return 0;
}
参见本篇博文错误信息打印注意点
这里关于错误信息的打印需要注意:在进程中获取错误信息使用的是perror函数如下所示:
void perror(const char *msg);
因为一般正确返回0,错误返回-1,出错时errno被系统自动设置为对应的错误号,perro根据错误号将其对应的错误信息字符串打印。
int ret = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));;
if (ret != 0)
{
perror("bind error: ");
exit(1);
}
而在线程中,正确返回的是0,出错返回的是错误码,因此不能使用perror去捕捉,直接用strerror就能把返回的错误号转换为错误信息字符串。
char * strerror(int errnum);
int ret = pthread_join(tid,NULL);
if (ret != 0)
{
fprintf(stderr, "pthread_join_error: %s\n", strerror(ret));
exit(1);
}
7.线程和进程的对比(其实是非常相似哒),其中线程多了一个分离属性,而进程是没有的。
进程 | 线程 |
---|---|
进程的创建:fork | 线程的创建:pthread_create |
进程的id:getpid | 线程的id:pthread_self |
进程的退出:exit | 线程的退出pthread_exit |
进程回收资源:waitpid | 线程回收资源:pthread_join |
杀死进程:kill | 杀死线程:pthread_cancel |
8.设置线程的属性
(1)int pthread_attr_init(pthread_attr_t *attr);
(2)int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
其中:detachstate取值有以下两种取值:
1. PTHREAD_CREATE_DETACHED:线程被设置为分离状态。
2. PTHREAD_CREATE_DETACHED:线程被设置为连接状态。
(3)int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
//(4)int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
(5)int pthread_attr_destroy(pthread_attr_t *attr);
9.线程属性注意点
- 主线程退出其他线程不退出,主线程应调用 pthread_exit
- 避免僵尸线程的方法
pthread_join:被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值。
pthread_detach
pthread_create 指定分离属性: - malloc 和 mmap 申请的内存可以被其他线程释放
应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit - 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
10.vim:一键排版:gg=G,不需要在command模式下输入:
11.一键替换:%s/old/new/g
12.复制多行:nyy,p