Unix\Linux多线程复健

线程是程序中完成一个独立任务的完整执行序列(是一个可调度的实体)

一个进程可以包含多个线程

查看指定进程的线程号:

ps -Lf pid

进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位

分类:

内核线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态

用户线程:用户级线程是指不需要内核支持而在用户程序中实现的线程,它的内核的切换是由用户态程序自己控制内核的切换,不需要内核的干涉。但是它不能像内核级线程一样更好的运用多核CPU

为什么要使用线程:

一个进程可以包含多个线程

  • 进程间的信息是难以共享的,父子进程除去只读代码外并没有共享内存所以需要采用通信方式来实现信息交换
  • fork()的代价较高,即使是写时复制,也需要复制内存页表,文件描述符表等
  • 线程之间可以方便的共享信息,只需将数据复制到共享(全局或堆)变量中
  • 创建线程开销更小更快,线程间共享虚拟地址空间,无需写时复制来复制内存,无需复制页表

创建一个线程并没有复制原来进程的虚拟地址空间,而是共享

image

每个线程有独立的寄存器,上下文切换:复用cpu时间片时将上一个状态保存,以便之后切换回来继续运行

Linux中现在一般使用NPTL线程库

使用getconf GNU_LIBPTHREAD_VERSION查看线程库版本

root@ziggy-virtual-machine:~# getconf GNU_LIBPTHREAD_VERSION
NPTL 2.23

线程创建/结束

如果想要同时运行多个函数,或对一个函数同时调用多次,则就需要多线程了

   #include <pthread.h>

   int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                      void *(*start_routine) (void *), void *arg);//void*是一个泛型,如果想传多个数据可以定义一个结构体
//用于创建线程,第一个参数为线程的地址(为传出参数),第二个为线程属性的指针,第三个为执行的函数名称

//第三个参数所指的函数的参数和返回值都为类型为void*的指针,用来允许他们指向任何类型的值
//第四个为第三个参数函数的参数

pthread_t pthread_self(void);//返回当前线程id,无符号长整形

//pthread_t:
typedef unsigned long int pthread_t
    //事实上,Linux上几乎所有资源标识符都是一个整形数
    一个用户可以打开的线程数量不能大于RLIMIT_NPROC限制
    所有用户能创建的线程总谁不能大于:/proc/sys/kernel/threads-max

在一个进程中调用此函数,此时这个程序就有了两个线程:主线程,子线程1

成功返回0,失败返回errcode(使用strerror(int errnum)接收,返回一个字符串)

       int pthread_join(pthread_t thread, void **retval);
//main函数用于等待线程执行路线的返回,第二个参数如果不是NULL,则会将线程的返回值存储其中
//此函数成功返回0,失败返回errcode


#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

#define NUMS 5
void *print_msg(void *m)
{
    char *cp = (char*)m;
    for(int i = 0;i<NUMS;i++)
    {
        printf("%s",cp);
        fflush(stdout);
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,NULL,print_msg,(void*)"hello");
    pthread_create(&t2,NULL,print_msg,(void*)"worldn");
    pthread_join(t1,NULL);//等待线程结束(防止主线程抢占cpu,子线程还未执行)
    pthread_join(t2,NULL);
    return 0;
}

主线程首先创建子线程,子线程被创建后执行任务函数callback,主线程继续向下执行

下面的程序中主线程没有等待子线程执行结束,主线程执行结束就释放虚拟地址空间

#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>

void* callback(void* arg){
    for(int i = 0;i < 5;i++){
        printf("子线程:i = %dn",i);
    }
    printf("子线程:%ldn",pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,callback,NULL);
    for(int i = 0;i < 5;i++){
        printf("主线程:%dn",i);
    }
    printf("主线程id:%ldn",pthread_self());
    return 0;
}

结果:
root@ziggy-virtual-machine:~/unix/ch11# ./thread1 
主线程:0
主线程:1
主线程:2
主线程:3
主线程:4
主线程id:139930043443008

pthread_exit

       #include <pthread.h>

       void pthread_exit(void *retval);

