从C++软件调试实战的角度去看多线程编程中的若干细节问题

news2025/1/11 3:00:10

目录

1、线程与线程函数基础知识

1.1、创建线程的函数返回时不代表代码执行到线程函数中了

1.2、创建线程的函数返回后要调用CloseHandle将线程句柄(引用计数)释放掉

1.3、线程何时退出并结束?

2、线程函数的几个细节

3、回调函数运行在主调线程中,不能发生堵塞

4、多线程之间在操作共享资源时要做同步

4.1、两个线程同时对一个整型的全局变量进行自加操作

4.2、一个线程在遍历STL列表、另一个线程在删除STL列表元素或者向列表中添加元素

5、多线程死锁问题

5.1、发生死锁的场景说明

5.2、锁的类型

5.3、多线程死锁的排查实例

6、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html开源组件及数据库技术(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_2276111.html       为了提高业务处理的效率,软件中会频繁地使用多线程,作为基础编程技能的多线程编程技术包含了多个细节问题。本文在多年的项目实践与排查问题经验的基础上,从C++软件调试实战的角度去讲解多线程编程中的若干知识点及常见问题,希望能给大家提供一个借鉴或参考。

1、线程与线程函数基础知识

       创建线程时会指定一个线程函数,最终这个线程是跑在这个线程函数中的。线程函数退出了,线程也就结束了。

1.1、创建线程的函数返回时不代表代码执行到线程函数中了

        以Windows平台为例,当我们调用CreateThread或者_beginthreadex去发起线程创建请求,当CreateThread或者_beginthreadex函数返回时,并不代表代码已经跑到线程函数里了。有可能已经跑到线程函数中了,也有可能还没运行到线程函数中。

       以前我们排查过这样一个问题:定义了如下的线程函数ThreadFunc,线程函数中访问了一个类指针对象m_pVideoCap:

unsigned int ThreadFunc(void* pParam)
{
    ... // 代码省略
        
    if ( m_pVideoCap != NULL )
    {
        m_pVideoCap->GetFrameData();
    }

    ... // 代码省略
}

创建线程的代码如下:

// 创建线程
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, NULL);
if ( hThread != NULL )
{
    CloseHandle(hThread);
}

// 将指针变量m_pVideoCap初始化为NULL 
m_pVideoCap = NULL;

我们在主线程中调用系统函数_beginthreadex创建一个新的线程,将线程的线程函数指定为ThreadFunc,然后在创建线程的函数返回之后去初始化指针变量m_pVideoCap。

       上述这段代码会时不时崩溃,崩溃在线程函数ThreadFunc中,具体是崩溃在使用m_pVideoCap调用函数GetFrameData处。但崩溃不是必现的,只是偶尔才会出现。这是一个我之前在项目中排查的问题实例。

       上述代码将指针变量m_pVideoCap初始化为NULL的操作,放到调用_beginthreadex创建线程之后。是否会崩溃,主要有以下两个场景:

1)不崩溃的场景:如果_beginthreadex返回时,线程还没走到线程函数中,紧接着将m_pVideoCap初始化为NULL,代码就不会崩溃。

2)发生崩溃的场景:如果_beginthreadex返回时,线程就执行到线程函数中,指针变量m_pVideoCap还没来得及初始化为NULL,线程函数中就访问了,即访问了未初始化的指针变量,所以产生了崩溃。

       对于发生崩溃的场景,为啥会发生崩溃呢?可能有人会说,不是已经判断m_pVideoCap是否为NULL了吗?为啥还会发生崩溃呢?原因是这样的,对于Visual Studio编译出来的程序,未初始化的变量的内存会被自动填充成0xCCCCCCCC(该变量是在栈上分配内存的)或0xCDCDCDCD(该变量是在堆上分配内存的),所以在执行到if判断m_pVideoCap指针是否为空时,m_pVideoCap还没初始化,其值是自动填充的0xCDCDCDCD,所以还是执行到if语句内部的m_pVideoCap->GetFrameData()这行代码上了。

       关于0xCCCCCCCC、0xCDCDCDCD、0xFEEEFEEE等常见异常值的说明,可以参见我之前写的文章:

内存中常见异常值的说明(0xcccccccc、0xcdcdcdcd、0xfeeefeee和0xdddddddd 等)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/128285918       至于m_pVideoCap->GetFrameData()这个函数调用为什么会发生崩溃,可能是以下这两个原因之一:

