一、线程的概念
1.1 什么是线程?
1)线程是一个进程并发执行多种任务的机制。
并发:
- 单核cpu多个任务同时运行。cpu以ms级别的速度进程进程调度,切换进程和线程。
串行、并发、并行:
- 并行:多个任务在多核CPU上运行。任务1运行在一个核上,任务2运行在另外一个核上。
- 并发:多个任务,在单核cpu上运行,同一个时间片上只能运行一个任务。时间片结束后,不管任务是否结束,都会切到下一个任务。
- 串行:多个任务有序执行,必须等第一个任务结束后才能执行第二个任务。
2) 线程的上下文切换
- 上下文:运行一个进程所需要的所有资源。
- 上下文切换:从访问进程1,到访问进程2。cpu访问的资源要替换原有内容,这个操作是一个耗时操作。
- 为了提高系统性能,引入了一个轻量级的进程的概念,称之为线程。
3)线程属于进程,每一个进程都至少有一个线程作为指令执行体,线程运行在进程空间内。一个进程中可以运行有多个线程,称之为多线程。
1.2 线程是任务运行的最小单位(重点)
同一个进程下的线程,共享该进程的所有资源
- 静态存储区(.txt .rodata .data .bss)
- 堆区
- 栈区
- 文件描述符表
1.3 进程和线程的区别(重点!!!!!)
二、线程相关的函数
man手册查看线程相关函数时:Compile and link with -pthread.
有关线程的程序编译时需要家 -pthread
例:gcc 1.c -pthread
2.1 pthread_create
功能:创建一个线程;
原型:
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数:
pthread_t *thread:存储创建后的线程的tid号; pthread_attr_t *attr:线程属性; 填NULL,代表默认属性。或者用pthread_attr_init(3)初始化线程属性后传参进入:分离属性。 void *(*start_routine) (void *):回调函数,函数指针,该函数指针指向线程执行体。 该指针可以指向返回值是void*类型,参数列表是void*类型的函数,例如: void* handler(void* arg){ } void *arg:传递给回调函数的参数;
返回值:
成功,返回0;
失败,返回错误编号,即非0,没说更新errno,所以不能用perror打印;
注意:
- 从main函数进来的线程称之为主线程,pthread_create创建的线程称之为分支线程或者子线程。
- 一般来说主线程会先运行,但是还是要依照时间片轮询机制。
- 主线程退出后(main函数结束),会导致进程结束,依附于该进程内的线程均会被强制退出。
- 其他线程退出后,会不会影响到主线程。
2.2 线程的传参
1. 定义一个全局变量int a=10,主线程和分支线程能否访问到?
访问到的是否是同一份资源?
- 答:均能访问到,且是同一份资源
2. 在主线程定一个局部变量int b=10,分支线程能否访问到?
- 答:不能,局部变量作用域在定义他的函数内部
3. 在分支线程定义一个局部变量int c=10,主线程能否访问到?
- 答:不能,局部变量作用域在定义他的函数内部
4. 若访问不到,用什么方式可以让对方线程访问到。
- 如下(主线程传参给分支线程)(分支线程传参给主线程)
1)主线程传参给分支线程
2)分支线程传参给主线程
2.3 pthread_exit
功能:退出分支线程,并传递线程退出状态值;
原型:
#include <pthread.h> void pthread_exit(void *retval);
参数:
void *retval:指定要传递给主线程的状态值,如果不想传递,填NULL;
传递的线程退出状态值被pthread_join函数接收;
PS:
当分支线程退出后,会残留一部分资源,例如线程的tid号,线程调度块等等,若不回收会出现类似僵尸线程的状态;
需要使用pthread_join等函数回收;
2.4 pthread_join
功能:阻塞函数,阻塞等待指定的分支线程退出,并接收分支线程退出状态值,同时回收分支线程的资源;
原型:
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
参数:
pthread_t thread:指定要等待哪个线程,填对应的tid号; void **retval:若retval不为空,则该函数会将线程退出状态值(void* retval)拷贝到该二级指针指向的一级指针的内存空间中,若不想接收填NULL; If retval is not NULL, then pthread_join() copies the exit status of the target thread into the location pointed to by retval.
返回值:
成功,返回0;
失败,返回错误编号,即非0,没说更新errno,所以不能用perror打印;
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//线程的执行体
void* callback(void* arg) //void* arg = (void*)&c
{
int i=0;
while(i<3)
{
printf("this is other func __%d__\n",__LINE__);
sleep(1);
i++;
}
static int a = 10; //利用static延长生命周期,否则callback结束,a的值被释放
printf("&a = %p\n",&a);
static char str[]="hello world";
printf("准备退出分支线程... ...\n");
pthread_exit(str); //void* retval = str;
}
int main(int argc, const char *argv[])
{
pthread_t tid;
if(pthread_create(&tid,NULL,callback,NULL) != 0)
{
fprintf(stderr, "pthread_create failed __%d__\n",__LINE__);
return -1;
}
printf("this is main func __%d__\n",__LINE__);
//线程的退出状态值会被拷贝到&pret指向的一级指针(pret)中
//即pret = &a;
void* pret = NULL;
pthread_join(tid,&pret); //阻塞等待tid分支线程退出
printf("*pret = %s %p\n",(char*)pret,pret);
printf("主线程准备退出\n");
return 0;
}
练习:创建两个线程:其中一个线程拷贝前半部分,另一个线程拷贝后半部分。
方法一:
先在主函数创建并清空拷贝的目标文件,再创建两个线程,在两个线程内部同时打开要读取的文件以及要拷贝的目标文件(两个线程不共用同一份资源)。
使用到的函数:
- 标准IO函数(fprintf)【用于打印错误信息】
- 文件IO函数(open、close、lseek)
- 有关线程的函数(pthread_create、pthread_exit、pthread_join)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <head.h>
//线程的执行体
void* callback_1(void* arg) //void* arg = (void*)&c
{
umask(0);
int fp_r=open("./1.png",O_RDONLY);
if(fp_r < 0)
{
ERR_MSG("open");
}
int fp_w=open("./copy.png",O_WRONLY);
if(fp_w <0)
{
ERR_MSG("open");
}
char c = 0;
off_t len=lseek(fp_r,0,SEEK_END);
int i=0;
lseek(fp_r,0,SEEK_SET);
lseek(fp_w,0,SEEK_SET);
for(i=0;i<len/2;i++)
{
bzero(&c,sizeof(c));
read(fp_r,&c,1);
write(fp_w,&c,1);
}
close(fp_r);
close(fp_w);
printf("前半部分拷贝完毕\n");
pthread_exit(NULL);
}
void* callback_2(void* arg)
{
umask(0);
int fp_r=open("./1.png",O_RDONLY);
if(fp_r < 0)
{
ERR_MSG("open");
}
int fp_w=open("./copy.png",O_WRONLY);
if(fp_w < 0)
{
ERR_MSG("open");
}
char c = 0;
off_t len=lseek(fp_r,0,SEEK_END);
int i=0;
lseek(fp_r,len/2,SEEK_SET);
lseek(fp_w,len/2,SEEK_SET);
for(i=0;i<len/2;i++)
{
bzero(&c,sizeof(c));
read(fp_r,&c,sizeof(c));
write(fp_w,&c,sizeof(c));
}
close(fp_r);
close(fp_w);
printf("后半部分拷贝完毕\n");
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
//两个线程在拷贝前,确保文件w存在,且是清空状态
int fp_w=open("./copy.png",O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fp_w <0)
{
ERR_MSG("open");
}
close(fp_w);
pthread_t tid_1,tid_2;
if(pthread_create(&tid_1,NULL,callback_1,NULL) != 0)
{
fprintf(stderr, "pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_join(tid_1,NULL);
if(pthread_create(&tid_2,NULL,callback_2,NULL)!=0)
{
fprintf(stderr, "pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_join(tid_2,NULL);
printf("主线程准备退出... ...\n");
return 0;
}
方法二:
创建一个结构体用于存放需要打开的两个文件文件标识符、需要拷贝的字节大小。
在主函数中打开两个文件,计算好需要拷贝的字节大小,再创建两个线程,将结构体fileinfo的地址传递到线程中(强转成(void*)类型再传,否则报错),线程中用指针void* arg接fileinfo的地址。线程中需要将指针arg的地址转为struct Msg类型。
PS:两个线程共享同一份资源,使用pthread_exit函数使线程1先完成拷贝(与sleep达到的效果一致)。
使用到的函数:
- 结构体struct
- 标准IO函数(fprintf)【用于打印错误信息】
- 文件IO函数(open、close、lseek)
- 有关线程的函数(pthread_create、pthread_exit、pthread_join)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <head.h>
struct Msg
{
int fp_r;
int fp_w;
off_t size;
};
//线程1的执行体:拷贝前半部分
void* callback_1(void* arg) //void* arg = &fileinfo
{
struct Msg *tp=(struct Msg*)arg;
int fp_r=tp->fp_r;
int fp_w=tp->fp_w;
off_t size=tp->size;
//将光标偏移到开头,拷贝size/2个字节到目标文件中
lseek(fp_r,0,SEEK_SET);
lseek(fp_w,0,SEEK_SET);
char c = 0;
for(int i=0;i<size/2;i++)
{
bzero(&c,sizeof(c));
read(fp_r,&c,1);
write(fp_w,&c,1);
}
printf("前半部分拷贝完毕\n");
pthread_exit(NULL);//退出分支线程
}
//线程2的执行体
void* callback_2(void* arg) //void* arg = &fileinfo
{
//sleep(5);//主动放弃cpu资源,让线程1先执行
struct Msg *tp=(struct Msg*)arg;
int fp_r=tp->fp_r;
int fp_w=tp->fp_w;
off_t size=tp->size;
//将光标偏移到size/2的位置,拷贝size/2个字节到目标文件中
lseek(fp_r,size/2,SEEK_SET);
lseek(fp_w,size/2,SEEK_SET);
char c = 0;
for(int i=0;i<size/2;i++)
{
bzero(&c,sizeof(c));
read(fp_r,&c,sizeof(c));
write(fp_w,&c,sizeof(c));
}
printf("后半部分拷贝完毕\n");
pthread_exit(NULL);//退出分支线程
}
int main(int argc, const char *argv[])
{
struct Msg fileinfo;
//以读的方式打开1.png
fileinfo.fp_r=open("./1.png",O_RDONLY);
if(fileinfo.fp_r < 0)
{
ERR_MSG("open");
return -1;
}
//以写的方式打开并创建(若存在则清空)copy.png
fileinfo.fp_w=open("./copy.png",O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fileinfo.fp_w <0)
{
ERR_MSG("open");
return -1;
}
//计算需要拷贝的字节大小
fileinfo.size=lseek(fileinfo.fp_r,0,SEEK_END);
//创建2个线程
pthread_t tid_1,tid_2;
if(pthread_create(&tid_1,NULL,callback_1,(void *)&fileinfo) != 0)
{
fprintf(stderr, "pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_join(tid_1,NULL);//阻塞等待线程1完成
if(pthread_create(&tid_2,NULL,callback_2,(void *)&fileinfo) !=0)
{
fprintf(stderr, "pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_join(tid_2,NULL);//阻塞等待线程2完成
//关闭文件
close(fileinfo.fp_r);
close(fileinfo.fp_w);
printf("主线程准备退出\n");
return 0;
}
2.5 pthread_detach
功能:分离线程,线程退出后资源由系统自动回收;
原型:
#include <pthread.h> int pthread_detach(pthread_t thread);
参数:
pthread_t thread:指定要分离哪个线程;
返回值:
成功,返回0;
失败,返回错误编号,即非0,没说更新errno,所以不能用perror打印;
- 当使用pthread_detach分离tid线程后,pthread_join函数就无法再回收tid线程的资源了,且pthread_join函数不阻塞
2.6 pthread_cancel
功能:请求指定线程退出; 请求成功,对方线程不一定退出
原型:
#include <pthread.h> int pthread_cancel(pthread_t thread);
参数:
pthread_t thread:指定要请求哪个线程退出;
返回值:
成功,返回0;
失败,返回错误编号,即非0,没说更新errno,所以不能用perror打印;
注意:
- pthread_cancel会给目标线程打上一个退出标识,cpu切换到目标线程后,运行到退出标识,才会让目标线程退出。
- 但是while for 等循环结构,以及算数运算等等位置,无法打上退出标识。所以当目标线程只有上述结构的时候,无法用pthread_cancel退出线程
- printf ,sleep等等函数结构可以打上退出标识
- 请求成功,不代表目标线程一定会退出。
练习:要求定义一个全局变量 char buf[] = "1234567",创建两个线程,不考虑退出条件
要求定义一个全局变量 char buf[] = "1234567",创建两个线程,不考虑退出条件,另:
- A线程循环打印buf字符串,
- B线程循环倒置buf字符串,即buf中本来存储1234567,倒置后buf中存储7654321. 不打印!!
- 倒置不允许使用辅助数组。
- 要求A线程打印出来的结果只能为 1234567 或者 7654321 不允许出现7634521 7234567
- 不允许使用sleep函数
方法一:使用flag将其分离
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <head.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
char buf[]="1234567";
int flag= 0 ;
void* callback_1(void* arg)//打印
{
while(1)
{
if(0 == flag)
{
printf("%s\n",buf);
flag=1;
}
}
pthread_exit(NULL);
}
void* callback_2(void* arg)//逆置 不打印
{
char t=0;
while(1)
{
if(1 == flag)
{
for(int i=0;i<strlen(buf)/2;i++)
{
t=buf[i];
buf[i] = buf[strlen(buf)-1-i];
buf[strlen(buf)-1-i] = t;
}
flag=0;
}
}
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
pthread_t tid_1,tid_2;
if(pthread_create(&tid_1,NULL,callback_1,NULL)!=0)
{
fprintf(stderr,"pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_detach(tid_1); //分离线程1
if(pthread_create(&tid_2,NULL,callback_2,NULL)!=0)
{
fprintf(stderr,"pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_join(tid_2,NULL);
printf("主线程准备退出");
return 0;
}
方法二:使用互斥锁
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <head.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
//临界资源
char buf[]="1234567";
//互斥锁
pthread_mutex_t mutex;
void* callback_1(void* arg)//打印
{
while(1)
{
/***********临界区**************/
//上锁
pthread_mutex_lock(&mutex);
printf("%s\n",buf);
//解锁
pthread_mutex_unlock(&mutex);
/***********临界区**************/
}
pthread_exit(NULL);
}
void* callback_2(void* arg)//逆置 不打印
{
char t=0;
while(1)
{
/***********临界区**************/
//上锁
pthread_mutex_lock(&mutex);
for(int i=0;i<strlen(buf)/2;i++)
{
t=buf[i];
buf[i] = buf[strlen(buf)-1-i];
buf[strlen(buf)-1-i] = t;
}
//解锁
pthread_mutex_unlock(&mutex);
/***********临界区**************/
}
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
//申请一个互斥锁
pthread_mutex_init(&mutex,NULL);
pthread_t tid_1,tid_2;
if(pthread_create(&tid_1,NULL,callback_1,NULL)!=0)
{
fprintf(stderr,"pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_detach(tid_1); //分离线程1
if(pthread_create(&tid_2,NULL,callback_2,NULL)!=0)
{
fprintf(stderr,"pthread_create failed __%d__\n",__LINE__);
return -1;
}
pthread_join(tid_2,NULL); //阻塞等待线程2退出
//销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}