Linux线程概念与创建

目录

1.1线程概念

1.2引入多线程的原因

1.3实现多线程的接口函数

1.4设置线程属性(设置pthread_create()函数的第二个参数)

1.5子线程获取主线程中的值

1.6多个子线程改变同一个值发生重复

1.7线程的实现

1.8Linux系统上的线程实现


线程是CPU调度和分配的基本单位

1.1线程概念

进程内部的一条执行路径,一个执行单元

单进程单线程:该进程有且只有一条执行路径。

1.2引入多线程的原因

为了在一个进程中实现多个功能,此时就需要用到多线程。比如:qq,微信等聊天软件。对于一个应用软件在使用时就是一个进程,它需要实现一边接收数据一边发送数据,而接收数据和发送数据各自是一条线程。利用单线程无法实现这样的功能,所以就需要用到多线程。(不可能使用两个单进程单线程去实现一个软件接收数据,另一个软件发送数据)

1.3实现多线程的接口函数

查看系统运行线程命令:ps -eLf

过滤使用 ps -eLf | grep " "(也可以不加双引号)

头文件:#include<pthread.h>

pthread_create() 创建线程

pthread_exit() 只退出当前线程

pthread_join() 等待线程结束或合并线程

简单的多线程程序

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void* fun(void* arg)//该fun函数的格式是固定的,返回值为void*,参数为void* arg
{
    for(int i=0;i<5;++i)
    {
        printf("fun run\n");
    }
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);
    for(int i=0;i<5;++i)
    {
        printf("main run\n");
    }
    exit(0);
}

在编译时需要链接pthread线程库,gcc -o main main.c -lpthread

pthread_create()函数最后一个参数是指针为fun函数的参数

在执行时会出现两种情况,有时候只打印5个main run,有时候打印5个main run和随意个数的fun run,甚至打印一个f

 

出现这种情况的原因是:

主函数顺序下去是一条线程,fun函数是另一条线程。在运行该进程时,两条线程都会被执行,但是存在执行的快慢。出现上面的原因就是fun函数子线程还没开始 或者 执行循环了三次 或者 刚执行打印fun run中的 f 时,主线程已经执行完毕,并且exit(0)退出了该进程,两个线程也随之结束。

一个进程中一个非主线程非异常结束不会影响其他线程的执行。如果是异常结束会导致该进程结束。主线程在不使用pthread_exit()函数时,主线程结束会导致当前进程结束以至于所有线程结束

void* fun(void* arg)
{
    for(int i=0;i < 5;++i)
    {
        printf("fun run\n");
        sleep(1);//加入sleep为了让两个线程交替执行
    }
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);
    for(int i=0;i < 2;++i)
    {
        printf("main run\n");
        sleep(1);
    }
    exit(0);//语句一
    //pthread_exit(NULL);//语句二
}

使用语句一时:

 当使用语句二时:

exit(0)是退出当前进程

pthread_exit(NULL)是退出当前线程

在主线程中不使用pthread_exit()会使得当前进程退出,而在子线程(fun函数)中使用或者不使用pthread_exit(),该线程执行完会自己结束,不会执行exit(0)使得整个进程结束。

需要子线程使用pthread_exit()函数:当子线程需要给创建它的线程返回信息时。返回的信息作为pthread_exit()函数的参数(参数为指针),而创建子进程的进程要接收这个信息需要使用到pthread_join()函数。

pthread_exit函数的参数为指针,该指针所指空间的生存周期要超过子线程的生存周期。因为在子线程结束后,需要在主线程中使用到此空间中的数据,如果该空间为局部变量,子线程结束后会销毁该数据空间,主线程将无法获取到该数据。

void* fun(void* arg)
{
    for(int i=0;i < 5;++i)
    {
        printf("fun run\n");
        sleep(1);//加入sleep为了让两个线程交替执行
    }
    pthread_exit("fun close");
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);
    for(int i=0;i < 2;++i)
    {
        printf("main run\n");
        sleep(1);
    }
    char* s=NULL;
    pthread_join(id,(void**)&s);//等待id对应的线程结束,并获取退出信息。
    exit(0);
}

pthread_join ( pthread_t thread , void * * retval )函数的作用:等待id对应的线程结束,并获取退出信息。如果当前线程先结束,子线程还未结束,则该函数会阻塞住直到子线程结束传回信息。

thread为线程的id,retval为指针的地址(是让该指针指向pthread_exit()返回的信息而pthread_exit()的参数为地址,为了让retval指向地址就得传入指针的地址)。

1.4设置线程属性(设置pthread_create()函数的第二个参数)

使用到的变量及函数:

pthread_attr_t attr; 定义属性变量

pthread_attr_init(&attr); 初始化属性变量

pthread_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); 设置属性变量,设置次属性后不需要使用pthread_join()函数去合并线程。即使使用了pthread_join()函数也会调用失败。

pthread_creat(&id,&attr,fun,NULL); 创建带属性的线程

1.5子线程获取主线程中的值

#define MAXID  5
void* fun(void* arg)
{
    int index = *(int*)arg;//语句三
    //int index = (int*)arg;//语句四
    for(int i=0;i < 3;++i)
    {
        printf("index = %d\n",index);
    }
}
int main()
{
    pthread_t id[MAXID];
    int i = 0;
    for(;i < MAXID; ++i)
    {
        pthread_create(&id[i],NULL,fun,(void*)&i);//参数&i为fun函数的参数       语句一
        //pthread_create(&id[i],NULL,fun,(void*)i);// 语句二
    }
    for(i = 0;i < MAXID; ++i)
    {
        pthread_join(id[i],NULL);
    }
    exit(0);
}