1)可能GetFrameData函数内部访问了其所在类的数据成员,读变量的值就是访问数据成员内存中的内容,因为所在类对象的地址是个异常值,通过这个异常值去访问类的数据成员(内存)(类的成员变量的内存首地址就是相对于当前类对象首地址的偏移值),会触发内存访问违例。
2)可能GetFrameData函数是虚函数,虚函数的调用会涉及到二次寻址,因为当前类对象的首地址为异常值,所以二次寻址时会访问不该访问的内存,引发内存访问违例。

1.2、创建线程的函数返回后要调用CloseHandle将线程句柄(引用计数)释放掉

      此外,在创建线程的函数返回后,要调用CloseHandle将线程句柄(引用计数)释放一次,否则会导致线程句柄泄漏。如果频繁地创建线程不释放线程句柄,就会导致进程的总句柄值累积,可能会达到进程总句柄数的上限,导致后面创建线程会失败。

       进程默认的总句柄上限值是10000个,包括文件句柄、线程句柄、进程句柄等,在注册表中可以查看到,也可以修改:(一般不建议修改这些系统的默认值)

       关于进程句柄上限的说明,可以参见我之前写的文章:

从注册表看Windows系统进程GDI句柄及进程句柄数上限icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/80076343深入探究 C++ 编程中的资源泄漏问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/133631728

1.3、线程何时退出并结束?

       当线程函数执行完退出时,线程就跟着退出了。在部分异常的场景下,我们可能要强制结束线程,可以调用ExitThread或者TerminateThread去强制结束线程。但一般不推荐使用这类强制结束线程的做法,可能会导致部分资源没有释放,应该让线程“优雅的”退出,即让线程函数执行完自行退出。

       让线程函数正常执行完后返回,可以确保以下的清理工作都能被执行:

  • 线程函数中创建的所有 C++对象都通过其析构函数被正确销毁。
  • 操作系统正确释放线程栈使用的内存。
  • 操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值。
  • 系统递减少线程的内核对象的使用计数。

       比如我们在线程函数中设置了一个while循环,去循环地处理事务,我们可以设置一个BOOL控制变量m_bRunning:

unsigned int ThreadFunc(void* pParam)
{
    while ( m_bRunning )
    {
        ... // do something
    }
}

在需要线程及时退出时将BOOL变量m_bRunning 置为FALSE,让While循环结束,让线程函数尽快退出,线程函数退出了,线程也就结束了。

       下面我们来看看调用ExitThread和TerminateThread的区别:(这段内容可以参看《Windows核心编程》相关章节)

1)调用ExitThread强制退出线程
ExitThread函数需要在线程函数中执行,只能退出当前线程,不能终止其他进程。该函数将终止线程的运行,操作系统会清理该线程使用的所有操作系统资源。
2)调用TerminateThread强制终止线程
调用TerminateThread可以直接去终止其他线程,只要能获取到要终止的线程句柄,将线程句柄传给TerminateThread函数就可以终止线程了。

       如果通过调用ExitThread函数的方式来终止一个线程的运行,该线程的堆栈也会被销毁。但如果使用的是 TerminateThread,那么除非拥有此线程的进程终止运行(进程退出或终止),否则系统不会销毁这个线程的堆栈。Microsoft 故意以这种方式来实现TerminateThread。否则,假如其他还在运行的线程要引用被“杀死”的那个线程的堆栈上的值,就会引起访问违规。让被“杀死”的线程的堆栈保留在内存中,其他的线程就可以继续正常运行。

       TerminateThread 函数是异步的。也就是说,它告诉系统你想终止线程,但在函数返回时,并不保证线程已经终止了。如果需要确定线程已终止运行,还需要调用WaitForSingleObject或类似的函数,并向其传递线程的句柄。相关代码如下所示:

HANDLE hThread = NULL;

... // 代码省略,已经获取到要终止的线程句柄,并保存到hThread中

TerminateThread( hThread, 0 );
WaitForSingleObject( hThread, INFINITE); // 参数值INFINITE表示无限等到,直到线程退出,也可以设置指定的超时时间,比如30ms

       此外,DLL动态链接库通常会在线程终止运行时收到通知。但如果线程是用TerminateThread强行“杀死”的,则DLL不会收到这个通知,其结果是不能执行正常的清理工作(这点可以参照《Windows核心编程》第20章DLL高级技术中查看详细说明)。

