当前位置: 首页 > news >正文

linux 系统编程之线程

线程

文章目录

  • 线程
    • 1 线程概念
    • 2 NPT
      • 安装线程 man page:
      • 查看指定线程的 LWP 号:
    • 3 线程的特点
    • 4 线程共享资源
    • 5 线程非共享资源
    • 6 线程的优缺点
    • 7线程常用操作
      • 1 线程号
        • pthread_self函数:
        • pthread_equal函数:
        • 参考代码
      • 2 错误返回值分析
        • 参考代码
      • 3 线程的创建
        • pthread_create函数:
        • 参考代码
        • 创建多个线程
          • 传地址作为函数参数
          • 传值作为函数参数
      • 4 线程共享资源的验证
          • 共享数据段
          • 共享堆空间
      • 5 线程资源回收
        • pthread_join函数:
        • 参考代码
      • 6 线程分离
        • pthread_detach函数:
        • 参考代码
      • 7 线程退出
        • pthread_exit函数
        • 参考代码
      • 8 线程取消
        • pthread_calcel函数
        • 参考代码
        • 无效的线程取消
          • 参考代码
        • 线程对Cancel信号的处理
          • 参考代码(修复无效的线程取消)
      • 9 线程清理
        • 使用方法
        • 线程清理函数调用
        • 线程清理例程
    • 8 线程属性
      • 8.2 线程属性初始化和销毁
      • 8.3 线程分离状态
      • 8.4 线程栈地址
      • 8.5 线程栈大小
      • 8.6 综合参考程序
      • 8.7 线程使用注意事项
    • 补充 : pthread_join的第二个参数

1 线程概念

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。为了让进程完成一定的工作,进程必须至少包含一个线程。

1528121100232

1

  • 进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位

  • 线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

  • 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  • 如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。

  • 进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。

  • 多线程和多进程的区别:

    • 多进程共享的资源:
      • 代码
      • 文件描述符
      • 内存映射区 --mmap
    • 多线程共享的资源:
      • 全局变量
      • 相比多进程,更加节省系统资源;对于系统 CPU 轮转时间片来说,不论是线程还是进程,它不认识,只认 PCB。
  • 主线程和子线程:

    • 共享:
      • 用户区内,除了栈区是不共享的,其余都是不共享的。
    • 不共享:
      • 栈区(当有 1 主 + 4 子线程时候,栈区会被平分为 5 份)
  • 在 Linux 下:

    • 线程就是进程 – 轻量级的进程
    • 对于内核来说,线程就是进程(内核只会用)

进程是操作系统分配资源的最小单位

线程是操作系统调度的最小单位

2 NPT

  • 当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合POSIX的要求。

  • 要改进 LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPTNext-GenerationPOSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL

  • NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合POSIX的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。

  • 查看当前pthread库版本:getconf GNU_LIBPTHREAD_VERSION

1528121402843

安装线程 man page:

  • 安装线程 man page 命令:

    sudo apt install manpages-posix-dev
    1
    

查看指定线程的 LWP 号:

  • 线程号和线程 ID 是有区别的
  • 线程号是给内核看的
  • 查看方式:
    • 找到程序的进程 ID
    • ps -Lf pid

一个例子,查看火狐浏览器程序由多线程构成:

如: Linux 下 查看火狐浏览器,发现其是由多线程构成的

ps ajx | grep "firefox"ps -Lf 31102
123

3 线程的特点

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切:

  • \1) 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  • \2) 从内核里看进程和线程是一样的,都有各自不同的PCB.
  • \3) 进程可以蜕变成线程
  • \4) 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

1528121496711

查看指定进程的LWP号:

ps -Lf pid

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。

Ø 如果复制对方的地址空间,那么就产出一个“进程”;

Ø 如果共享对方的地址空间,就产生一个“线程”。

Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

4 线程共享资源

  • \1) 文件描述符表

  • \2) 每种信号的处理方式

  • \3) 当前工作目录

  • \4) 用户ID和组ID

  • 内存地址空间 (.text/.data/.bss/heap/共享库)

5 线程非共享资源

  • \1) 线程id
  • \2) 处理器现场和栈指针(内核栈)
  • \3) 独立的栈空间(用户空间栈)
  • \4) errno变量
  • \5) 信号屏蔽字
  • \6) 调度优先级

6 线程的优缺点

优点:

  • Ø 提高程序并发性
  • Ø 开销小
  • Ø 数据通信、共享数据方便

缺点:

  • Ø 库函数,不稳定
  • Ø 调试、编写困难、gdb不支持
  • Ø 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

7线程常用操作

1 线程号

就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。

进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。

有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

pthread_self函数:

#include <pthread.h>
pthread_t pthread_self(void);
功能:获取线程号。
参数:无
返回值:调用线程的线程 ID 。

pthread_equal函数:

int pthread_equal(pthread_t t1, pthread_t t2);
功能:判断线程号 t1 和 t2 是否相等。为了方便移植,尽量使用函数来比较线程 ID。
参数:t1,t2:待判断的线程号。
返回值:相等:  非 0不相等:0

参考代码

