SQLAlchemy的使用

OpenGL管线基础概念编程

  返回  

Linucx -- 线程(一)

2021/8/21 20:49:59 浏览:

Linucx -- 线程一

  • 线程概念
  • Linux内核线程实现原理
  • 线程共享资源
  • 线程非共享资源
  • 线程的优点
  • 线程的缺点
  • 创建一个线程
  • 线程的退出
    • 线程退出的注意事项:
    • 线程的回收
  • 杀死线程
    • 取消点
  • 线程分离
    • 线程属性设置--分离态
  • Native POSIX Thread Library (NPTL)
  • 线程使用注意事项总结
  • 作业:实现多线程拷贝

线程概念

轻量级的进程(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编译代码的注意事项。

线程的退出

除了使用sleep来延迟时间让线程有机会来执行之外,还有什么方法可以让线程有机会执行呢?可以使用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;
}

执行结果如下:
会有警告,忽略。
在这里插入图片描述

杀死线程

需要了解一个函数-- 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)
  6 {
  7     while(1)
  8     {
  9   //    printf("I am a thread, my pid=%d, my tid = %ld.\n",getpid(),pthread_self());
 10 //          sleep(1);
 11     }
 12
 13     return NULL;
 14 }

再运行程序,我们会发现杀不死线程了,原因是什么呢?就是取消点的问题,在上图中有所说明。

我们使用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函数对线程进行回收了,否则会报错。

线程属性设置–分离态

在上述代码中,我们使用了线程分离函数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

线程使用注意事项总结

在这里插入图片描述

作业:实现多线程拷贝

联系我们

如果您对我们的服务有兴趣,请及时和我们联系!

服务热线:18288888888
座机:18288888888
传真:
邮箱:888888@qq.com
地址:郑州市文化路红专路93号