(六)Linux系统编程之线程(上)

    xiaoxiao2023-10-26  162

    线程

    那么线程不要传递局部变量的地址,因为每个线程的栈区是不稳定的,经常会改变。


    Linux和Windows的线程不一样

    Linux: (1)在用户看来,线程和进程不一样:因为线程没有自己的地址空间,多个线程共用一块内存空间

    (2)在内核看来:由于内核只看pcb,因此它分不清进程和线程。【如果你有n个pcb,那么有n个进程】

    Linux下的线程来的比较晚,Linux操作系统诞生之后并不是就有线程,而是只有进程。但是Windows那边使用线程比较普遍,提高了CPU的利用率。

    因此后来强行应用到Linux,但是由于Linux没有线程的概念,Linux OS设计的初期没有预留接口给线程,所以将原本的进程进行了修改,因此线程和进程的底层实现是一样的,因此Linux与Windows的线程是不一样的。


    效率问题: 程序的进程越多,抢占到的CPU时间片越多;

    假如N个进程变成一个进程内N个线程去抢占CPU时间片,但是内核只认pcb,所以进程内N个线程,CPU轮流分配线程时间片与N个进程一样的多。

    因此程序的效率没有降低,只是占用的资源变少了。


    线程的相关概念:

    自行安装线程man page,命令 sudo apt-get install manpages-posix-dev察看指定线程的LWP号 线程号和线程ID是有区别的

    线程号是给内核看的 查看方式:找到程序的进程ID ----> ps -Lf pid

    线程ID是给用户看的


    1、创建线程 – pthread_create

    函数原型:

    //如果成功0,失败返回错误号 //perror() 不能使用该函数打印错误信息 int pthread_create( pthread_t *thread, //线程ID=无符号长整形【十分大】 const pthread_attr_t *attr, //线程属性,NULL void *(*start_routine)(void *), //线程处理函数 void *arg //线程处理函数参数 );

    参数:

    thread:传出参数,线程创建成功之后,会被设置一个合适的值attr:默认传NULL,或者设置线程分离【可以自己释放自己】start_routine:子线程的处理函数arg:回调函数start_routine的参数 #include<fcntl.h> #include<unistd.h> #include<iostream> #include<errno.h> #include<stdlib.h> #include<string.h> #include<stdio.h> #include<sys/stat.h> #include<sys/types.h> #include<sys/wait.h> #include<sys/mman.h> #include<signal.h> #include<sys/time.h> #include<pthread.h> void* myfunc(void *arg) { //打印子线程的ID printf("child thread id : %lu\n",pthread_self()); return NULL; } int main(int argc,const char* argv[]) { //创建一个子线程 //线程ID变量 pthread_t pthid; pthread_create(&pthid,NULL,myfunc,NULL); printf("parent thread id : %lu\n",pthread_self()); for(int i=0;i<5;++i) { printf("i = %d\n",i); } return 0; }

    子线程被创建出来之后,需要去抢占CPU,但是此时主线程继续往下执行,return 0,父线程退出。

    由于主线程和子线程共用一个地址空间,因此主线程退出了,地址空间释放了,于是子线程也被迫退出了。

    for(int i=0;i<5;++i) { printf("i = %d\n",i); } sleep(2);//增加 return 0;

    主线程先退出,子线程会被强制结束。


    循环创建多个子进程

    //循环创建多个子进程 void* myfunc(void *arg) { //取数据 int num=*(int *)arg; //打印子线程的ID printf("%dth child thread id : %lu\n",num,pthread_self()); return NULL; } int main(int argc,const char* argv[]) { //创建一个子线程 //线程ID变量 pthread_t pthid[5]; for(int i=0;i<5;++i){ pthread_create(&pthid[i],NULL,myfunc,(void*)&i);//此处传递的是i的地址 } printf("child thread id : %lu\n",pthread_self()); for(int i=0;i<5;++i){ printf("i = %d\n",i); } sleep(2); return 0; }

    出现两个2th子线程的原因:


    解决办法:

    //myfunc()中 int num=*(int *)arg; //改为下面 int num=(int)arg; //传地址 pthread_create(&pthid[i],NULL,myfunc,(void*)&i); //改为下面的传值 //传值 pthread_create(&pthid[i],NULL,myfunc,(void*)i); /*如果强行转成void*类型:32位,由于void*4个字节,int*4个字节,所以不会丢失字节,但如果是后者是8个字节,那么会丢失字节*/

    线程函数打印错误信息

    int ret=pthread_create(&pthid,NULL,myfunc,NULL); if(ret!=0){ printf("error number : %d\n",ret); //打印错误信息 printf("%s\n",strerror(ret)); }

    通过设置属性设置线程分离

    避免线程刚创建完,还没设置线程分离,线程就死了

    1、线程属性类型:pthread_attr_t attr

    2、线程属性操作函数:

    对线程属性变量的初始化 int pthread_attr_init(pthread_attr_t* attr); 设置线程分离属性 int pthread_attr_setdetachstate( pthread_attr_t* attr, int detachstate ); 参数: (1)attr:线程属性 (2)detachstate: PTHREAD_CREATE_DETACHED (分离) PTHREAD_CREATE_JOINABLE (非分离)

    3、释放线程资源函数

    int pthread_attr_destroy(pthread_attr_t *attr); void* myfunc(void *arg) { //打印子线程的ID printf("child thread id : %lu\n",pthread_self()); return NULL; } int main(int argc,const char* argv[]) { //创建一个子线程 //线程ID变量 pthread_t pthid; //返回错误号 //初始化线程的属性 pthread_attr_t attr; pthread_attr_init(&attr); //设置分离 pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //创建线程的时候设置线程分离 int ret=pthread_create(&pthid,&attr,myfunc,NULL); if(ret!=0){ printf("error number : %d\n",ret); //打印错误信息 printf("%s\n",strerror(ret)); } printf("parent thread id : %lu\n",pthread_self()); for(int i=0;i<5;++i) { printf("i = %d\n",i); } sleep(2); //释放资源 pthread_attr_destroy(&attr); return 0; }

    2、单个线程退出 – pthread_exit

    exit(0); 进程退出函数

    函数原型:void pthread_exit(void *retval);

    retval指针:必须指向全局变量,堆

    主线程退出是否影响子线程?

    void* myfunc(void *arg) { //打印子线程的ID printf("child thread id : %lu\n",pthread_self()); for(int i=0;i<5;++i){ printf("child i = %d\n",i); } return NULL; } int main(int argc,const char* argv[]) { //创建一个子线程 //线程ID变量 pthread_t pthid; int ret=pthread_create(&pthid,NULL,myfunc,NULL); if(ret!=0){ printf("error number : %d\n",ret); //打印错误信息 printf("%s\n",strerror(ret)); } printf("parent thread id : %lu\n",pthread_self()); //退出主线程 pthread_exit(NULL); for(int i=0;i<5;++i){ printf("parent i = %d\n",i); } return 0; }

    主线程退出不影响子线程。


    假如使用的是exit(),主线程退出是否影响子线程?

    void* myfunc(void *arg) { //打印子线程的ID printf("child thread id : %lu\n",pthread_self()); for(int i=0;i<5;++i){ printf("child i = %d\n",i); if(i==2){ //子线程退出 exit(1); //这里应该使用,而非exit //pthread_exit(NULL); } } return NULL; } int main(int argc,const char* argv[]) { //创建一个子线程 //线程ID变量 pthread_t pthid; int ret=pthread_create(&pthid,NULL,myfunc,NULL); if(ret!=0){ printf("error number : %d\n",ret); //打印错误信息 printf("%s\n",strerror(ret)); } printf("parent thread id : %lu\n",pthread_self()); int i=0; while(1){ printf("parent i= %d\n",i++); } //退出主线程 pthread_exit(NULL); return 0; }

    影响 exit()退出的是一个进程,主线程和子线程使用同一个地址空间,让子线程对应的地址空间强制结束,主线程也被强制退出。

    因此,子线程退出不能用exit(),而应该使用

    pthread_exit(NULL); //也可以 return 0;//返回调用者的位置,也即从子线程退出 //但是这种情况不可以 myreturn(); //这种情况下虽然有if(i==2),但是还是循环5次 void* myreturn(){ return NULL; }

    想要在pthread_exit()中传值

    void * myfunc() { ... int number=100; pthread_exit(&number); /* 这种情况当函数返回,number就不存在了, &number的值(地址)就不存在了,这是不合法的 number必须是全局变量、或者位于堆 */ }

    3、阻塞等待线程退出,获取线程的退出状态 – pthread_join

    函数原型: int pthread_join(pthread_t thread,void **retval);

    thread:要回收的子线程的线程idretval:读取线程退出的时候携带的状态信息 传出参数: (1)void *ptr (2)pthread_join(pthid,&ptr); (3)指向的内存和pthread_exit参数指向同一块内存地址 int number=100;//必须指向全局变量、堆 void* myfunc(void *arg) { //打印子线程的ID printf("child thread id : %lu\n",pthread_self()); for(int i=0;i<5;++i){ printf("child i = %d\n",i); if(i==2){ pthread_exit(&number); } } return NULL; } int main(int argc,const char* argv[]) { //创建一个子线程 //线程ID变量 pthread_t pthid; int ret=pthread_create(&pthid,NULL,myfunc,NULL); if(ret!=0){ printf("error number : %d\n",ret); //打印错误信息 printf("%s\n",strerror(ret)); } printf("parent thread id : %lu\n",pthread_self()); //阻塞等待子线程推出,并且回收起pcb void *ptr=NULL; pthread_join(pthid,&ptr); printf("number = %d\n",*(int *)ptr); int i=0; while(i<10){ printf("parent i= %d\n",i++); } return 0; }

    4、线程分离 – ptheread_detach

    函数原型: int pthread_detach(pthread_t thread);

    调用该函数之后不需要pthread_join 子线程会自动回收自己的pcb


    5、杀死(取消)线程 – pthread_cancel

    函数原型: int pthread_cancel(pthread_t thread);

    使用注意事项:

    在要杀死的子线程对应的处理的函数的内部,必须做过一次系统调用例如:write read printf还能使用:设置一个取消点 pthread_testcancel(); //这种代码就无法到达系统调用的层级上 //没有系统调用,没有取消点,因此主线程无法杀死子线程 int a; a=2; int b=a+3//若是这样,只有运行到printf才能取消,运行到int b=a+3;都不能取消 int a; a=2; int b=a+3printf("b = %d\n",b);//此处为取消点

    6、比较两个线程ID是否相等(预留函数)-- pthread_equal

    函数原型: int pthread_equal(pthread_t t1,pthread_t t2);

    此时的pthread_t是一个长整形,当以后类型发生改变,使用该函数进行比较


    线程同步

    #define MAX 10000 //全局变量 int number; //线程处理函数 void* funcA_num(void *arg){ for(int i=0;i<MAX;++i){ int cur=number; cur++; number=cur; printf("Thread A,id = %lu, number =%d\n",pthread_self(),number); usleep(10); } return NULL; } void* funcB_num(void *arg){ for(int i=0;i<MAX;++i){ int cur=number; cur++; number=cur; printf("Thread B,id = %lu, number =%d\n",pthread_self(),number); usleep(10); } return NULL; } int main(int argc,const char *argv[]){ pthread_t p1,p2; //创建两个子线程 pthread_create(&p1,NULL,funcA_num,NULL); pthread_create(&p2,NULL,funcB_num,NULL); //阻塞,资源回收 pthread_join(p1,NULL); pthread_join(p2,NULL); return 0; }

    结果: number<20000


    数据混乱原因:

    操作了共享资源cpu调度问题

    解决:

    线程同步什么叫同步: 协同步调,按照先后顺序执行操作

    线程同步思想:


    一、互斥量(互斥锁)

    1、互斥锁类型

    创建一把锁 pthread_mutex_t mutex;

    2、互斥锁的特点 多个线程访问共享数据的时候是串行的

    3、使用互斥锁的缺点

    效率低

    4、互斥锁的使用步骤

    创建互斥锁:pthread_mutex_t mutex;初始化这把锁:pthread_mutex_init(&mutex,NULL) – mutex=1寻找共享资源: 操作共享资源的代码之前加锁

    pthread_mutex_lock(&mutex); – mutex=0 …(临界区) pthread_mutex_unlock(&mutex); – mutex=1

    for(int i=0;i<MAX;++i){ if(){ //共享数据 加锁 10行代码 - 有操作共享数据的 解锁 20行代码 - 没有操作共享数据的 加锁 5行代码 - 有操作共享数据的 解锁 } else{ } }

    5、互斥锁相关函数

    初始化互斥锁 pthread_mutex_init( pthread_mutex_t* mutex; const pthread_mutexattr_t* attr; //加上restrict //pthread_mutex_t* restrict mutex; //const pthread_mutexattr_t* restrict attr; );

    销毁互斥锁 pthread_mutex_destroy(pthread_mutex_t *mutex); 加锁 pthread_mutex_lock(pthread_mutex_t *mutex);

    mutex: (1)没有被上锁,当前线程会将这把锁锁上 (2)被锁上了:当前线程阻塞;锁被打开之后,线程解除阻塞

    尝试加锁,失败返回,不阻塞 pthread_mutex_trylock(pthread_mutex_t *mutex);

    (1)没有锁上:当前线程会给这把锁加锁 (2)如果锁上了:不会阻塞,返回

    if(pthread_mutex_trylock(&mutex)==0) { //尝试加锁,并且成功了 //访问共享资源 } else { //错误处理 //或者等一会,再次尝试加锁 } 解锁 pthread_mutex_unlock(pthread_mutex_t *mutex);

    练习:

    #define MAX 10000 //全局变量 int number; //创建一把互斥锁 pthread_mutex_t mutex; //线程处理函数 void* funcA_num(void *arg){ for(int i=0;i<MAX;++i){ //访问全局变量之前加锁 //如果mutex被锁上了,代码阻塞在当前位置 pthread_mutex_lock(&mutex); int cur=number; cur++; number=cur; printf("Thread A,id = %lu, number =%d\n",pthread_self(),number); //解锁 pthread_mutex_unlock(&mutex); usleep(10); } return NULL; } void* funcB_num(void *arg){ for(int i=0;i<MAX;++i){ pthread_mutex_lock(&mutex); int cur=number; cur++; number=cur; printf("Thread B,id = %lu, number =%d\n",pthread_self(),number); //解锁 pthread_mutex_unlock(&mutex); usleep(10); } return NULL; } int main(int argc,const char *argv[]){ pthread_t p1,p2; //初始化互斥锁 pthread_mutex_init(&mutex,NULL); //创建两个子线程 pthread_create(&p1,NULL,funcA_num,NULL); pthread_create(&p2,NULL,funcB_num,NULL); //阻塞,资源回收 pthread_join(p1,NULL); pthread_join(p2,NULL); // 释放互斥锁资源 pthread_mutex_destroy(&mutex); return 0; }

    最新回复(0)