// todo 创建线程号,获取线程号,比较线程号
#include "../tou.h"
int main()
{pthread_t tid = 0;    // todo 创建线程号//todo 如果不确定 pthread_t 是无符号的整型,还是一个结构体,可以使用下面的方式进行初始化
/*
memset(&tid,0,sizeof(tid));
或者bzero(&tid,sizeof(tid));
*/tid = pthread_self(); // todo 获取当前线程的线程号printf("tid: =%ld", tid);// todo 为了方便移植, 使用函数来比较线程idpthread_t tid2 = pthread_self();if (pthread_equal(tid, tid2)){printf("两个线程相同\n");}else{printf("两个线程不同\n");}
}

【注意】线程函数的程序在 pthread 库中,故链接时要加上参数 -lpthread。

2 错误返回值分析

注意,所有线程的错误号返回都只能使用strerror这个函数判断,不能使用perror .因为perror是调用进程的全局错误号,不适合单独线程的错误分析,所以只能使用strerror

参考代码

#include "../tou.h"void *thrd_func(void *arg)
{printf("i am detach.\n");
}int main(void)
{pthread_t tid;int ret;ret = pthread_create(&tid, NULL, thrd_func, NULL);if (ret != 0){fprintf(stderr, "pthread_create error:%s\n", strerror(ret));exit(1);}ret = pthread_detach(tid);if (ret != 0){fprintf(stderr, "pthread_detach error:%s\n", strerror(ret));exit(1);}sleep(1);ret = pthread_join(tid, NULL);if (ret != 0){fprintf(stderr, "pthread_detach error:%s\n", strerror(ret));exit(1);}// 如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
}

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。

3 线程的创建

pthread_create函数:

#include <pthread.h>
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg );
功能:创建一个线程。
参数:thread:线程标识符地址。attr:线程属性结构体地址,通常设置为 NULL。start_routine:线程函数的入口地址。arg:传给线程函数的参数。
返回值:成功:0失败:非 0

参考代码

#include "../tou.h"
void *func(void *arg)
{printf("子线程:%ld被执行\n", pthread_self());if (arg == NULL){printf("参数为空\n");}return NULL;
}
void *func2(void *arg)
{printf("子线程:%ld被执行\n", pthread_self());if (arg != NULL){int arg_t = (int)(long)arg; // 把八个字节的arg 给四个字节的arg_t .long类型占八字节printf("传入的参数为%d\n", arg_t);}return NULL;
}
int main()
{pthread_t tid = -1;int ret = -1;// 传入空的线程描述符和空的线程函数参数ret = pthread_create(&tid, NULL, func, NULL); // 如果ret >0 就说明创建成功if (ret == 0){printf("2子线程:%ld被执行\n", tid);};// 传入空的线程描述符和 一个线程参数pthread_t tid2 = -1;ret = pthread_create(&tid2, NULL, func2, (void *)200); // void* 占8字节if (ret == 0)                                          // 如果ret >0 就说明创建成功{printf("子线程:%ld被执行\n", tid2);}; // 传入空的线程描述符和空的线程函数参数pthread_join(tid, NULL);pthread_join(tid2, NULL);
}

创建多个线程

传地址作为函数参数
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>void* pthread_fun(void* arg)
{int index = *(int*)arg;int i = 0;for(; i < 5; i++){printf("index = %d\n",index);sleep(1);}
}int main()
{pthread_t id[5];int i = 0;for(; i < 5; i++){pthread_create(&id[i],NULL,pthread_fun,(void*)&i);}for(i = 0; i < 5; i++){pthread_join(id[i],NULL);}exit(0);
}

运行结果

在这里插入图片描述

或者

在这里插入图片描述

为什么会产生这种情况呢?线程并发问题。

这是因为我们向pthread_fun传入i的地址。首先来说说为什么会出现多个线程拿到同一个i的值。线程创建在计算机中需要很多个步骤,我们进入for循环传入i的地址后就去进行下一个for循环,创建的线程还没有从地址中获取打印i的值,主函数就继续创建后面的线程了,导致多个线程并发,拿到同一个i值,而且不是创建该线程的时候i的值。

注意到打印第一个运行结果都是打印0,这是因为主函数第一个for循环已经结束了,后面一个for循环将i又置为0,而这些线程在主函数第一个for循环执行的时候,都没有回获取i的值打印,直到下一个for循环,这些线程才获取i值打印,所以打印出来 都是0。

传值作为函数参数
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>void* pthread_fun(void* arg)
{int index =(int) arg;int i = 0;for(; i < 5; i++){printf("index = %d\n",index);sleep(1);}
}int main()
{pthread_t id[5];int i = 0;for(; i < 5; i++){pthread_create(&id[i],NULL,pthread_fun,(void*)i);}for(i = 0; i < 5; i++){pthread_join(id[i],NULL);}exit(0);
}

4 线程共享资源的验证

共享数据段
#include "../tou.h"// 创建一个全局变量
int num = 100;
void *func(void *arg)
{printf("begin func\n");num++;printf("end func\n");
}
int main()
{pthread_t tid;bzero(&tid, sizeof(tid)); // todo 初始化 线程号// todo 创建线程int ret = pthread_create(&tid, NULL, func, NULL);pthread_join(tid, NULL);if (ret == 0){printf("线程创建成功,num 为:%d\n ", num);}
}
共享堆空间
#include "../tou.h"void *func(void *arg)
{int *pn = (int *)arg;printf("begin func\n");(*pn)++;printf("end func\n");
}
int main()
{// 创建堆空间int *p = NULL;p = malloc(sizeof(int));if (NULL == p){printf("分配失败");exit(1);}// 对堆空间进行初始化memset(p, 0, sizeof(int));// 放入数据到该堆空间*p = 828;pthread_t tid;bzero(&tid, sizeof(tid)); // todo 初始化 线程号// todo 创建线程int ret = pthread_create(&tid, NULL, func, (void *)p);pthread_join(tid, NULL);if (ret == 0){printf("线程创建成功,*p 为:%d\n ", *p);}
}

5 线程资源回收

pthread_join函数:


#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:thread:被等待的线程号。retval:用来存储线程退出状态的指针的地址。
返回值:成功:0失败:非 0

pthread_join得到的终止状态是不同的,总结如下:

  • 1) 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值
  • 2)如果thread线程被别的线程调用pthread_cance异常终止掉, reuva所指向的单元里存放的是常数PTHREAD_CANCELED
  • 3)如果thread线程是自己调用pthread_exit终止的, reuvaI所指向的单元存放的是传给pthread_exit的参数。

