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

目录

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++初学者或刚入门的人提供一定的借鉴或参考。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

扭矩传感器信号模拟地、数据地与电源地

在电子电路中&#xff0c;电源地、信号地、数字地和模拟地都是不同的地&#xff08;ground&#xff09;节点&#xff0c;它们在电路中有不同的作用。 电源地&#xff08;Power Ground&#xff09;是指用于连接电源电源回路的地节点。在大多数电子设备中&#xff0c;电源地通常是…

线性代数理解笔记

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

《数据结构、算法与应用C++语言描述》-队列的应用-工厂仿真

工厂仿真 完整可编译运行代码见&#xff1a;Github::Data-Structures-Algorithms-and-Applications/_19Factory simulation/ 问题描述 一个工厂有m台机器。工厂的每项任务都需要若干道工序才能完成。每台机器都执行一道工序&#xff0c;不同的机器执行不同的工序。一台机器一…

如何让useEffet支持async/await

前言 刚开始学react写过类似下面的代码&#xff0c;就是想直接在useEffect中使用async/await。然后浏览器就会报错如下图&#xff1a; useEffect(async () > {const res await Promise.resolve({ code: 200, mes: });}, [])报错的意思&#xff1a; useEffect 期望接受一…

算法导论笔记5:贪心算法

P216 第15章动态规划 最优子结构 具有它可能意味着适合应用贪心策略 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。 剪切-粘贴技术证明 每个子问题的解就是它本身的最优解&#xff08;利用反证法&#xff0…

CCC数字钥匙设计 --数字钥匙数据结构

1、数字钥匙是什么&#xff1f; 汽车数字钥匙&#xff0c;将传统实体钥匙数字化&#xff0c;用卡片、手机等智能设备来做数字钥匙的载体。 从而实现无钥匙进入/启动、为他人远程钥匙授权、个性化的车辆设置等功能。 目前市场上流行的数字钥匙方案是通过NFC、BLE、UWB通信技术…

【数据库开发】DataX开发环境的安装部署

文章目录 1、简介1.1 DataX简介1.2 DataX功能1.3 支持的数据通道 2、DataX安装配置2.1 DataX2.2 Java2.3 Python2.4 测试 3、DataX Web安装配置3.1 mysql3.2 DataX Web3.2.1 简介3.2.2 架构图3.2.3 依赖环境3.2.4 安装 结语 1、简介 DataX是阿里云DataWorks数据集成的开源版本。…

考研分享第2期 | 中央财经大学管理科学跨考北大软微金融科技406分经验分享

一、个人信息 本科院校&#xff1a;中央财经大学 管理科学与工程学院 管理科学专业 上岸院校&#xff1a;北京大学 软件与微电子学院 金融科技专业硕士 考试科目&#xff1a; 初试&#xff1a;思想政治理论 英语一 数学二 经济学综合 面试考察范围广&#xff0c;包括英语自…

深度学习1【吴恩达】

视频链接&#xff1a;1.5 关于这门课_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1FT4y1E74V?p5&spm_id_frompageDriver&vd_source3b6cdacf9e8cb3171856fe2c07acf498 视频中吴恩达老师所有的话语收录&#xff1a; 机器学习初学者-AI入门的宝典 (ai-start.c…

CorelDRAW2023中文免费版矢量图设计软件

设计工作经验丰富的人一定对比过多种设计软件&#xff0c;在对众多矢量图设计软件进行对比之后&#xff0c;多数资深设计师认为CorelDRAW的专业性、便捷性以及兼容性的综合表现更好&#xff0c;而且软件还配置了海量艺术笔&#xff0c;这让工作成果更为出众&#xff0c;因此更愿…

Clickhouse学习笔记(8)—— 建表优化

数据类型 时间字段 建表时能用数值型或日期时间类型&#xff08;DateTime&#xff09;表示的字段就不要用字符串 因为clickhouse进行分区时一般使用时间字段来进行分区&#xff0c;而将时间字段使用DateTime表示&#xff0c;不需要经过函数转换处理&#xff0c;执行效率高、…