可以保证 线程安全干净的退出

通过val向pthread_join(此线程的回收者)传递退出信息,执行后不会返回到调用者,且永远不会失败

在哪个线程调用此函数之后,哪个线程就会终止

主线程退出,并不影响其他在运行的线程,所有的线程都终止进程才能退出,下面代码中子线程变为僵尸线程

#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>

void* callback(void* arg){
    for(int i = 0;i < 5;i++){
        printf("子线程:i = %dn",i);
    }
    printf("子线程:%ldn",pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,callback,NULL);
    printf("主线程id:%ldn",pthread_self());
    pthread_exit(NULL);
    return 0;
}

#include<stdio.h>
#include<pthread.h>
#include<string.h>

void* callback(void* arg){
    printf("child thread id:%ldn",pthread_self());
}

int main()
{
    pthread_t t1;
    int ret = pthread_create(&t1,NULL,callback,NULL);
    if(ret!=0){
        char *err = strerror(ret);
        printf("%sn",err);
    }
    for(int i = 0;i<5;i++){
        printf("%dn",i);
    }
    printf("tid :%ld,main thread id:%ldn",t1,pthread_self());
    pthread_exit(NULL);
    return 0;
}
/*
root@ziggy-virtual-machine:~/unix_linux/chapter14/pthread# ./ExitDemo 0
1
2
3
4
child thread id:139903656199936
tid :139903656199936,main thread id:139903664531200
*/
int pthread_equal(pthread_t t1, pthread
_t t2);
//不同操作系统中pthread_t的实现可能不同

pthread_join 线程连接

一个进程中的所有线程都可以调用此函数来回收其他线程,类似wait,waitpid,阻塞,等待要回收的线程结束,成功返回0,失败返回errcode

子线程退出时其内核(用户区的在退出时就释放了)资源主要是主线程来回收,此函数被调用一次只能回收一个子线程

       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);
//成功返回0
//retval指向一级指针的地址,保存了pthread_exit()传递出的数据
/*
errcode:
EDEADLK: 可能造成死锁,例如两个进程互相对对方调用此函数,或者自己对自己调用
EINVAL:目标线程是不可回收的,或已经有其他线程在回收目标线程
ESRCH:目标线程不存在
*/

如果主线程需要等待子线程结束之后再结束,而在具体的逻辑上主线程是早于子线程结束的,这时候就要用到此函数

默认情况下我们创建的线程都是非分离的,如果一个线程非分离且没有对其pthread_join,这个线程结束之后会变为僵尸线程(结束后不会释放其内存空间)

#include<stdio.h>
#include<pthread.h>
#include<string.h>

int num = 5;
void* callback(void* arg){
    printf("child thread id:%ldn",pthread_self());
    // int num = 5;//注意不能用局部变量,因为临时变量在栈空间退出作用域会被销毁
    // printf("num address:%pn",(void*)&num);
    pthread_exit((void*)&num);
}

int main()
{
    pthread_t t1;
    int ret = pthread_create(&t1,NULL,callback,NULL);
    if(ret!=0){
        char *err = strerror(ret);
        printf("%sn",err);
    }
    for(int i = 0;i<5;i++){
        printf("%dn",i);
    }
    
    printf("tid :%ld,main thread id:%ldn",t1,pthread_self());
    int *retval;    //存储的是地址
    pthread_join(t1,(void**)&retval);
    printf("%dn",*retval);
    return 0;
}
struct test{
    int num;
    int age;
};
struct test t;//全局变量
//或在堆创建
void* callback(void* arg){
    for(int i = 0;i < 5;i++){
        printf("子线程:i = %dn",i);
    }
    //局部变量为栈空间,退出时已经还回去了
    printf("子线程:%ldn",pthread_self());
    t.age = 1;
    t.num = 2;
    pthread_exit(&t);
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,callback,NULL);
    printf("主线程id:%ldn",pthread_self());
    void* ptr;
    pthread_join(tid,&ptr);//ptr一级指针的地址指向参数&t的地址
    struct test* pt = (struct test*)ptr;
    printf("num:%d,age:%dn",pt->num,pt->age);
    return 0;
}