参考代码


#include "../tou.h"void *func(void *arg)
{int ret = 999;pthread_exit((void *)(long)ret);return NULL;
}void *func2(void *arg)
{int ret = 888;return ((void *)(long)ret);
}int main()
{pthread_t tid;bzero(&tid, sizeof(tid)); // todo 初始化 线程号// todo 创建线程int ret = pthread_create(&tid, NULL, func, NULL);// 定义一个变量来存储线程退出状态的指针的地址void *p = NULL;// todo 演示 pthread_exit()pthread_join(tid, &p); // 这里是void **类型  // 参见  https://www.coder.work/article/1566260if (ret == 0){printf("线程创建成功");printf("*p :%d", (int)(long)p);}printf("\n\n===================================\n");// todo 演示直接返回ret = pthread_create(&tid, NULL, func2, NULL);pthread_join(tid, &p); // 这里是void **类型  // 参见  https://www.coder.work/article/1566260if (ret == 0){printf("线程创建成功");printf("*p :%d\n", (int)(long)p);}
}

pthread_cancel 后面线程取消再演示

6 线程分离

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

pthread_detach函数:

#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:thread:线程号。
返回值:成功:0失败:非0

参考代码

#include "../tou.h"void *func(void *arg)
{sleep(5);return NULL;
}
int main()
{pthread_t tid;bzero(&tid, sizeof(tid)); // todo 初始化 线程号// todo 创建线程int ret = pthread_create(&tid, NULL, func, NULL);if (ret == 0){printf("线程创建成功\n");}
#if 0 // 此分支的代码会阻塞5s,再结束主线程ret = pthread_join(tid, NULL);if (ret == 0){printf("阻塞成功\n");}#endif
#if 1// 此分支,主线程会立即结束ret = pthread_detach(tid);if (ret == 0){printf("分离成功\n");}#endifprintf("main end...\n\n");
}

7 线程退出

在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。

  • 线程从执行函数中返回。
  • 线程调用pthread_exit退出线程。
  • 线程可以被同一进程中的其它线程取消

pthread_exit函数

pthread_exit函数:#include <pthread.h>void pthread_exit(void *retval);
功能:退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
参数:retval:存储线程退出状态的指针。
返回值:无  

参考代码

#include "../tou.h"// void *func(void *arg)//todo 使用exit(0)
// {
//     printf("begin func\n");
//     exit(0); //线程和主进程进程直接退出
//     printf("end func\n");
// }void *func(void *arg) // todo 使用 phread_exit(void*retval)
{printf("begin func\n");pthread_exit(NULL);printf("end func\n");
}void *func(void *arg) // todo 使用 return NULL
{printf("begin func\n");return NULL;  //等同于使用pthread_exit()printf("end func\n");
}int main()
{pthread_t tid;bzero(&tid, sizeof(tid)); // todo 初始化 线程号// todo 创建线程int ret = pthread_create(&tid, NULL, func, NULL);if (ret == 0){printf("线程创建成功\n");}pthread_join(tid, NULL);printf("主线程开始睡眠\n");sleep(5);printf("主线程结束睡眠\n");
}

8 线程取消

pthread_calcel函数


#include <pthread.h>int pthread_cancel(pthread_t thread);
功能:杀死(取消)线程
参数:thread : 目标线程ID。
返回值:成功:0失败:出错编号

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。

可粗略认为一个系统调用(进入内核)即为一个取消点。

参考代码

线程取消,线程可以被同一进程中的其它线程取消

