Linux -- 线程(一)


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
					(包含正在调用线程的IDSYNOPSIS
       #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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuechanba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值