2、线程函数的几个细节

       有时为了不阻塞主线程,我们需要把一些比较耗时的操作放到一个线程中去执行,待事务处理完线程就退出了。

       有时我们需要在线程中设置一个循环,去循环执行相关的业务,比如需要持续地处理底层上来的,或者服务器过来的消息及数据。对于后面这种循环执行业务操作的场景,一般我们需要人为地在循环体加上一小段延时,不让线程一直持续的运行,如果线程一直在不间歇的运行,则会占用大量的CPU时间片,会导致进程占用CPU较高的问题。

       一旦线程调用Sleep或WaitForSingleObject时,线程就会挂起,不再消耗CPU时间片,待Sleep时间到或者wait到对象了,所在线程就退出挂起状态,进入待运行状态,然后系统就会给该线程重新分配时间片,线程得以继续运行。

       之前排查的多个CPU占用高的问题,就是线程函数中代码一直在不间歇的运行导致的,有可能是发生了死循环,也有可能是在某些场景下没有人为地加Sleep导致的,相关案例可以查看我之前写的文章:

使用Process Explorer/Process Hacker和Windbg高效排查软件高CPU占用问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/134180480使用Process Explorer查看线程的函数调用堆栈去排查程序高CPU占用问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/132830803

3、回调函数运行在主调线程中,不能发生堵塞

       特别是在业务分层的软件中,下层模块的消息与数据是通过上层给下层设置的回调函数回调给上层的。回调函数是上层模块实现的,将回调函数的地址设置给下层模块,相关示例代码如下:

// 1、回调函数声明
typedef void (__stdcall *PMsgCallBackFunc)( DWORD dwEvent, const char* pMsgBody, DWORD dwMsgLen );

// 2、设置回调函数(底层模块提供的接口)
void __stdcall SetMsgCallBack( PMsgCallBackFunc pMsgCallBackFunc );

// 3、回调函数实现(上层模块实现的回调函数)
void __stdcall MsgCallBackFunc( DWORD dwEvent, const char* pMsgBody, DWORD dwMsgLen )
{
    ... // 代码省略
}

下层模块在向上层模块投递消息及数据时会调用回调函数。所以回调函数虽然是上层模块实现的,但实际上回调函数是运行在下层模块的线程中的。

       所以一般消息及数据的处理不能放在回调函数中处理,因为消息与数据的处理可能会比较耗时,直接在回调函数中处理会导致回调函数的堵塞,会直接导致调用回调函数的下层模块中的线程的堵塞,这样就会直接影响下层模块线程的业务处理。一般会采用如下的处理方式:

在回调函数中将消息及数据先缓存起来,然后通知上层模块的线程到缓存中取消息及数据,上层线程从缓存中拿到消息及数据后再去处理,这样回调函数能尽快地返回,不会堵塞底层模块的线程,不会影响到底层模块线程的业务处理。

注意,这套逻辑中涉及到两个线程的交互,一个回调函数所在的线程(底层模块的线程),一个是上层模块的线程,两个线程在操作缓存时要加锁做同步。

4、多线程之间在操作共享资源时要做同步

       多个线程在同时操作共享资源时要做同步,所谓同步就是在访问共享资源时加锁,防止出现一个线程在读、另一个线程在写的冲突,或者两个线程在同时写的冲突,以避免出现unexpected不可预知的异常。

       下面我举两个典型的问题场景来说明问题,可能问题本身比较简单,但都很有代表性,都很能说明问题:

1)实例1:两个线程同时对一个整型的全局变量进行自加操作的问题;

2)实例2:两个线程在同时操作同一个STL列表,一个线程在遍历STL列表,另一个线程在从STL列表中删除元素或者向STL列表中添加元素的问题。

4.1、两个线程同时对一个整型的全局变量进行自加操作

       这是来自《Windows核心编程》一书第8章第1节中的一个典型实例,之前我在讲了解汇编的好处的话题时也引用了这个实例。要完全理解这个实例,需要从汇编代码的角度去看。

       该例子中定义了一个long型的全局变量,然后创建了两个线程,线程函数分别是ThreadFunc1和ThreadFunc2,这两个线程函数中均对g_x变量进行自加操作(在访问共享变量g_x时未加锁同步),相关代码如下:

// define a global variable
long g_x = 0;
 
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
    g_x++;
    return 0;
}
 
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
    g_x++;
    return 0;
}

这里有个问题,当这两个线程函数执行完后,全局变量g_x的值会是多少呢?一定会是2吗?

       实际上,在两个线程函数执行完后,g_x的值不一定为2。这个实例需要从汇编代码的角度去理解,从C++源码看则很难搞懂,这是一个从汇编代码角度去理解代码执行细节的典型实例。

熟悉汇编代码,不仅可以辅助排查C++软件异常,还可以理解很多高级语言无法理解的代码执行细节。

       有些人可能觉得,代码中就是一个自加的操作,一下子就执行完了,中间应该不会被打断。会不会被打断,其实要看汇编代码的,这行C++源码对应三行汇编代码,对g_x变量的自加这句C++代码,对应的汇编代码如下:

MOV EAX, [g_x]  // 将g_x变量的值读到EAX寄存器中
INC EAX         // 将EAX中的值执行自加操作
MOV [g_x], EAX  // 然后将EAX中的值设置到g_x变量内存中

看C++代码:g_x++,只能保证CPU执行某条汇编指令时不会被打断(汇编指令是CPU执行的最小粒度),但3条汇编指令,指令与指令之间是可能被打断的。

       为什么说两个线程执行完成后g_x变量的值是不确定的呢?比如可能存在两种场景:

1)场景1(最终结果g_x=2) 

       假设线程1先快速执行了三行汇编指令,未被打断,g_x的值变成1。然后紧接着线程2执行,在g_x=1的基础上累加,最终两个线程执行完后,g_x等于2。

2)场景2(最终结果g_x=1)

      假设线程1先执行,当执行完前两条汇编指令后,线程1失去时间片(线程上下文信息保存到CONTEXT结构体中):

即线程1前两条汇编指令执行完,第3条汇编指令还没来得及执行,就失去CPU时间片了!

        线程2执行,一次执行完三条指令,当前g_x=1。然后线程1获得CPU时间片,因为上次执行两条汇编指令后EAX寄存器中的值为1,因为线程1获取了时间片,保存线程上下文信息的CONTEXT恢复到线程1中,EAX=1,继续执行第3条指令,执行完后g_x还是1。

       所以,这个多线程问题,需要从汇编代码的角度去理解,从C++源码的角度很难想明白。

      从本例可以看出,即使是简单的变量自加操作,多线程操作时也要做同步,可以加锁,可以使用系统的原子锁Interlocked系列函数,比如原子自加函数InterlockedIncrement和原子自减函数InterlockedDecrement:

LONG InterlockedIncrement(  
    LPLONG volatile lpAddend   // variable to increment
);
 
LONG InterlockedDecrement(  
    LPLONG volatile lpAddend   // variable address
);

这些原子函数能保证会被原子地被执行,中间不会被打断。 修改后的代码为:

// define a global variable
long g_x = 0;
 
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
    InterlockedIncrement(&g_x);  // 调用原子锁函数InterlockedIncrement实现自加
    return 0;
}
 
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
    InterlockedIncrement(&g_x);  // 调用原子锁函数InterlockedIncrement实现自加
    return 0;
}

      关于为什么要学习汇编以及学习汇编有哪些好处,可以查看我之前写的文章:
为什么要学习汇编?学习汇编有哪些好处?icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/130935428     关于排查C++软件异常所需要掌握的基础汇编知识,可以查看我之前写的文章:
分析C++软件异常需要掌握的汇编知识汇总icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124758670

4.2、一个线程在遍历STL列表、另一个线程在删除STL列表元素或者向列表中添加元素

       两个线程都访问了同一个STL列表,一个线程会遍历STL列表,另一线程会删除STL列表中的元素或者向列表中加入新的元素。但这两个线程操作同一个STL列表时不加锁,不一定会引发问题,甚至是很难出现读写的冲突,只是偶尔出现甚至这样的代码在好几年中都没出现过问题!巧合的是,这样的问题场景前段时间我们就遇到过,代码是好几年前写的,之前几年从来没报过问题,结果在某天突然出现了。

       只有当一个线程正在遍历STL列表,另一个线程正在向STL列表删除或添加元素,正好赶上时机了,两个线程在同时操作STL列表,然后就出问题了。这些对STL列表的操作可能掩藏的比较深,经历了多个函数的调用触发的,走读代码时可能很难快速发现问题相关的场景演示代码如下所示:

// 1、ROOM信息结构体定义
typedef struct tagTRoomInfo
{
    char  szRoomName[128];  // Room名称
    char  szRoomId[128];    // RoomId
    ...     // 其他字段省略

    tagTRoomInfo() { memset(this, 0, sizeof(tagTRoomInfo)); }
}*PTRoomInfo, TRoomInfo;


// 2、定义看存放房间信息的STL列表
std::vector<TRoomInfo*> g_vtChatRooms;


// 3、通过roomid去遍历STL列表
TRoomInfo* FindRoom(char* lpszRoomId)
{
    std::vector<TRoomInfo*>::iterator it = g_vtChatRooms.begin();
    for (  ; it != g_vtChatRooms.end(); it++ )
    {
        if ( strcmp(lpszRoomId, it->szRoomId) == 0 )
        {
            return *it;
        }
    }

    return NULL;
}


// 4、线程函数中调用了遍历STL列表的接口FindRoom(线程1)
unsigned int ThreadFunc(void* pParam)
{
    ... // 代码省略

    string strRoomId;
    // 中间已经对strRoomId赋值,相关代码省略

    TRoomInfo* pDstRoom = FindRoom(strRoomId.c_str());

    ... // 代码省略
}


// 5、向STL列表中添加元素的接口(线程2调用了这个接口)
void AddRoom(TRoomInfo* lpRoomInfo)
{
    g_vtChatRooms.push_back(lpRoomInfo);
}

线程函数ThreadFunc中调用FindRoom接口去遍历STL列表g_vtChatRooms(线程1),与此同时,另一个线程(线程2)在调用AddRoom在向STL列表中添加元素,这样就造成读写冲突了。应该在两个线程相关的地方都加锁。

5、多线程死锁问题

       为了实现多线程之间能安全地访问一些共享资源(比如内存资源),我们会给共享资源加锁,以保证某个时刻不会出现一个线程在写资源、另一个线程在读资源的冲突情况。但加锁后,如果控制的不好,则可能会出现多线程之间的死锁问题。

       死锁一般发生在多个线程之间,一般会涉及到两个或两个以上的锁。下面我们先大概地讲述一些发生死锁的场景及用于线程间同步的锁的类型。

5.1、发生死锁的场景说明

       比如当前有两个线程,线程1和线程2;当前有两个锁,锁1和锁2。假设线程1占用了锁1,正在申请锁2,同时线程2占用了锁2,正在申请锁1,两线程都占用了各自的锁,都在申请对方占用的锁,各不相让,如下所示:

这样就导致了死锁,这是个典型的死锁场景。

       还有一个比较典型的场景是,线程1和线程2之间发生了死锁,导致了线程3的死锁。假设线程2占用了线程3要申请的锁3,因为线程1与线程2之间产生了死锁,导致线程2一直在占用锁3,一直没有释放。而线程3的代码进入了要申请锁3的代码中,因为线程2一直在占用锁3不释放,这样也导致了线程3的死锁,如下所示:

5.2、锁的类型

       此处我们以Windows平台的多线程锁为例来展开。在Windows平台中可以用多个对象来实现多线程间的锁,比如临界区对象、事件对象、互斥量对象、信号量对象等。

       这些对象主要分用户态对象和内核态对象,其中临界区属于用户态对象,事件、互斥量和信号量则属于内核态对象。使用用户态对象的好处是,不用在用户态与内核态之间切换,在效率上相对高一些,所以在Windows平台上用户态的临界区用的比较多一些。使用内核态对象时,大部分程序代码都运行在用户态的,当操作到这些内核态对象时在底层就需要切换到内核态中,完成对应的操作后再返回到用户态代码中。如果代码在用户态和内核态之间频繁的切换,则执行效率上会有损伤。

       用户态的临界区锁,只能用于一个进程中的多个线程间的同步。而事件、互斥量和信号量都属于内核态的对象,除了可以用于一个进程中的多个线程的同步,还可以跨进程使用。

       使用Windbg去排查用户态的临界区死锁,则相对容易一些,Windbg默认是运行在用户态中的。如果要排查内核态锁引发的死锁,则要复杂一些,Windbg需要切入到内核态中去分析。