#include "../tou.h"
void *func(void *arg) // todo 使用 return NULL
{printf("begin func\n");while (1){sleep(1);printf("I am func \n");}printf("end func\n");
}
void *func2(void *arg) // todo 使用 return NULL
{pthread_t tid = (pthread_t)arg;printf("begin func2\n");sleep(5); // 先让子线程活5s,在取消它int ret = pthread_cancel(tid);if (ret == 0){printf("线程取消成功\n");}printf("end func2\n");
}
int main()
{pthread_t tid;bzero(&tid, sizeof(tid)); // todo 初始化 线程号// todo 创建线程int ret = pthread_create(&tid, NULL, func, NULL);if (ret == 0){printf("func线程创建成功\n");}pthread_t tid2;ret = pthread_create(&tid2, NULL, func2, (void *)tid);if (ret == 0){printf("func2线程创建成功\n");}ret = pthread_join(tid2, NULL);if (ret != 0){printf("等待func2线程失败\n");return 0;}void *mess = NULL; // 获取线程被强制取消之后的状态// 获取已终止线程的返回值ret = pthread_join(tid, &mess);if (ret != 0){printf("等待func线程失败\n");return 0;}// 如果线程被强制终止,其返回值为 PTHREAD_CANCELEDif (mess == PTHREAD_CANCELED){printf("func 线程被强制终止\n");}else{printf("func error\n");}
}

无效的线程取消

参考代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>   //调用 sleep() 函数
void * thread_Fun(void * arg) {printf("新建线程开始执行\n");//插入无限循环的代码,测试 pthread_cancel()函数的有效性while(1);
}
int main()
{pthread_t myThread;void * mess;int value;int res;res = pthread_create(&myThread, NULL, thread_Fun, NULL);if (res != 0) {printf("线程创建失败\n");return 0;}sleep(1);//令 myThread 线程终止执行res = pthread_cancel(myThread);if (res != 0) {printf("终止 myThread 线程失败\n");return 0;}printf("等待 myThread 线程执行结束:\n");res = pthread_join(myThread, &mess);if (res != 0) {printf("等待线程失败\n");return 0;}if (mess == PTHREAD_CANCELED) {printf("myThread 线程被强制终止\n");}else {printf("error\n");}return 0;
}

https://raw.githubusercontent.com/xkyvvv/blogpic/main/pic1/image-20210711094517042.png

程序中,主线程( main() 函数)试图调用 pthread_cancel() 函数终止 myThread 线程执行。从运行结果不难发现,pthread_cancel() 函数成功发送了 Cancel 信号,但目标线程仍在执行。

也就是说,接收到 Cancel 信号的目标线程并没有立即处理该信号,或者说目标线程根本没有理会此信号。解决类似的问题,我们就需要搞清楚目标线程对 Cancel 信号的处理机制。

根据上节的内容,pthread_join会阻塞调用它的线程,因此程序在执行完printf(“等待 myThread 线程执行结束:\n”);后就会一直阻塞在res = pthread_join(myThread, &mess);等待目标线程myThread执行完毕

线程对Cancel信号的处理

对于默认属性的线程,当有线程借助 pthread_cancel() 函数向它发送 Cancel 信号时,它并不会立即结束执行,而是选择在一个适当的时机结束执行。

所谓适当的时机,POSIX 标准中规定,当线程执行一些特殊的函数时,会响应 Cancel 信号并终止执行,比如常见的 pthread_join()、pthread_testcancel()、sleep()、system() 等,POSIX 标准称此类函数为“cancellation points”(中文可译为“取消点”)。

POSIX 标准中明确列举了所有可以作为取消点的函数,这里不再一一罗列,感兴趣的读者可以自行查阅 POSIX 标准手册。

此外,<pthread.h> 头文件还提供有 pthread_setcancelstate() pthread_setcanceltype() 这两个函数,我们可以手动修改目标线程处理 Cancel 信号的方式。

在这里插入图片描述

在这里插入图片描述

1)线程取消函数:pthread_cancel()
2)设置线程取消响应—>是否响应取消信号
3)设置响应取消信号的类型---->立即响应、延时响应
代码段:

参考代码(修复无效的线程取消)
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
struct Data
{char pthread_name[10];
};void *Pthread_Task(void *arg)
{//设置线程取消状态---接受取消请求int pthread_setcancelstate_ret = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);if (pthread_setcancelstate_ret != 0){perror("pthread_setcancelstate");exit(-1);}while (1){struct Data *p = (struct Data *)arg;printf("%s\n", p->pthread_name);sleep(1);}pthread_exit(NULL);
}int main()
{pthread_t pid;struct Data d1;memset(&d1, 0, sizeof(d1));strcpy(d1.pthread_name, "hello");//创建线程int ret = pthread_create(&pid, NULL, Pthread_Task, (void *)&d1);if (ret != 0){perror("pthread_create");exit(-1);}printf("5s之后发送取消请求\n");sleep(5);//取消线程pthread_cancel(pid);pause();return 0;
}

9 线程清理

有时候我们希望线程退出时能够自动的执行某些函数,为了能达到此目的,OS 提供了两个函数帮我们完成这个功能:

void pthread_cleanup_push(void (*rtn)(void*), void *arg);
void pthread_cleanup_pop(int execute);

在这里插入图片描述

使用方法

如果想要你的线程在退出时能够执行清理函数,你需要使用 pthread_cleanup_push 对你的清理函数进行注册,如下:

void clean(void *arg) {// ...
}void *th_fn(void *arg) {// push 和 pop 必须对对出现pthread_cleanup_push(clean, /* 清理函数 clean 的参数*/);// ...pthread_cleanup_pop(1);
}