上面代码用到了全局变量

也可以使用主线程的栈空间:在主线程创建结构体,通过pthread_create的参数传递

void* callback(void* arg){
    for(int i = 0;i < 5;i++){
        printf("子线程:i = %dn",i);
    }
    printf("子线程:%ldn",pthread_self());
    struct test* t3 = (struct test*)arg; 
    t3->age = 1;
    t3->num = 2;
    pthread_exit(t3);
    return NULL;
}

int main()
{
    pthread_t tid;
    struct test t2;
    pthread_create(&tid,NULL,callback,&t2);
    printf("主线程id:%ldn",pthread_self());
    void* ptr;
    pthread_join(tid,&ptr);//ptr一级指针的地址指向参数&t的地址
    struct test* pt = (struct test*)ptr;
    printf("num:%d,age:%dn",pt->num,pt->age);
    return 0;
}

线程之间默认是不共享栈空间的,但是在这里主线程主动给子线程传递了地址,本身线程都在同一块地址空间内

为什么要传入二级指针:
pthread_exit其实在此线程中相当于return

而pthread_join的第二个参数用于在pthread_join函数体中接收这个void*类型的值

如果传入的是一个一级指针,这就和在c语言,swap函数中传入int型的形参没什么区别,传入的是一个指针的副本,所以函数结束后,传入的参数不会有改变

所以要传二级指针,因为pthread_exit返回的不是一个值,而是一个地址,所以不能用一级指针

线程的分离

       #include <pthread.h>

       int pthread_detach(pthread_t thread);

主线程有自己要做的事,不能因为为了回收子线程资源而用join子线程没退出就一直被阻塞

通过此函数分离的线程,在结束时,会自动释放资源给系统,无需pthread_join

主线程退出后,子线程也就不存在了(地址空间没了)

detach后不需要主线程显式回收,其内核资源被其他进程回收

不要多次分离一个线程(文档里说可能会发生不可知的结果)

且分离后不要再对此线程连接

//分离自动回收资源,且使用pthread_exit防止影响子线程执行
void* callback(void* arg){
    for(int i = 0;i < 5;i++){
        printf("子线程:i = %dn",i);
    }
    printf("子线程:%ldn",pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    struct test t2;
    pthread_create(&tid,NULL,callback,&t2);
    printf("主线程id:%ldn",pthread_self());
    pthread_detach(tid);
    pthread_exit(NULL);
    
    return 0;
}

线程的取消 pthread_cancel

在一个线程中杀死另一个线程

线程A中调用pthread_cancel杀死B,要在B中进行一次系统调用从用户区切换到内核区B才会被真正杀死

注意标准C函数等内部调用系统调用的也算

       #include <pthread.h>

       int pthread_cancel(pthread_t thread);

取消点在文档pthread(7)中

看Linux高性能服务器编程补充此部分

void* work(void* arg){
    int j = 0;
    for(int i = 0;i<9;i++){
        j++;
    }
    printf("子线程id:%ldn",pthread_self());
    for(int i = 0;i<9;i++){
        printf("child i:%dn",i);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,work,NULL);
    printf("创建子线程id:%ldn",tid);
    printf("主线程id:%ldn",pthread_self());
    for(int i = 0;i<3;i++){
        printf("i = %dn",i);
    }
    pthread_detach(tid);
    pthread_cancel(tid);
    pthread_exit(NULL);
    return 0;
}

结果:
创建子线程id:140123976349440
主线程id:140123984848704
i = 0
i = 1
i = 2
子线程id:140123976349440

线程的属性

一般系统默认为每个线程分配8MB的栈空间
即 8 MB = 8388608 字节
image
image

原文链接: https://www.cnblogs.com/ziggystardust-pop/p/17064770.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    Unix\Linux多线程复健

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/313558

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 下午12:56
下一篇 2023年2月16日 下午12:57

相关推荐