5.3、多线程死锁的排查实例

       多线程死锁可以使用打印日志去排查,也可以通过调试器去分析。之前写过一篇使用Windbg分析多线程死锁的实例,可以查看对应的文章:

使用Windbg分析多线程临界区死锁问题分享icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/128532743

6、最后

       本文系统地总结了多线程编程中的若干细节问题,希望能给C++初学者或刚入门的人提供一定的借鉴或参考。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1199004.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CenterOS 安装 Jira 需求/BUG管理工具

一、Jira 安装配置 1.1 安装 Jira 下载安装包 https://product-downloads.atlassian.com/software/jira/downloads/atlassian-jira-software-9.5.0-x64.bin将下载的安装包上传至服务器中。 创建 jira 安装目录和数据存放目录 mkdir -p /opt/jira/data添加可运行权限 chmo…

线性代数理解笔记

一.向量引入: 向量&#xff1a;只由大小和方向决定&#xff0c;不由位置决定。 二.向量加减法 向量的加法是首尾相连&#xff0c;减法是尾尾相连。 而向量v向量w为平行四边形主对角线。 向量v-向量w为平行四边形副对角线。 2.向量内积点乘&#xff08;内积&#xff09; 内积…

八种架构设计模式优缺点

目录 1、软件架构 2、架构设计模式 2.1、单库单应用模式 2.2、内容分发模式 2.3、查询分离模式 2.4 微服务模式 2.5 多级缓存模式 1、软件架构 软件架构是指对软件系统整个结构和组成部分之间的关系进行抽象和定义的过程&#xff0c;旨在解决系统设计和实现过程中的复杂…

CSS注入的四种实现方式

目录 CSS注入窃取标签属性数据 简单的一个实验&#xff1a; 解决hidden 方法1&#xff1a;jsnode.js实现 侧信道攻击 方法2&#xff1a;对比波兰研究院的方案 使用兄弟选择器 方法3&#xff1a;jswebsocket实现CSS注入 实验实现&#xff1a; 方法4&#xff1a;window…

ROC 曲线详解

前言 ROC 曲线是一种坐标图式的分析工具&#xff0c;是由二战中的电子和雷达工程师发明的&#xff0c;发明之初是用来侦测敌军飞机、船舰&#xff0c;后来被应用于医学、生物学、犯罪心理学。 如今&#xff0c;ROC 曲线已经被广泛应用于机器学习领域的模型评估&#xff0c;说…

「题解」反转链表 返回中间节点

文章目录 &#x1f349;题目1&#xff1a;反转链表&#x1f349;解析&#x1f34c;解法一&#xff1a;创建一个新链表&#x1f34c;解法二&#xff1a;直接操作原链表 &#x1f349;题目2&#xff1a;返回中间节点&#x1f34c;解法一&#xff1a;快慢指针&#x1f34c;解法二&…

2023年【汽车驾驶员(高级)】找解析及汽车驾驶员(高级)复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 汽车驾驶员&#xff08;高级&#xff09;找解析是安全生产模拟考试一点通总题库中生成的一套汽车驾驶员&#xff08;高级&#xff09;复审考试&#xff0c;安全生产模拟考试一点通上汽车驾驶员&#xff08;高级&#…

【Linux】WSL安装Kali及基本操作

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍WSL安装Kali及基本操作。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路…

采用示波器显示扭矩传感器模拟信号

扭矩传感器输出的信号波形通常是模拟电压信号&#xff0c;可以通过示波器等仪器进行分析。扭矩传感器的输出信号波形通常有两种类型&#xff1a;正弦波和方波。 应变片传感器扭矩测量采用应变电测技术。在弹性轴上粘贴应变计组成测量电桥&#xff0c;当弹性轴受扭矩产生微小变…

IPV4过渡IPV6的关键技术NAT(Network AddressTranslation,网络地址转换)

文章目录 NAT的由来NAT基本工作机制NAT技术的分类推荐阅读 NAT的由来 随着物联网、工业互联网、5G的快速发展&#xff0c;网络应用对IP地址的需求呈现出爆炸式的增长。 然而&#xff0c;早在2011年&#xff0c;ICANN就发布公告称最后五组IP地址已分配完毕&#xff0c;已无IPv4…