在 Linux 中,pthread_cleanup_push 和 pthread_cleanup_pop 这两个函数是通过宏来做的,pthread_cleanup_push 被替换成以左花括号 { 为开头的一段代码,而 pthread_cleanup_pop 被替换成以右花括号 } 结尾的一段代码,这就意味着这两个函数必须要成对出现才能将左右花括号匹配上,否则就出现编译错误。
有些平台可能不是使用宏来实现,就算不成对也没什么关系。

线程清理函数调用

有三种情况线程清理函数会被调用:
1、线程还未执行 pthread_cleanup_pop 前,被 pthread_cancel 取消
2、线程还未执行 pthread_cleanup_pop 前,主动执行 pthread_exit 终止
3、线程执行 pthread_cleanup_pop,且 pthread_cleanup_pop 的参数不为 0

注意:如果线程还未执行 pthread_cleanup_pop 前通过 return 返回,是不会执行清理函数的。

线程清理例程

程序 clean 需要传入两个参数,第 1 个参数表示是否提前返回(在执行 pthread_cleanup_pop 前返回),第 2 个参数表示 pthread_cleanup_pop 的参数。所以有 4 种组合情况。

#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>int excute;void cleanup(void* arg) {printf("cleanup: %s\n", (char*)arg);
}void* th_fn1(void* arg) {puts("thread 1 starting");pthread_cleanup_push(cleanup, "线程 1 清理者 1 号");pthread_cleanup_push(cleanup, "线程 1 清理者 2 号");if (arg) {printf("线程 1 提前退出\n");return (void*)1;}pthread_cleanup_pop(excute);pthread_cleanup_pop(excute);printf("线程 1 正常退出\n");return (void*)10;
}void* th_fn2(void* arg) {puts("thread 2 starting");pthread_cleanup_push(cleanup, "线程 2 清理者 1 号");pthread_cleanup_push(cleanup, "线程 2 清理者 2 号");if (arg) {printf("线程 2 提前退出\n");pthread_exit((void*)2);}pthread_cleanup_pop(excute);pthread_cleanup_pop(excute);printf("线程 2 正常退出\n");pthread_exit((void*)20);
}int main(int argc, char* argv[]) {if (argc < 3) {printf("Usage: %s <arg 0|1> <excute 0|1>\n", argv[0]);return -1;}pthread_t tid1, tid2;int err;void* ret;void *arg = NULL;excute = 0;arg = (void*)atoi(argv[1]);excute = atoi(argv[2]);err = pthread_create(&tid1, NULL, th_fn1, arg);err = pthread_create(&tid2, NULL, th_fn2, arg);err = pthread_join(tid1, &ret);printf("thread 1 exit code %d\n", (int)ret);err = pthread_join(tid2, &ret);printf("thread 2 exit code %d\n", (int)ret);return 0;
}

img

结果可以看到:

当 clean 程序中的线程正常返回时,只有 pthread_cleanup_pop 的参数非 0 时,才会正常执行清理函数。

当 clean 程序中的线程在执行 pthread_cleanup_pop 前时,使用 pthread_exit 退出时,清理函数才会被执行,和pthread_cleanup_pop 的参数没有关系。而使用 return 返回的线程 1 并不会执行清理函数。清理函数的执行顺序,是按照注册时候相反的顺序执行的。

注意,在有些系统中(如Mac OS X),提前终止可能会出现段错误。

8 线程属性

Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。

如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

typedef struct
{int             etachstate;     //线程的分离状态int             schedpolicy;    //线程调度策略struct sched_param  schedparam; //线程的调度参数int             inheritsched;   //线程的继承性int             scope;      //线程的作用域size_t          guardsize;  //线程栈末尾的警戒缓冲区大小int             stackaddr_set; //线程的栈设置void*           stackaddr;  //线程栈的位置size_t          stacksize;  //线程栈的大小
} pthread_attr_t;

主要结构体成员:

\1) 线程分离状态

\2) 线程栈大小(默认平均分配)

\3) 线程栈警戒缓冲区大小(位于栈末尾)

\4) 线程栈最低地址

  • 属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。
  • 线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

8.2 线程属性初始化和销毁

#include <pthread.h>int pthread_attr_init(pthread_attr_t *attr);
功能:初始化线程属性函数,注意:应先初始化线程属性,再pthread_create创建线程
参数:attr:线程属性结构体
返回值:成功:0失败:错误号
​
int pthread_attr_destroy(pthread_attr_t *attr);
功能:销毁线程属性所占用的资源函数
参数:attr:线程属性结构体
返回值:成功:0失败:错误号

8.3 线程分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

  • 非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
  • 分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

相关函数:


#include <pthread.h>int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
功能:设置线程分离状态
参数:attr:已初始化的线程属性detachstate:    分离状态PTHREAD_CREATE_DETACHED(分离线程)PTHREAD_CREATE_JOINABLE(非分离线程)
返回值:成功:0失败:非0int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
功能:获取线程分离状态
参数:attr:已初始化的线程属性detachstate:    分离状态PTHREAD_CREATE_DETACHED(分离线程)PTHREAD _CREATE_JOINABLE(非分离线程)
返回值:成功:0失败:非0

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。

要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。

设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

8.4 线程栈地址

POSIX.1定义了两个常量来检测系统是否支持栈属性:

  • _POSIX_THREAD_ATTR_STACKADDR
  • _POSIX_THREAD_ATTR_STACKSIZE