运行结果存在好几种情况:

出现这种情况的原因:

五个线程中的 arg 参数都获取了 i 的地址,所以 index 的值就是该子线程获取 ii 的值,而在主线程中 i 的值是随着主线程的运行是变化的,五个子线程运行在不同时刻获取 i 的值导致五个线程的index值不同。

以第二个结果为例:在主线程中当 i==0 时创建了第一个子线程,而在第一个子线程获取 i 的值时(即执行 int index = *(int *)arg; 语句),i 的值因为 ++i 变为 1 ,此时第一个子线程的 index 就为 1 。同样的,第二个子进程创建后,在获取 i 的值时,主线程可能运行很快已经循环了四遍,i 因为四次 ++i 变为 4 ,此时刚好被第二个子线程获取导致其 index 值为 4 。第三个子线程和第四个子线程可能运行非常快,主线程执行了一次 ++i 将 i 变为 5 就将其获取了,致使三四子进程的 index 都为 5。而第五个进程可能运行比较缓慢,主线程运行到第二个for循环并且执行两次 ++i 时,第五个线程获取了 i 的值,以至于第五个线程的 index 变为 5。

因为主线程和子线程是分开运行的,运行快慢是不确定的,而且五个子线程获取的是 i 的值,i 的值会随着主线程的运行而发生变化,子线程的index的值取决于执行 int index = *(int *)arg; 语句时主线程中 i 的值,才会导致上述情况。

如果按照顺序打印则需要将语句一和三换成语句二和四。改成值传递后,执行 int index = *(int *)arg; 语句时不会因为主线程 i 值的改变而影响 index,此时 index 的值就为传递时 i 的值。但是这样会出现警告!!也可以在创建线程前开辟一块属于该子线程的空间将此时 i 的值给该空间,将该空间的地址作为参数传给 fun函数。如下:

int* p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&id[i],NULL,fun,(void*)p);

在fun函数执行结束前进行空间释放

1.6多个子线程改变同一个值发生重复

实现对全局变量 val 每个子进程加 1000 次,预期结果为 1 到 5000

#define MAXID  5
int val=1;//全局变量
void* fun(void* arg)
{
    for(int i=0;i < 1000;++i)
    {
        printf("%d\n",val++);
    }
}
int main()
{
    pthread_t id[MAXID];
    for(int i = 0;i < MAXID; ++i)
    {
        pthread_create(&id[i],NULL,fun,NULL);
    }
    for(i = 0;i < MAXID; ++i)
    {
        pthread_join(id[i],NULL);
    }
    exit(0);
}

运行结果存在:

 

第一种为预期情况,但是在多次运行后会出现第二种和第三种情况。

原因是:val++ 不是原子操作,val++ 会转换为汇编代码去执行,val++ 一条c语言代码对应的几条汇编代码,会先将 val 的地址计算出来将 val 的值放在寄存器中,然后再将 val 放进 ALU 中进行加一操作,加完后再放入寄存器中,再将加后的值写回到变量 val 中。在这个过程中,存在一个子线程刚把 val 的值读取出来还没有去改变并写回时,另一个线程此时也读取 val 的值,而此时另外一个线程读取 val 的值会和第一个线程读取的值相同。两个线程加一后的值也相同,导致两个线程执行 val++ 后,val 只加了一次。这种情况一定是两个线程并行运行,即两个线程同时运行。当为为单核cpu时,同一时间只能运行一个线程

1.7线程的实现

分类:用户级 内核级 组合模型

用户级

操作系统提供或者不提供创建线程的接口都可以

假如该进程存在三条执行路径(线程)在用户空间模拟出来三条执行路径,而在内核空间只能感知到一条执行路径,则操作系统只会调度一个核去处理内核空间看到的这一条执行路径,即使有多个核空闲,这三个线程也只会在当前一个核上时间片轮转,进行并发执行。

特点:可以创建很多执行路径,且开销小,但无法利用多个核资源,因为内核无法感知用户空间创建了多条执行路径。

内核级

需要操作系统提供创建线程的接口

假如该进程存在三条执行路径(线程)因为使用的是操作系统提供的接口进行创建的,所以内核可以感知到三条执行路径,用户空间也能模拟出三条执行路径。如果有三个核空闲,则操作系统会将三条执行路径放在三个核上去执行,实现并行执行。但如果线程数多余空闲核数,则多余的线程依然会时间片轮转执行。

特点:由内核直接管理,且开销大,可以利用多核资源。

组合模型

同时使用用户级核内核级

特点:当需要创建线程数少于或者等于空闲核数时使用内核级,当多余空闲核数时先创建与空闲核数相应个数的线程,多余的线程使用用户级进行创建。这样既能用到多核的资源,又能在有很多线程时开销小。

1.8Linux系统上的线程实现

Linux的内核并没有线程这个概念,它是把所有线程都当作进程来实现,其内核并没有准备特别的调度算法或定义特别的数据结构来表征线程。线程仅仅被看作与其他进程贡献某些资源的进程。每个线程都有它自己的唯一的task_struct(PCB),看起来就像普通的进程(只是和其他一些进程共享一些资源,如地址空间等)。而在Windows 或者 Sun Solaris等操作系统上,都在内核中提供了专门支持线程的机制。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值