华为ensp搭建小型园区网络规划

文章目录 前言一、拓扑图二、数据规划三、设备配置四.配置命令1.配置接入层交换机ACC11.1 设备命名&#xff0c;创建VLAN1.2 配置eth-trunk 11.3 配置用户端 2.配置核心层交换机CORE2.1设备命名2.2配置Eth-Trunk2.3 vlan配置ip2.4 上行接口配置 3.DHCP配置3.1 CORE: 4.配置路由…

【CASS精品教程】cass3d 11.0加载超大影像、三维模型、点云数据

CAD2016+CASS11.0(内置3d)下载与安装: 【CASS精品教程】CAD2016+CASS11.0安装教程(附CASS11.0安装包下载)https://geostorm.blog.csdn.net/article/details/132392530 一、cass11.0 3d支持的数据 cass11.0中的3d模块增加了多种数据的支持,主要有: 1. 三维模型 点击…

Python文件、文件夹操作汇总

目录 一、概览 二、文件操作 2.1 文件的打开、关闭 2.2 文件级操作 2.3 文件内容的操作 三、文件夹操作 四、常用技巧 五、常见使用场景 5.1 查找指定类型文件 5.2 查找指定名称的文件 5.3 查找指定名称的文件夹 5.4 指定路径查找包含指定内容的文件 一、概览 ​在…

Least Square Method 最小二乘法(图文详解,必懂)

最小二乘法是一种求解线性回归模型的优化方法&#xff0c;其目标是最小化数据点和拟合直线之间的残差平方和。这意味着最小二乘法关注的是找到一个直线&#xff0c;使得所有数据点与该直线的偏差的平方和最小。在数学公式中&#xff0c;如果y是实际值&#xff0c;y是函数估计值…

头歌答案Python——JSON基础

目录 ​编辑 Python——JSON基础 第1关&#xff1a;JSON篇&#xff1a;JSON基础知识 任务描述 第2关&#xff1a;JSON篇&#xff1a;使用json库 任务描述 Python——XPath基础 第1关&#xff1a;XPath 路径表达式 任务描述 第2关&#xff1a;XPath 轴定位 任务描述…

计算机毕业设计:疲劳驾驶检测识别系统 python深度学习 YOLOv5 (包含文档+源码+部署教程)

[毕业设计]2023-2024年最新最全计算机专业毕设选题推荐汇总 1、项目介绍 基于YOLOv5的疲劳驾驶检测系统使用深度学习技术检测常见驾驶图片、视频和实时视频中的疲劳行为&#xff0c;识别其闭眼、打哈欠等结果并记录和保存&#xff0c;以防止交通事故发生。本文详细介绍疲劳驾…

2023-11-12 LeetCode每日一题(Range 模块)

2023-03-29每日一题 一、题目编号 715. Range 模块二、题目链接 点击跳转到题目位置 三、题目描述 Range模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们。 半开区间 [left, right) 表示所有 left < x < right 的实数 x 。 实…

服务号如何升级订阅号

服务号和订阅号有什么区别&#xff1f;服务号转为订阅号有哪些作用&#xff1f;首先我们要知道服务号和订阅号有什么区别。服务号侧重于对用户进行服务&#xff0c;每月可推送4次&#xff0c;每次最多8篇文章&#xff0c;发送的消息直接显示在好友列表中。订阅号更侧重于信息传…

利用移动性的比例公平蜂窝调度测量和算法

&#xff08;一支笔一包烟&#xff0c;一节论文看一天 &#xff09;&#xff08;一张纸一瓶酒&#xff0c;一道公式推一宿&#xff09; 摘要1. 引言2. 相关工作3. 模型和问题公式4. 预测FPF调度 &#xff08; P F &#xff09; 2 S &#xff08;PF&#xff09;^2S &#xff08;…

在线制作仿真病历证明软件,易语言实现病例报告生成器,取画板快照+标签+编辑框

闲着无聊用易语言开发了一个病例生成器&#xff0c;当然我加了水印的&#xff0c;这个图片你就算截图你也用不了&#xff0c;模板是从百度图库搜的&#xff0c;很多&#xff0c;我就随便找了一个&#xff0c;然后实现逻辑就是加了一个画板&#xff0c;然后载入了素材图&#xf…