也可以给sysconf函数传递来进行检测:

  • _SC_THREAD_ATTR_STACKADDR
  • _SC_THREAD_ATTR_STACKSIZE

当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。

#include <pthread.h>int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr,  size_t stacksize);
功能:设置线程的栈地址
参数:attr:指向一个线程属性的指针stackaddr:内存首地址stacksize:返回线程的堆栈大小
返回值:成功:0失败:错误号
​
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr,  size_t *stacksize);
功能:获取线程的栈地址
参数:attr:指向一个线程属性的指针stackaddr:返回获取的栈地址stacksize:返回获取的栈大小
返回值:成功:0失败:错误号
​

8.5 线程栈大小

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

#include <pthread.h>int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
功能:设置线程的栈大小
参数:attr:指向一个线程属性的指针stacksize:线程的堆栈大小
返回值:成功:0失败:错误号
​
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
功能:获取线程的栈大小
参数: attr:指向一个线程属性的指针stacksize:返回线程的堆栈大小
返回值:成功:0失败:错误号
​

8.6 综合参考程序

#define SIZE 0x100000void *th_fun(void *arg)
{while (1){sleep(1);}
}int main()
{pthread_t tid;int err, detachstate, i = 1;pthread_attr_t attr;size_t stacksize;void *stackaddr;pthread_attr_init(&attr);  //线程属性初始化pthread_attr_getstack(&attr, &stackaddr, &stacksize); //获取线程的栈地址pthread_attr_getdetachstate(&attr, &detachstate);           //获取线程分离状态if (detachstate == PTHREAD_CREATE_DETACHED){printf("thread detached\n");}else if (detachstate == PTHREAD_CREATE_JOINABLE){printf("thread join\n");}else{printf("thread unknown\n");}pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置分离状态while (1) {stackaddr = malloc(SIZE);if (stackaddr == NULL) {perror("malloc");exit(1);}stacksize = SIZE;pthread_attr_setstack(&attr, stackaddr, stacksize); //设置线程的栈地址err = pthread_create(&tid, &attr, th_fun, NULL); //创建线程if (err != 0) {printf("%s\n", strerror(err));exit(1);}printf("%d\n", i++);}pthread_attr_destroy(&attr); //销毁线程属性所占用的资源函数return 0;
}

8.7 线程使用注意事项

\1) 主线程退出其他线程不退出,主线程应调用pthread_exit

\2) 避免僵尸线程

a) pthread_join

b) pthread_detach

c) pthread_create指定分离属性

被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

\3) malloc和mmap申请的内存可以被其他线程释放

\4) 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程t在子进程中均pthread_exit

\5) 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

补充 : pthread_join的第二个参数

在看pthread相关,遇到了pthread_join函数

#include <pthread.h>
int pthread_join(pthread_t pthread_id, void** retval);

这个函数的第二个参数为什么是void**?有啥用?

首先,这个函数的用途是什么? manpage给出的解释:

The pthread_join() function waits for the thread specified by threadto terminate.  If that thread has already terminated, thenpthread_join() returns immediately.  The thread specified by threadmust be joinable.If retval is not NULL, then pthread_join() copies the exit status ofthe target thread (i.e., the value that the target thread supplied topthread_exit(3)) into the location pointed to by retval.  If thetarget thread was canceled, then PTHREAD_CANCELED is placed in thelocation pointed to by retval.If multiple threads simultaneously try to join with the same thread,the results are undefined.  If the thread calling pthread_join() iscanceled, then the target thread will remain joinable (i.e., it willnot be detached).

意图很明显,以阻塞的方式等待指定线程(可joinable)结束。成功返回0,失败返回错误号。

一个线程的结束,有两种方式,一种是正常结束。一种是使用pthread_exit。对于使用pthread_exit结束的线程,可以返回一个"status"给主线程。

void pthread_exit(void* retval);

那么它的参数就和pthread_join第二个参数就对应上了。看一下怎么用!

//错误演示
#include <error.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>#include <pthread.h>struct my_threadfunc_error
{int a;int b;
};void* StartFunction(void* arg)
{my_threadfunc_error var{1,2};pthread_exit((void*)&var);return NULL;
}int main(int argc, char* argv[])
{pthread_t my_pthread_t = 0;if (0 != pthread_create(&my_pthread_t, NULL, StartFunction, NULL)){printf("pthread_create error!\n");return -1;}void** p = NULL;if (0 != pthread_join(my_pthread_t, p)){printf("pthread_join error!\n");return -1;}my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11printf("var.a = %d, var.b = %d\n", var->a, var->b);return 0;
}

上面代码在线程函数中返回的是一个栈上的变量的地址,所在在代码运行到my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11的时候,就出现段错误了。很好理解,因为线程函数结束,线程的栈也会被回收,var的空间也被清理了,所以使用未知的内存发生段错误正常。

既然如此,那么使用malloc或者全局变量即可处理这种问题!

