多线程基础操作

版权声明:允许转载,请注明文章出处 https://blog.csdn.net/Vickers_xiaowei/article/details/83753151

本片博客会粘贴部分代码,想要了解更多代码信息,可访问小编的GitHub关于本篇的代码

- 对比进程与线程的区别

线程概念及特点

  1. Linux下线程是以进程模拟的,Linux下的进程控制块pcb实际就是一个线程,其他系统不一定
  2. Linux对进程和线程不作区分,Linux下的线程以进程PCB模拟,也就是说task_struct其实就是线程CPU调度的基本单位是线程,进程就是线程组
  3. Linux下的pcb其实就是线程的描述,Linux下的线程以进程pcb模拟,因此也叫轻量级进程。
  4. 一个进程中至少有一个线程,线程是进程内的一条执行流。
线程共享进程数据,但也拥有自己的部分数据:

线程ID
组寄存器(上下文数据)

errno
信号屏蔽字
调度优先级

如果定义1个函数,在各线程中都可以调用,如果定义1个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者?定义的信号处理函数)
当前工作目录
用户id和组id
Text Segment、Data Segment都是共享的。

进程和线程的比较,线程优缺点

线程的优点:
一个进程中有可能会有多个线程,而这些线程共享同一个虚拟地址空间,因此有时候也会说线程是在进程中的

a、 因此他们共享了整个代码段、数据段,线程间通信变得极为方便
b、创建或销毁一个线程相比较于进程来说成本更低(不需要额外创建虚拟地址空间)
c、线程的调度切换相较于进程也较低
d、线程占用的资源比进程少很多
e、能够充分利用多处理器的可并行数量
f、在等待IO操作、CPU计算等任务时候,线程支持多处理器并行处理,任务分摊,提高效率。

线程的缺点:一个进程中有可能会有多个线程,而这些线程共享同一个虚拟地址空间

a、 因为线程间的数据访问变得简单,因此数据安全访问问题更加突出,要考虑的问题增多,编码难度增多。资源争抢问题更加突出。
b、 一些系统调用和异常都是针对整个进程的,因此一个线程中出现了异常,那么,整个进程都会受到影响,以及一些系统调用的使用也是需要注意的。

进程的优点:安全、稳定(因为进程的独立性)

- 线程相关代码,总结线程属性

线程的控制

进程是操作系统资源分配的一个基本单位(通过页表)
线程是CPU调度的一个基本单位(CPU调度的是pcb)

线程创建:线程共享进程地址空间,但是每一个线程都有自己相对独立的一个地址空间

操作系统并没有提供系统调用来创建线程,所以就在posix标准库中,实现了一套线程控制(创建、终止、等待…)的接口,因为这套接口创建的线程是库函数,是用户态的线程创建,因此创建的线程也叫做用户线程。 一个用户线程对应了一个轻量级进程来进行调度运行的。(对操作系统来说,创建了一个轻量级进程)

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.

pthread_t * thread:创建的线程id
const pthread_attr_t:线程属性,一般设置为NULL
void *(*start_routine) (void *):线程的执行函数
void *arg:给线程的执行函数传的参数

pthread_create接口创建了一个用户线程,并且通过第一个参数返回了一个用户的id,这个id数字非常大,其实它就是一个地址,是指向自己的线程地址空间在整个进程虚拟地址空间中的位置。
每一个线程都需要有自己的栈区,否则如果所有线程共用一个栈的话,会引起调用栈混乱,并且因为CPU是以pcb来调度的,因此CPU调度的基本单位,所以每一个线程也都应该有自己的上下文数据来保存CPU调度切换时的数据。(而这些都是在线程地址空间中,每一个线程都有自己的线程地址空间,它们相对来说独立,但是线程地址空间是在虚拟地址空间内的)

#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>

