线程(一)
linux 系统中,实现并发任务处理的方式有哪两种?
进程和线程两种,进程往往和信号共同使用。
线程概念
轻量级的进程(light weight process – LWP),一个进程内部可以有多个线程。默认情况下,一个进程只有一个线程。
线程的(t)id在进程的内部是唯一的。但在整个系统中有可能不是唯一的。
进程和线程的简要关系图。
可以打一个比较形象的比喻。
上面提到,多个线程会共用一个进程的空间。
下图为一个进程的空间。
除了栈(stack)之外,其他空间都是共用的。
因为线程实际上是有自己的执行目的(逻辑)的。目的非常明确,比如刷碗就是刷碗的,打酱油就是带酱油的。等到后面会发现,实际上,线程执行的就是一个函数。函数是在栈上的,所以栈是不被共享的。
由此,可以看出线程的好处
1、通信方便。
2、提高程序的并发性。原先一个进程只有一个线程,这个线程既要刷完,又要打酱油,现在线程多了,有专人干专活。
现在有一个问题是,假如只有一个CPU(一个核心数),那多进程、多线程还有用吗?
答:没用
假设只有一个核心数的CPU,那么即使多个线程,同一时刻能够干活的也只是一个进程中的一个线程。同一进程中的其他线程也只能干看着。
线程是最小的执行单位,进程是最小的系统资源分配单位。
Linux内核线程实现原理
一个进程中有三个线程。
线程共享资源
1、文件描述符表
2、每种信号的处理方式
信号和线程不要放在一起使用。
3、当前工作目录
4、用户ID和组ID
5、内存地址空间(.text/.data/.bss/heap/共享库)
比如全局变量。
线程非共享资源
1、线程id
2、处理器现场和栈指针(内核(空间中的)栈(保存处理器现场))
3、独立的栈空间(用户(空间中的)栈)
4、errno变量(当使用多线程时,每个线程有自己特有的errno。)
在之前的代码(单个进程单个线程)中,有异常或错误时,可以使用perror将错误信息给输出出来。
当使用多线程之后,因为每个线程都有自己特有的error,需要使用strerror这个函数,在每个线程中使用。
5、信号屏蔽字
6、调度优先级
线程的优点
1、提高并发性
2、占用资源小
3、通信方便
线程的缺点
1、编写、调试困难。
2、库函数,不稳定。(早期的时候,Linux没有引入线程的概念,后加的,所以线程以库函数形式实现。而库函数总是换版本,所以不稳定。)
3、对信号支持不好。
创建一个线程
使用命令man pthread_create来查看这个函数的使用。
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
//编译和链接的时候需要带上 -pthread 。
//因为线程属于库函数,所以要加上。
第一个参数是
pthread_t *thread,
可以理解为一个长整型的指针变量,具体含义是线程的id,是传出参数。
第二个参数是
const pthread_attr_t *attr,
是线程的属性信息,一般不用。
第三个参数是函数指针,
void *(*start_routine) (void *),
指向线程执行函数。
第四个参数是
void *arg
是传给线程执行函数的参数。
返回值:
On success, pthread_create() returns 0;
on error, it returns an error number, and the contents of *thread are undefined.
再使用命令man pthread_self来查看这个函数的使用。
NAME
pthread_self - obtain ID of the calling thread
(包含正在调用线程的ID)
SYNOPSIS
#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.
RETURN VALUE
This function always succeeds, returning the calling thread's ID.
线程的调度取决于调度器的调度策略。
创建一个线程
vi thread.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thr(void* arg)
{
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
printf("I am a main thread,pid = %d,tid = %ld.\n",getpid(),pthread_self());
//为了能看到现象,先sleep(1)
sleep(1);
return 0;
}
我们会有疑问,为什么要加上sleep(1)?
int main()
{
...
//为了能看到现象,先sleep(1)
sleep(1);
return 0;
}
因为在main函数中,当执行完 return 0;后,程序就退出了,相当于进程结束了,从而所有线程的用户空间也就不复存在了。我们必须使用sleep(1)来延迟一段时间,让线程函数中的输出语句得以执行,有现象产生。
注意打印线程号要使用%ld,使用%d不可以。
程序运行结果如下:
使用gcc编译代码的注意事项。
-lpthread和-pthread的区别
参考链接: https://www.cnblogs.com/suntp/p/6473751.html.
编译程序包括 预编译, 编译,汇编,链接。代码中包含头文件,仅能说明有了线程函数的声明, 但是还没有实现, 加上-lpthread是在链接阶段,链接这个库。
<stdio.h>等都是静态库,不需要做额外的表示,连接时会直接链接进代码里。pthread是动态库,需要用-lpthread,所有的动态库都需要用-lxxx来引用。
用gcc编译使用了POSIX thread的程序时,通常需要加额外的选项,以便使用thread-safe的库及头文件。
一些老的书里说直接增加链接选项 -lpthread 就可以了,而gcc手册里则指出应该在编译和链接时都增加 -pthread 选项。
编译选项中指定 -pthread 会附加一个宏定义 -D_REENTRANT,该宏会导致 libc 头文件选择那些thread-safe的实现;
链接选项中指定 -pthread 则同 -lpthread 一样,只表示链接 POSIX thread 库。由于 libc 用于适应 thread-safe 的宏定义可能变化,因此,在编译和链接时都使用 -pthread 选项而不是传统的-lpthread,-lpthread 能够保持向后兼容,并提高命令行的一致性。
目前gcc 4.5.2中已经没有了关于 -lpthread 的介绍了。所以以后的多线程编译推荐用-pthread,而不是-lpthread。(因为保持向后兼容,所以也可以使用)
使用gcc编译代码的注意事项。
线程的退出
线程的退出可以使用pthread_exit函数。
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.
参数retval表示在退出时,向外传一个退出值,可以通过这个值来告诉外界该线程为什么退出,以及可用来分辨是哪个线程退出了。类似于回收子进程时通过信号得到是怎么死的。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thr(void* arg)
{
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
//线程退出可以使用pthread_create函数,也可以使用return
// return NULL;
//线程退出
pthread_exit(NULL);
//exit(1);//代表退出进程
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
printf("I am a main thread,pid = %d,tid = %ld.\n",getpid(),pthread_self());
//主控线程退出
pthread_exit(NULL);
return 0;
}
程序运行结果如下:
线程退出的注意事项:
线程退出可以使用pthread_exit函数,也可以使用return(主控线程return代表退出进程)
exit代表退出进程。
线程的回收
子进程死的话,父进程没有回收,会变成僵尸进程。
如果线程死(退出)的话,主线程没有回收的话,也会变成僵尸线程
需要了解一个函数-- pthread_join。
这个线程的作用就是回收线程。
SYNOPSIS
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
参数:
pthread_t thread是指定回收哪个线程。(创建线程的第一个参数)
retval代表传出线程的退出信息。
线程回收函数是阻塞等待。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thr(void* arg)
{
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
sleep(3);
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
return (void*)100;
// return (void*)100;和pthread_exit((void*)100);等价
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
void* ret;
pthread_join(tid,&ret);//这里会阻塞等待子线程退出,然后回收线程
printf("ret exit with %d.\n",(int)ret);
printf("I am a main thread,pid = %d,tid = %ld.\n",getpid(),pthread_self());
pthread_exit(NULL);
return 0;
}
执行结果如下:
会有警告,忽略。
多线程练习
我们要实现的目标:
利用多线程完成对30000000–30000200之间质数的查找。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)
void* thr_prime(void *p)
{
int i,j,mark;
i = *(int* )p;
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
mark = 0;
break;
}
}
if(mark)
{
printf("%d is a primer.\n",i);
}
pthread_exit(NULL);
}
int main()
{
int i,err;
pthread_t tid[THRNUM];
for(i = LEFT;i <= RIGHT;i++)
{
err = pthread_create(&tid[i-LEFT],NULL,thr_prime,&i);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
for(i = LEFT;i<= RIGHT;i++)
{
pthread_join(tid[i-LEFT],NULL);
}
exit(0);
}
运行下程序试试看
可以发现,每次运行结果的值都不一样,出现的这种现象的原因是因为出现了"竞争"。
注意程序中的这句代码:
err = pthread_create(&tid[i-LEFT],NULL,thr_prime,&i);
这句代码说明,我们在创建线程的时候,给201个线程传的参数都是&i,尽管i的值不同,但共享着同一块地址。201份线程同时执行着函数thr_prime,都在拼命的访问着&i这块地址,因为&i这块地址中的值随着每次线程创建后,值一直在变。所以能执行到下述代码
i = *(int* )p;
的线程(抢到资源的线程),所访问到的i的值是随机的,不确定的。
就像201辆车同时经过同一个十字路口。
那怎么改进呢?我们需要将这两处给改成值传递,同时需要进行类型强转。
改进后代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)
void* thr_prime(void *p)
{
int i,j,mark;
i = (int)p;
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
mark = 0;
break;
}
}
if(mark)
{
printf("%d is a primer.\n",i);
}
pthread_exit(NULL);
}
int main()
{
int i,err;
pthread_t tid[THRNUM];
for(i = LEFT;i <= RIGHT;i++)
{
err = pthread_create(&tid[i-LEFT],NULL,thr_prime,(void *)i);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
for(i = LEFT;i<= RIGHT;i++)
{
pthread_join(tid[i-LEFT],NULL);
}
exit(0);
}
这种警告被称为我们可以解释的警告,可以忽略。
杀死线程
需要了解一个函数-- pthread_cancel。
这个线程的作用就是杀死线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
RETURN VALUE
On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.
运行下面程序,看下,被杀死后,使用回收函数回收线程能得到什么值。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thr(void* arg)
{
while(1)
{
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
sleep(3);
pthread_cancel(tid);
void* ret;
pthread_join(tid,&ret);
printf("ret exit with %d.\n",(int)ret);//线程回收
printf("I am a main thread,pid = %d,tid = %ld.\n",getpid(),pthread_self());
return 0;
}
程序运行结果如下:
那为什么会是-1呢?
所以,被pthread_cancel杀死的线程,退出状态为这个宏
#definePTHREAD_CANCELED ((void* ) -1)
取消点
如果线程的的while(1)中什么都不写,那还能杀死线程吗?
void* thr(void* arg)
{
while(1)
{
}
return NULL;
}
再运行程序,我们会发现杀不死线程了,原因是什么呢?就是取消点的问题,在上图中有所说明。
我们使用man 7 pthreads来查看取消点都有哪些。
也就是说,线程函数中不能为空。
如果担心取消点的问题的话,还可以通过调用来强行设置取消点。
线程分离
使用这个函数的好处是,线程结束后,其退出状态不由其他线程获取,而是直接自己自动释放,也就是说线程的回收不需要其他线程调用 pthread_join 函数来回收,而是系统自动回收。
NAME
pthread_detach - detach a thread
SYNOPSIS
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread.
RETURN VALUE
On success, pthread_detach() returns 0; on error, it returns an error number.
注意该函数的返回值。
使用pthread_deatch来实现线程分离
先不使用线程分离函数试试
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void* thr(void* arg)
{
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
sleep(3);
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
// pthread_detach(tid);//线程分离
//等待线程先结束
sleep(5);
int ret=0;
//线程回收成功返回0,回收失败会返回错误值
if((ret = pthread_join(tid,NULL))>0)
{
printf("join err:%d,%s.\n",ret,strerror(ret));
}
else
{
printf("ret exit with %d.\n",ret);//线程回收
}
printf("I am a main thread,pid = %d,tid = %ld.\n",getpid(),pthread_self());
return 0;
}
使用线程分离函数试试(将上述代码中的pthread_deatch取消注释)
注意,错误信息码的打印函数使用的是strerror
总结,当使用线程分离函数pthread_deatch之后,就不能再使用pthread_join函数对线程进行回收了,否则会报错。
线程属性
这里只描述线程属性–分离态,其他属性可以通过命令man pthread_attr_init 的 see also来查看
线程属性–分离态
在上述代码中,我们使用了线程分离函数pthread_detach。但是,有一种极端的情况,就是如果线程创建好之后,线程还没有来的及分离,就执行结束了。因此,为了避免这个情况的发生,我们可以在线程创建之前直接将线程属性设置为分离态。不需要再调用pthread_deatch来实现线程分离。
设定线程属性需要用到下面几个函数。
#include <pthread.h>
//初始化线程属性
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);
//获取线程属性--分离态(还是非分离态)
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
attr:已初始化的线程属性变量。
The following values may be specified in detachstate:
PTHREAD_CREATE_DETACHED:(分离线程)
PTHREAD_CREATE_JOINABLE:(非分离线程)-- 线程默认是非分离态
练习:将线程的属性设置为分离态
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void* thr(void* arg)
{
printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
sleep(1);
return NULL;
}
int main()
{
pthread_attr_t attr;
pthread_attr_init(&attr);//初始化线程属性
//将线程属性设置为线程分离
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t tid;
pthread_create(&tid,&attr,thr,NULL);
sleep(2);//等待线程运行完毕
int ret = 0;
//线程回收成功返回0,回收失败会返回错误值
if((ret=pthread_join(tid,NULL))>0)
{
printf("join err:%d,%s.\n",ret,strerror(ret));
}
else
{
printf("return value is %d.\n",ret);//线程回收
}
printf("I am a main thread,pid = %d,tid = %ld.\n",getpid(),pthread_self());
pthread_attr_destroy(&attr);//摧毁属性
return 0;
}
程序运行输出如下:
注意,初始化线程属性之后,最后要销毁。
Native POSIX Thread Library (NPTL)
Linux线程库 – Native POSIX Thread Library (NPTL),本机POSIX线程库
查看当前pthread库版本的命令
getconf GNU_LIBPTHREAD_VERSION
线程使用注意事项总结
作业:实现多线程拷贝
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#define THR_CNT 5 //线程数
typedef struct _TaskInfo{
int num;
void* src;
void* des;
long int size;
}TaskInfo;
void* thr_cp(void* arg)
{
TaskInfo* info = arg;
int num = info->num;//第几个线程
int cpsize =info->size/THR_CNT;//每个线程平均分配的文件大小
int mod = info->size% THR_CNT;//平均分配后的余量
if(num == THR_CNT - 1)//如果是最后一个子线程
{
//void *memcpy(void *dest, const void *src, size_t n);
//从存储区str2复制n个字节到存储区str1
memcpy(info->des+num*cpsize,info->src+num*cpsize,cpsize+mod);
}
else
{
memcpy(info->des+num*cpsize,info->src+num*cpsize,cpsize);
}
pthread_exit(NULL);
}
int main(int argc,char* argv[])
{
//输入参数是3个
if(argc!=3)
{
printf("%s sourcefilepath destinationfilepath\n",argv[0]);
return -1;
}
//线程数
int n = THR_CNT;
//open source file
int srcfd =open(argv[1],O_RDONLY);
if(srcfd < 0)
{
perror("open source error.\n");
exit(1);
}
//open destination file
int dstfd = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0666);
if(dstfd < 0)
{
perror("open destination error.\n");
exit(1);
}
//使用stat函数从源文件获得文件大小
struct stat sb;
if(stat(argv[1],&sb)<0)
{
perror(argv[1]);
exit(1);
}
long int length = sb.st_size;
truncate(argv[2],length);
//将源文件映射到缓冲区
//mmap
void* pdst = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,dstfd,0);
if(pdst == MAP_FAILED)
{
perror("mmap src error.\n");
exit(1);
}
//创建多个线程
int i,err;
TaskInfo Tid_Info[n];
pthread_t tid[n];
for(i = 0;i < n;i++)
{
Tid_Info[i].num = i;
Tid_Info[i].src = psrc;
Tid_Info[i].des = pdst;
Tid_Info[i].size=length;
err = pthread_create(&tid[i],NULL,thr_cp,&Tid_Info[i]);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
//等待多个线程被回收
for(i = 0;i < n ;i++)
{
pthread_join(tid[i],NULL);
}
//释放映射区和关闭文件描述符
if(munmap(psrc,length)<0)
{
perror("munmap source error.\n");
exit(1);
}
if(munmap(pdst,length)<0)
{
perror("munmap destination error.\n");
exit(1);
}
close(srcfd);
close(dstfd);
return 0;
}