#include <error.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>#include <pthread.h>struct my_threadfunc_error
{int a;int b;
};void* StartFunction(void* arg)
{my_threadfunc_error* var = (my_threadfunc_error*)malloc(sizeof(my_threadfunc_error));var->a = 100;var->b = 200;pthread_exit((void*)var);return NULL;
}int main(int argc, char* argv[])
{pthread_t my_pthread_t = 0;if (0 != pthread_create(&my_pthread_t, NULL, StartFunction, NULL)){printf("pthread_create error!\n");return -1;}void* p = NULL;if (0 != pthread_join(my_pthread_t, &p)){printf("pthread_join error!\n");return -1;}my_threadfunc_error* var = (my_threadfunc_error*)(p);printf("var.a = %d, var.b = %d\n", var->a, var->b);free(var);return 0;
}

{
printf(“pthread_create error!\n”);
return -1;
}
void** p = NULL;
if (0 != pthread_join(my_pthread_t, p))
{
printf(“pthread_join error!\n”);
return -1;
}
my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11
printf(“var.a = %d, var.b = %d\n”, var->a, var->b);
return 0;
}


上面代码在线程函数中返回的是一个栈上的变量的地址,所在在代码运行到my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11的时候,就出现段错误了。很好理解,因为线程函数结束,线程的栈也会被回收,var的空间也被清理了,所以使用未知的内存发生段错误正常。既然如此,那么使用malloc或者全局变量即可处理这种问题!```c++
#include <error.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>#include <pthread.h>struct my_threadfunc_error
{int a;int b;
};void* StartFunction(void* arg)
{my_threadfunc_error* var = (my_threadfunc_error*)malloc(sizeof(my_threadfunc_error));var->a = 100;var->b = 200;pthread_exit((void*)var);return NULL;
}int main(int argc, char* argv[])
{pthread_t my_pthread_t = 0;if (0 != pthread_create(&my_pthread_t, NULL, StartFunction, NULL)){printf("pthread_create error!\n");return -1;}void* p = NULL;if (0 != pthread_join(my_pthread_t, &p)){printf("pthread_join error!\n");return -1;}my_threadfunc_error* var = (my_threadfunc_error*)(p);printf("var.a = %d, var.b = %d\n", var->a, var->b);free(var);return 0;
}

相关文章:

linux 系统编程之线程

线程 文章目录线程1 线程概念2 NPT安装线程 man page&#xff1a;查看指定线程的 LWP 号&#xff1a;3 线程的特点4 线程共享资源5 线程非共享资源6 线程的优缺点7线程常用操作1 线程号pthread_self函数&#xff1a;pthread_equal函数:参考代码2 错误返回值分析参考代码3 线程的…...

从0开始学python -35

Python3 File(文件) 方法 open() 方法 Python open() 方法用于打开一个文件&#xff0c;并返回文件对象。 在对文件进行处理过程都需要使用到这个函数&#xff0c;如果该文件无法被打开&#xff0c;会抛出 OSError。 注意&#xff1a;使用 open() 方法一定要保证关闭文件对…...

1.14 golang中的结构体

1. 结构体 Go语言中没有“类”的概念&#xff0c;也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。 1.1. 类型别名和自定义类型 1.1.1. 自定义类型 在Go语言中有一些基本的数据类型&#xff0c;如string、整…...

原创不易,坚持更难

早上CSDN发消息&#xff0c;今天是创作满三年的纪念日&#xff0c;邀请写一篇博文&#xff0c;谈谈感受 开博原因 2020年是一个特殊的年份&#xff0c;疫情刚爆发第一年&#xff0c;也是第一次居家办公&#xff0c;从过完年就一直居家办公&#xff0c;一直居家了38天。2020年…...

计算机网络 | 谈谈TCP的流量控制与拥塞控制

文章目录一、TCP的流量控制1、利用滑动窗口实现流量控制【⭐⭐⭐】2、如何破解【死锁】局面❓二、TCP的拥塞控制1、拥塞控制的一般原理① 解决网络拥塞的误区② 拥塞控制与流量控制的关系【重点理解✔】2、TCP的拥塞控制方法① 接收窗口【rwnd】与拥塞窗口【cwnd】② 慢开始和拥…...

Flask入门(7):内置装饰器(钩子函数)

目录7.内置装饰器&#xff08;钩子函数&#xff09;7.1 before_request7.2 after_request7.3 before_first_request7.4 error_handlers7.5 template_filter7.6 template_global复习装饰器基础及其应用&#xff0c;可参考文章&#xff1a;闭包和装饰器 7.内置装饰器&#xff08…...

Java8新特性

✨作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 写在最前面 在企业中更多的都是使用 Java8 &#xff0c;随着 Java8 的普及度越来越高&#xff0c;很多人都提到面试中关于Java 8 也…...

哈希表题目:设计哈希集合

文章目录题目标题和出处难度题目描述要求示例数据范围解法一思路和算法代码复杂度分析解法二思路和算法代码复杂度分析题目 标题和出处 标题&#xff1a;设计哈希集合 出处&#xff1a;705. 设计哈希集合 难度 3 级 题目描述 要求 不使用任何内建的哈希表库设计一个哈希…...

java static关键字 万字详解

目录 一、为什么需要static关键字&#xff1a; 二、static关键字概述 : 1.作用 : 2.使用 : 三、static修饰成员变量详解 : 1.特点 : 2.细节 : ①什么时候考虑使用static关键字? ②静态变量和非静态变量的区别&#xff1f; ③关于静态变量的初始化问题 : ④关于静态变…...

光谱实验反射、透射光谱测量

标题反射、透射光谱测量的基本原理  暗背景/基线&#xff1a;Dark………………………………………………………………0%  &#xff08;空&#xff09;白参考&#xff1a;Reference…………………………………………………………100%  样品反射/透射光谱&#xff1a;Sampl…...

【基础算法】之 冒泡排序优化

冒泡排序思想基本思想: 冒泡排序&#xff0c;类似于水中冒泡&#xff0c;较大的数沉下去&#xff0c;较小的数慢慢冒起来&#xff08;假设从小到大&#xff09;&#xff0c;即为较大的数慢慢往后排&#xff0c;较小的数慢慢往前排。直观表达&#xff0c;每一趟遍历&#xff0c;…...

Python | 线程锁 | 3分钟掌握【同步锁】(Threading.Lock)

文章目录概念无锁加锁死锁解决死锁概念 threading.Lock 同步锁&#xff0c;可以用于保证多个线程对共享数据的独占访问。 当一个线程获取了锁之后&#xff0c;其他线程在此期间将不能再次获取该锁&#xff0c;直到该线程释放锁。这样就可以保证共享数据的独占访问&#xff0c…...

Linux下安装MySQL8.0的详细步骤(解压tar.xz安装包方式安装)

Linux下安装MySQL8.0的详细步骤 第一步&#xff1a;下载安装配置 第二步&#xff1a;修改密码&#xff0c;并设置远程连接&#xff08;为了可以在别的机器下面连接该mysql&#xff09; 第三步&#xff1a;使用Navicat客户端连接 搞了一台云服务器&#xff0c;首先要干的活就是…...

leaflet 绘制多个点的envelope矩形(082)

第082个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中如何根据多边形的几个坐标点来绘制envelope矩形。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共78行)安装插件相关API参考:专栏目标示例…...

CAJ论文怎么批量免费转换成Word

大家都知道CAJ文件吗&#xff1f;这是中国学术期刊数据库中的文件&#xff0c;这种文件类型比较特殊。如果想要提取其中的内容使用&#xff0c;该如何操作呢&#xff1f;大家可以试试下面这种免费的caj转word的方法,多个文档也可以一起批量转换。准备材料&#xff1a;CAJ文档、…...

面试必问: 结构体大小的计算方法

结构体大小的计算需同时满足以下几点 一、结构体成员的偏移量必须是当前成员大小的整数倍。&#xff08;0是任何数的整数倍&#xff09; 举一个例子 struct Test1{char a; // 当前偏移量为0&#xff0c;是char所占字节数1的整数倍 所以所占大小为1char b; …...

Java中super函数的用法

1 问题 Java中super函数有很多方法&#xff0c;在使用的时候我们应该如何正确区分&#xff1f; 2 方法 三种用法&#xff1a; 访问父类的方法。 调用父类构造方法。 访问父类中的隐藏成员变量。 class A{ int x,y; A(int x,int y){ System.out.println("A"); } } cla…...

第十一届“泰迪杯”数据挖掘挑战赛携“十万”大奖火热来袭

第十一届“泰迪杯”数据挖掘挑战赛 竞赛组织 主办单位&#xff1a; 泰迪杯数据挖掘挑战赛组织委员会 承办单位&#xff1a; 广东泰迪智能科技股份有限公司 人民邮电出版社 协办单位&#xff1a; 重庆市工业与应用数学学会、广东省工业与应用数学学会、广西数学学会、河北省工业…...

分享三个可以在家做的正规兼职工作,看到就是赚到

你可以在家做正式的兼职工作。在线兼职工作值得考虑&#xff0c;时间相对自由。在线兼职收入可能不如线下滴滴和外卖立竿见影&#xff0c;但仍然可以坚持收入。有些人比工作工资发展得更高。当然&#xff0c;天上不会有馅饼&#xff0c;不劳无获。那么有哪些正规的兼职可以在家…...

javaFx实现鼠标穿透画布,同时操作画布和桌面,背景透明,类似ppt批注

一、功能需要由来和大致效果 今天&#xff0c;我们要用javaFx来实现一个鼠标穿透画布的功能&#xff0c;该需求来自于在我们的javaFx桌面应用中&#xff0c;需要实现一个悬浮的桌面侧边工具栏&#xff0c;在工具栏中有画笔绘制&#xff0c;批注的功能&#xff0c;能够实现在任何…...

后进先出(LIFO)详解

LIFO 是 Last In, First Out 的缩写&#xff0c;中文译为后进先出。这是一种数据结构的工作原则&#xff0c;类似于一摞盘子或一叠书本&#xff1a; 最后放进去的元素最先出来 -想象往筒状容器里放盘子&#xff1a; &#xff08;1&#xff09;你放进的最后一个盘子&#xff08…...

Python爬虫实战:研究MechanicalSoup库相关技术

一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

大型活动交通拥堵治理的视觉算法应用

大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动&#xff08;如演唱会、马拉松赛事、高考中考等&#xff09;期间&#xff0c;城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例&#xff0c;暖城商圈曾因观众集中离场导致周边…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

uniapp中使用aixos 报错

问题&#xff1a; 在uniapp中使用aixos&#xff0c;运行后报如下错误&#xff1a; AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...

OpenLayers 分屏对比(地图联动)

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能&#xff0c;和卷帘图层不一样的是&#xff0c;分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...