void* start(void *arg){
    int num = (int)arg;
    while(1){
    printf("The pthread%d\n",num);
    sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,start,(void*)999);
    //pthread_t pthread_self(void);
    //  获取调用线程的线程id(用户态线程id)tid是pthread_t类型即无符号长整型lu
    printf("Main pthread ID:%lu\n",pthread_self());
    printf("The pthread!!ID:%lu\n",tid);
    while(1){
        printf("This is main pthread!!\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
Linux下线程操作函数都是库函数,需要链接动态库-pthread

thread: 用于接受一个用户线程ID
attr: 用户设置线程属性,一般置空
start_routine:线程的入口函数,线程运行的就是这个函数,这个函数退出了,线程也就退出了
arg:用于给线程入口函数传递参数
返回值:成功:0 失败:errno

其实在每一个task_struct里边都有pid、tgid,其中pid线程标识,tgid是描述该线程属于哪个线程组(进程)的线程,这个tgid实际上是该线程所处线程组的首线程的pid,也就是说在一个线程组中tgid等于pid的那个线程就是主线程。

  1. 查看进程中所有线程信息1:ps -efL
ps -efL |head -1&&ps -efL |grep create

在这里插入图片描述
LWP:轻量级进程,这一列记录的就是每个线程task_struct中的tid
NLWP:这个进程中有几个线程
PPID:父进程ID
PID:进程ID(线程组tgid),也就是主线程的tid。
2、查看线程:

ps -aL |head -1&&ps -aL |grep create

在这里插入图片描述

PID:进程ID(线程组tgid),也就是主线程的tid。

LWP:线程ID,各个task_struct中的tid.

线程没有父子之分,所有线程都是有平级的,如果非要说有区别的话,那么就是主线程和其他线程的区别。

线程终止

线程的退出方式:

  1. return num;退出:在main函数中调用return,效果是退出进程,在线程中调用return,退出线程。
  2. exit(num)针对整个进程,即使在非主线程中使用exit(),也会是整个进程退出.
void pthread_exit(void *retval);

       Compile and link with -pthread.

DESCRIPTION
The  pthread_exit()  function terminates the calling thread and returns a value
 via retval that (if the thread is joinable)is available to another thread in
  the same process that calls pthread_join(3).

retval存储线程退出状态信息,如果不关心,可以置空。
终止一个调用线程,线程调用线程退出,main函数调用,主线程退出,其他线程成了僵尸线程,进程显示僵尸进程,进程显示的是主线程stat。终止一个调用线程,线程调用线程退出,main函数调用,主线程退出,其他线程成了僵尸线程,进程显示僵尸进程,进程显示的是主线程stat。

查看线程的命令

 ps aux -L | head -1 && ps aux -L | grep exit | grep -v 'test'
ps -aL
int pthread_cancel(pthread_t thread);
	  Compile and link with -pthread.

主线程用来取消主线程自己的线程id为thread的线程

//这段代码用于演示线程退出的几种方式
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>

void *thr_start(void *arg)
{
    //在线程中调用exit函数会怎样?
    //进程要是退出了,那么进程中的线程也会退出
    //exit(0);
    //return NULL;
    //sleep(5);
    //pthread_exit(NULL);
    while(1){
    printf("child pthread!!!\n");
    sleep(1);
    }
return NULL;
}
int main()
{
    pthread_t tid;
    int ret = -1;

    ret = pthread_create(&tid,NULL,thr_start,NULL);
    if(ret != 0){
        printf("pthread create error\n");
        return -1;
    }
    //int pthread_cancel(pthread_t thread);
    //取消普通线程
    pthread_cancel(tid);
    while(1){
        printf("first pthread!!\n");
        sleep(1);
    }
    return 0;
}

线程等待(线程分离)

主线程退出,其他线程也会形成僵尸线程:占用了一部分资源不释放,最终造成资源泄露。
线程等待就是接受线程的返回值,然后释放线程的所有资源。
线程处于joinable状态才能被等待,如果一个线程在调用pthread_join函数之前已经退出,则pthread_join函数立即返回,否则阻塞等待,直到这个指定的线程退出,才会返回。

pthread_join函数的返回值只需要考虑线程的退出码或者errorno,不需要考虑线程异常的情况,因为一旦线程产生异常,系统会认为整个进程异常,这个进程就挂了。
 int pthread_join(pthread_t thread, void **retval);   //避免资源泄露,线程属性是joinable状态

pthread_t thread:指定等待线程的线程ID
void **retval:将线程退出信息接收到retval中

The pthread_join() function waits for the thread specified by thread to terminate. If that thread has already terminated,
then pthread_join() returns immediately. The thread specified by thread must be joinable.

//演示线程退出等待,获取线程返回值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

//线程需要被等待的条件是:线程处于joinable状态,一个线程创建出来默认属性就是joinable状态
void* Func()
{
    sleep(2);//让这个非主线程睡觉,让主线程等它睡醒后,接收该线程的退出状态值,释放资源
    printf("I am not the main pthread\n");
    pthread_exit("I am not the main pthread !I will exit!");
    return "I am the best!!";
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,Func,NULL);
    if(ret!=0){
        perror("pthread_create error");
        exit(-1);
    }
   
    printf("I am the Main pthread\n");

    char* addr;//定义一级指针用于给pthread_join的第二个参数初始化,否则二级指针为空,不能使用
    char** ptr = &addr;
    sleep(5);
    pthread_join(tid,(void**)ptr);//主线程到这里会阻塞等待它创建的线程id是tid的线程
    printf("%s\n",*ptr);

    //pthread_cancel(tid);
    //sleep(5);
    //pthread_join(tid,(void**)ptr);//主线程到这里会阻塞等待它创建的线程id是tid的线程
    //如果一个线程是被取消的那么它的返回值只有一个-1,PTHREAD_CANCELD
    //printf("%d\n",addr);
    return 0;
}

在这里插入图片描述
线程分离:功能是设置状态

我们等待一个线程是因为需要获取线程的返回值,并且释放资源。那么假如我不关心返回值,那么这个等待将毫无意义,仅仅是为了释放资源。因此就有一个线程属性叫:线程分离属性 detach属性,这个属性就是需要设置,它是告诉操作系统,这个指定的线程我不关心返回值,所以如果线程退出了,操作系统就自动把所有资源回收。而设置一个线程分离属性我们常称为线程分离

线程的detach属性与joinable属性相对应,也相冲突,两者不会同时存在。如果一个线程属性是detach,那么pthread_join的时候将直接报错,所以我们说,只有一个线程处于joinable状态才可以被等待

线程被设置成detach属性,退出后将自动释放资源,不会形成僵尸线程
detach与joinable属性相冲突,无法同时存在,设置detach就表明了不关心返回值

int pthread_detach(pthread_t thread);
线程也可以分离自己pthread_detach(pthread_self());

- 使用gdb调试的注意事项

1、编译过程一定要加-g选项:因为在Linux系统下,默认生成的是release(不加调试信息)版本的可执行程序,如果不加-g,则不能调试。例如编译hello.c生成hello的debug版本;

gcc -g hello.c -o hello

2、在开启gdb调试不想看到那么一大堆版本信息可以加-q,例如调试hello

gdb -q hello

3、常用选项:

run/r:执行程序到结束 continue:从当前位置开始连续而非单步执行程序到结束
breaktrace(或bt):查看各级函数调⽤及参数
start:开始单步调试,next/n下一步
step/s:进入函数,类似于VS里的F11
finish:执⾏到当前函数返回,然后挺下来等待命令

在这里插入图片描述

break/b:打断点,可以加行号或者函数

在这里插入图片描述

info break/i b:查看断点信息
info local:查看当前栈帧局部变量的值
delete/d breakpoints/number:删除所有断点/删除断点编号为number的断点
print/p:打印表达式的值,通过表达式可以修改变量的值或者调⽤函数
p 变量:打印变量值。
q/ctrl+d:退出gdb

猜你喜欢

转载自blog.csdn.net/Vickers_xiaowei/article/details/83753151