[Android]_[初级]_[配置gradle的环境变量设置安装位置]

场景 在开发Android项目的时候, gradle是官方指定的构建工具。不同项目通过wrapper指定不同版本的gradle。随着项目越来越多&#xff0c;使用的gradle版本也增多&#xff0c;导致它以来的各种库也增加&#xff0c;系统盘空间不足&#xff0c;怎么解决&#xff1f; 说明 grad…

.Net-C#文件上传的常用几种方式

1.第一种上传方式,基本通用于.net所有的框架 [HttpPost][Route("Common/uploadFile1")]public string uploads(){HttpContextBase context (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统contextHttpRequestBase request context.Re…

CUMT-----Java课后第六章编程作业

文章目录 一、题11.1 问题描述1.2 代码块1.3 运行截图 二、题22.1 问题描述2.2 代码块2.3 运行截图 一、题1 1.1 问题描述 (1)创建一个用于数学运算接口&#xff0c;算数运算加、减、乘和除都继承该接口并实现具体的算数运算。(2)编写一个测试类进行运行测试。 1.2 代码块 p…

服务器中了locked勒索病毒怎么处理,locked勒索病毒解密,数据恢复

近几年&#xff0c;网络应用技术得到了迅速发展&#xff0c;越来越多的企业开始走向数字化办公&#xff0c;极大地为企业的生产运营提供了帮助&#xff0c;但是网络技术的发展也为网络安全埋下隐患。最近&#xff0c;locked勒索病毒非常嚣张&#xff0c;几乎是每隔两个月就会对…

美团2024届秋招笔试第二场编程真题-小美的数组构造

分析&#xff1a;暴力角度看&#xff0c;因为数组a和b总和一样&#xff0c;所以实际上是将总和m划分为n个数字&#xff0c;且每个数字都和a数组不一样的方案数。当然会超时。从数据角度看&#xff0c;平方级别算法是可以的。 其实用动态规划的四步法分析起来还是很简单的&…

Python实战 | 使用 Python 和 TensorFlow 构建卷积神经网络(CNN)进行人脸识别

专栏集锦&#xff0c;大佬们可以收藏以备不时之需 Spring Cloud实战专栏&#xff1a;https://blog.csdn.net/superdangbo/category_9270827.html Python 实战专栏&#xff1a;https://blog.csdn.net/superdangbo/category_9271194.html Logback 详解专栏&#xff1a;https:/…

EXCEL中将UTC时间戳转为日期格式(精确到秒)

UTC时间戳的格式通常是一个整数&#xff0c;表示从1970年1月1日00:00:00 UTC到当前时间的总秒数。它可以以秒或毫秒为单位表示。例如&#xff0c;如果当前时间是2023年3月17日 12:34:56 UTC&#xff0c;则对应的UTC时间戳为1679839496&#xff08;以秒为单位&#xff09;或1679…

通过防火墙禁止访问指定网站(个人电脑,Windows系统)

背景 近年沉迷B站视频不能自拔&#xff0c;使用了诸多手段禁用&#xff0c;都很容易破戒。为了彻底杜绝B站的使用&#xff0c;决定手动进行设置。在ChatGPT和文心一言提问&#xff0c;得到了以下四种方法&#xff08;按个人认为的戒断水平由低到高排序&#xff09;&#xff1a;…

分享10个地推拉新和网推拉新app推广接单平台,一手接任务平台

文章首推平台&#xff1a;”聚量推客“ 官方邀请码000000 从事地推、拉新、推广这一类型的工作&#xff0c;是一定要有稳定的一手接单平台的&#xff0c;因为在瞬息万变的拉新推广市场中&#xff0c;很多APP应用的推广拉新存在周期性&#xff0c;有可能这个月还在的拉新项目&a…