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

Qt /MFC线程同步机制之互斥锁、 信号量

线程同步机制之互斥锁、 信号量

1 摘要

线程同步是确保多线程程序安全和稳定运行的关键。Qt/MFC提供了多种机制来实现线程同步,包括互斥锁、读写锁、条件变量、信号和槽,以及原子操作。选择合适的同步方法取决于具体的应用场景和需求。理解这些机制并正确使用它们,可以有效避免多线程编程中常见的问题,如数据竞争、死锁等。线程同步是多线程编程中的一个重要概念,主要用于控制多个线程之间的访问,以避免数据竞争和不一致性。本文就线程同步机制中的信号量和互斥锁进行简单的应用介绍。

2 QT 线程之QSemaphore

QSemaphore是Qt中的一个同步机制,用于控制对共享资源的并发访问。它可以用于限制同时访问某个资源的线程数,常用于实现生产者-消费者模型、限制线程池中的活动线程数等场景。

2.1 QSemaphore 的基本概念

1)计数器:QSemaphore内部维护一个计数器,表示可用的资源数量。当计数器大于零时,线程可以获取资源并将计数器减一;当计数器为零时,线程将被阻塞,直到资源被释放。
2)资源管理:通过QSemaphore,可以限制同时访问某个共享资源的线程数量,确保不会超出资源的最大容量。
本文详解QSemaphore 线程同步过程中,有关生产者/消费者线程中同步问题。后续从简单的例程应用到实际的数据采集中。

2.2 QSemaphore 的基本操作:

1)QSemaphore::QSemaphore ( int n = 0 ) : 新建一个信号量,守护的资源数为n(默认为0)
2)QSemaphore::~QSemaphore ():销毁信号量警告:销毁一个正在使用的信号量将导致一个不确定的行为
3)int QSemaphore::available() const :用于返回可用资源的数目
4)void QSemaphore::acquire ( int n = 1 ):从该信号量守护的资源中获取n个资源。如果获取的资源数目大于available()返回值,那么自己将阻塞,直到可获取足够的资源数
5)void QSemaphore::release ( int n = 1 ):释放n个信号量守护的资源给信号量。该函数也可以用来增加一个信号量可使用的资源数目
6)bool QSemaphore::tryAcquire ( int n = 1 ):尝试从信号量守护的资源中获取n个资源。成功,则返回true。如果当前可用的资源数目不够立即返回,返回值为false,并且不占用任何资源
7)bool QSemaphore::tryAcquire ( int n, int timeout ):尝试从信号量守护的资源中获取n个资源。成功,则返回true。如果当前可用的资源数目available()不够,将等待timeout微秒。

2.3 QSemaphore 生产者/消费者线程实现:

这里生产这线程负责产生一个连续的递增数,消费者线程负责打印连续递增数。使用QSemaphore进行数据同步,效果如图
在这里插入图片描述

核心代码.h文件

#ifndef QMYTHREAD_H
#define QMYTHREAD_H

#include    <QThread>
class QThreadDAQ : public QThread
{
    Q_OBJECT

private:
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QThreadDAQ();
    void    stopThread();
};

class QThreadShow : public QThread
{
    Q_OBJECT
private:
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QThreadShow();
    void    stopThread();
signals:
    void    newValue(int *data,int count, int seq);
};
#endif // QMYTHREAD_H

其中QThreadDAQ 为生产者线程,QThreadShow 为消费者线程
核心代码.c文件

#include    "qmythread.h"
#include    <QSemaphore>
//#include    <QTime>

const int BufferSize = 8;
static int buffer1[BufferSize];
static int buffer2[BufferSize];
static int curBuf=1; //当前正在写入的Buffer

static int bufNo=0; //采集的缓冲区序号

static  quint8   counter=0;//数据生成器

static  QSemaphore emptyBufs(2);//信号量:空的缓冲区个数,初始资源个数为2
static  QSemaphore fullBufs; //满的缓冲区个数,初始资源为0

QThreadDAQ::QThreadDAQ()
{

}

void QThreadDAQ::stopThread()
{
    m_stop=true;
}

void QThreadDAQ::run()
{
    m_stop=false;//启动线程时令m_stop=false
    bufNo=0;//缓冲区序号
    curBuf=1; //当前写入使用的缓冲区
    counter=0;//数据生成器

    int n=emptyBufs.available();
    if (n<2)  //保证 线程启动时emptyBufs.available==2
      emptyBufs.release(2-n);

    while(!m_stop)//循环主体
    {
        //emptyBufs.acquire();//获取一个空的缓冲区
       bool acqureret=emptyBufs.tryAcquire(1,10);
       if(acqureret){
        for(int i=0;i<BufferSize;i++) //产生一个缓冲区的数据
        {
            if (curBuf==1)
                buffer1[i]=counter; //向缓冲区写入数据
            else
                buffer2[i]=counter;
            counter++; //模拟数据采集卡产生数据

            msleep(10); //每50ms产生一个数
        }

        bufNo++;//缓冲区序号
        if (curBuf==1) // 切换当前写入缓冲区
          curBuf=2;
        else
          curBuf=1;

        fullBufs.release(); //有了一个满的缓冲区,available==1
    }
         }
    //quit();
}
int bufferData[BufferSize];
void QThreadShow::run()
{
    m_stop=false;//启动线程时令m_stop=false

    int n=fullBufs.available();
    if (n>0)
       fullBufs.acquire(n); //将fullBufs可用资源个数初始化为0

    while(!m_stop)//循环主体
    {
       //fullBufs.acquire(); //等待有缓冲区满,当fullBufs.available==0阻塞
       bool acqureret=fullBufs.tryAcquire(1,10);
       if(acqureret){
        int seq=bufNo;

        if(curBuf==1) //当前在写入的缓冲区是1,那么满的缓冲区是2
            for (int i=0;i<BufferSize;i++)
               bufferData[i]=buffer2[i]; //快速拷贝缓冲区数据
        else
            for (int i=0;i<BufferSize;i++)
               bufferData[i]=buffer1[i];
        emptyBufs.release();//释放一个空缓冲区
        emit    newValue(bufferData,BufferSize,seq);//给主线程传递数据
       }
    }
   
}

QThreadShow::QThreadShow()
{

}

void QThreadShow::stopThread()
{

    m_stop=true;
}


例程中生产者线程生产了两个满buffer用于存储产生的递增数据,消费者线程中生产了两个空buffer。只有当缓存满信号发生时消费者线程才能打印递增数,也只有当缓存空信号发生时生产者线程才能继续产生递增数数据。通过QSemaphore信号量达到线程之间同步,使得生产者生产的数据时连续同时使得消费者拿到的数据也是连续递增的。效果如图
在这里插入图片描述

3 MFC线程之CSemaphore

3.1 CSemaphore同步实现

上面已经介绍了QSemaphore的基本概念和基本操作,在MFC中同样适用,用法也基本差不多,这里不再介绍。同样也讨论递增数生产者/消费者问题。使用Semaphore进行数据同步,效果如图。
在这里插入图片描述

核心代码.h文件

 
// SemaphoreDlg.h : 头文件
//

#pragma once
#include "afxcmn.h"


// CSemaphoreDlg 对话框
class CSemaphoreDlg : public CDialogEx
{
// 构造
public:
	CSemaphoreDlg(CWnd* pParent = NULL);	// 标准构造函数

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_SEMAPHORE_DIALOG };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV 支持


// 实现
protected:
	HICON m_hIcon;

	// 生成的消息映射函数
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	DECLARE_MESSAGE_MAP()
	HANDLE mStartProduceHandle;
	HANDLE mStartCumserHandle;
	HANDLE Produce_fulls;
	HANDLE Cumser_emptys;
	DWORD dwProduceThreadID;
	DWORD dwCumserThreadID;
public:
	bool m_stop;
	LRESULT ShowTextEdit(WPARAM wParam, LPARAM lParam);
	static DWORD WINAPI ProduceTThread(LPVOID lpParam);
	static DWORD WINAPI CumserTThread(LPVOID lpParam);
	afx_msg void OnBnClickedButton2();
	void AddRichEdit1String(CString strTemp, int level);
	CRichEditCtrl m_edit;
	afx_msg void OnBnClickedButton3();
};


核心代码.c文件


// SemaphoreDlg.cpp : 实现文件
//

#include "stdafx.h"
#include "Semaphore.h"
#include "SemaphoreDlg.h"
#include "afxdialogex.h"
#define WM_MESSAGE_SHOW WM_USER + 1000
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
typedef struct {
	void* p;
	bool m_stop;
} ThreadPara;
static ThreadPara ThreadProducePara;
static ThreadPara ThreadCumserPara;
const int BufferSize = 8;
static int buffer1[BufferSize];
static int buffer2[BufferSize];
static int curBuf = 1;   //当前正在写入的Buffer
static int counter = 0;  //数据生成器
static int bufNo = 0;    //缓冲区序号
// 用于应用程序“关于”菜单项的 CAboutDlg 对话框

class CAboutDlg : public CDialogEx
{
public:
	CAboutDlg();

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_ABOUTBOX };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持

// 实现
protected:
	DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()


// CSemaphoreDlg 对话框



CSemaphoreDlg::CSemaphoreDlg(CWnd* pParent /*=NULL*/)
	: CDialogEx(IDD_SEMAPHORE_DIALOG, pParent)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CSemaphoreDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_RICHEDIT21, m_edit);
}

BEGIN_MESSAGE_MAP(CSemaphoreDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_MESSAGE(WM_MESSAGE_SHOW, ShowTextEdit)
	ON_BN_CLICKED(IDC_BUTTON2, &CSemaphoreDlg::OnBnClickedButton2)
	ON_BN_CLICKED(IDC_BUTTON3, &CSemaphoreDlg::OnBnClickedButton3)
END_MESSAGE_MAP()


// CSemaphoreDlg 消息处理程序

BOOL CSemaphoreDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 将“关于...”菜单项添加到系统菜单中。

	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO: 在此添加额外的初始化代码

	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

void CSemaphoreDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
		CAboutDlg dlgAbout;
		dlgAbout.DoModal();
	}
	else
	{
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

// 如果向对话框添加最小化按钮,则需要下面的代码
//  来绘制该图标。  对于使用文档/视图模型的 MFC 应用程序,
//  这将由框架自动完成。

void CSemaphoreDlg::OnPaint()
{
	if (IsIconic())
	{
		CPaintDC dc(this); // 用于绘制的设备上下文

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// 使图标在工作区矩形中居中
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// 绘制图标
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
		CDialogEx::OnPaint();
	}
}

//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CSemaphoreDlg::OnQueryDragIcon()
{
	return static_cast<HCURSOR>(m_hIcon);
}
void CSemaphoreDlg::AddRichEdit1String(CString strTemp, int level)
{
	COLORREF color;
	m_edit.LimitText(60000);
	CTime timeTemp = CTime::GetCurrentTime();
	CString strInfo;
	strInfo.Format(_T("%02d:%02d:%02d: %s\n"), timeTemp.GetHour(), timeTemp.GetMinute(), timeTemp.GetSecond(), strTemp);
	CString m_filename;
	SYSTEMTIME t_systime;
	GetLocalTime(&t_systime);
	unsigned int m_sysYears = t_systime.wYear;
	unsigned int m_sysMonths = t_systime.wMonth;
	unsigned int m_sysDays = t_systime.wDay;
	unsigned int m_sysHours = t_systime.wHour;
	unsigned int m_sysMinutes = t_systime.wMinute;
	unsigned int m_sysSeconds = t_systime.wSecond;
	unsigned int m_sysminSeconds = t_systime.wMilliseconds;
	if (level == 1)//红色
	{
		color = RGB(250, 0, 0);
	}
	else if (level == 0)//黑色  默认是黑色
	{
		color = RGB(0, 0, 0);
	}
	else if (level == 2)
	{
		color = RGB(0, 139, 0);
	}
	else if (level == 3)
	{
		color = RGB(0, 0, 255);
	}

	int length = m_edit.GetLimitText();
	int nLength = (int)m_edit.SendMessage(WM_GETTEXTLENGTH);
	//自动清除
	if (length<(nLength + strInfo.GetLength()))
	{
		m_edit.SetSel(0, -1);
		m_edit.SetReadOnly(FALSE);
		m_edit.Clear();
		m_edit.SetReadOnly(TRUE);
	}
	m_edit.SetSel(-1, -1);
	CHARFORMAT cf;
	ZeroMemory(&cf, sizeof(CHARFORMAT));
	cf.cbSize = sizeof(CHARFORMAT);
	cf.dwMask = CFM_BOLD | CFM_COLOR | CFM_FACE |
		CFM_ITALIC | CFM_SIZE | CFM_UNDERLINE;
	cf.dwEffects = 0;
	cf.yHeight = 16 * 8;//文字高度
	cf.crTextColor = color; //文字颜色
	m_edit.SetSelectionCharFormat(cf);
	m_edit.ReplaceSel(strInfo);
}

LRESULT CSemaphoreDlg::ShowTextEdit(WPARAM wParam, LPARAM lParam)  
{
	int *data_Buf = (int *)wParam;
	int size = lParam;
	CString strTemp=_T("");
	CString temp = _T("");

	for (int i = 0; i < size; i++)
	{
		temp.Format(_T("%d,"), data_Buf[i]);
		strTemp += temp;
	}
	strTemp += "\r";
	AddRichEdit1String(strTemp, 2);
	return 1;
}

DWORD WINAPI CSemaphoreDlg::ProduceTThread(LPVOID lpParam)
{
	ThreadPara* pPara = (ThreadPara*)lpParam;
	CSemaphoreDlg *pDlg = (CSemaphoreDlg *)pPara->p;
	bufNo = 0;
	bufNo = 0;    //缓冲区序号
	curBuf = 1;   //当前写入使用的缓冲区
	counter = 0;  //数据生成器
	while (!pPara->m_stop)
	{
		WaitForSingleObject(pDlg->Cumser_emptys, INFINITE);
		for (int i = 0; i<BufferSize; i++)      //产生一个缓冲区的数据
		{
			if (curBuf == 1)
				buffer1[i] = counter;         //向缓冲区写入数据
			else
				buffer2[i] = counter;
			counter++;                       //模拟数据采集卡产生数据
			Sleep(100);                     //每50ms产生一个数
		}

		bufNo++;//缓冲区序号
		if (curBuf == 1) // 切换当前写入缓冲区
			curBuf = 2;
		else
			curBuf = 1;
		ReleaseSemaphore(pDlg->Produce_fulls, 1, NULL);
	}
	return 1;
}
DWORD WINAPI CSemaphoreDlg::CumserTThread(LPVOID lpParam)
{
	ThreadPara* pPara = (ThreadPara*)lpParam;
	CSemaphoreDlg *pDlg = (CSemaphoreDlg *)pPara->p;
	unsigned int para_send = BufferSize;
	while (!pPara->m_stop)
	{
		WaitForSingleObject(pDlg->Produce_fulls, INFINITE);
		int bufferData[BufferSize];
		int seq = bufNo;

		if (curBuf == 1) //当前在写入的缓冲区是1,那么满的缓冲区是2
			for (int i = 0; i<BufferSize; i++)
				bufferData[i] = buffer2[i]; //快速拷贝缓冲区数据
		else
			for (int i = 0; i<BufferSize; i++)
				bufferData[i] = buffer1[i];
		ReleaseSemaphore(pDlg->Cumser_emptys, 1, NULL);
		::PostMessage(pDlg->GetSafeHwnd(), WM_MESSAGE_SHOW, (WPARAM)(bufferData), (LPARAM)para_send);
	}
	return 1;
}

void CSemaphoreDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	Produce_fulls = CreateSemaphore(NULL, 0, 2, _T("BufferFulls"));
	Cumser_emptys = CreateSemaphore(NULL, 1, 2, _T("emptyFulls"));
	ThreadProducePara.m_stop = 0;
	ThreadProducePara.p = (void *)this;
	dwProduceThreadID = 1;
	mStartProduceHandle = CreateThread(NULL, 0, ProduceTThread, &ThreadProducePara, 0, &dwProduceThreadID);
	CloseHandle(mStartProduceHandle);
	mStartProduceHandle = NULL;

	ThreadCumserPara.m_stop = 0;
	ThreadCumserPara.p = (void *)this;
	dwCumserThreadID = 1;
	mStartCumserHandle = CreateThread(NULL, 0, CumserTThread, &ThreadCumserPara, 0, &dwCumserThreadID);
	CloseHandle(mStartCumserHandle);
	mStartCumserHandle = NULL;
}


void CSemaphoreDlg::OnBnClickedButton3()
{
	// TODO: 在此添加控件通知处理程序代码
	ThreadProducePara.m_stop = 1;
	ThreadCumserPara.m_stop = 1;
}

mStartProduceHandle = CreateThread(NULL, 0, ProduceTThread, &ThreadProducePara, 0, &dwProduceThreadID)建立生产者线程
mStartCumserHandle = CreateThread(NULL, 0, CumserTThread, &ThreadCumserPara, 0, &dwCumserThreadID)建立消费者线程
Produce_fulls = CreateSemaphore(NULL, 0, 2, _T(“BufferFulls”))初始化满信号量数量为2
Cumser_emptys = CreateSemaphore(NULL, 1, 2, _T(“emptyFulls”))初始化空信号号量数量为2

3.2 CSemaphore同步验证

同样通过Semaphore信号量验证数据是否连续递增打印,生产者线程不断产生连续递增数,消费者取出数据不断打印连续数据,效果如图,在MFC中使用CSemaphore同样实现了两个线程之间数据同步问题。
在这里插入图片描述

4 互斥锁(Mutex)

4.1 Qt多线程之QMutex

Mutex是Qt中用于保护共享数据的基本同步机制。它确保同一时间只有一个线程可以访问特定的代码段或数据,其他线程处于等待状态直到锁释放。使用锁比较简单,示例代码如下,篇幅问题这里不在仔细讨论。

#include <QReadWriteLock>
QReadWriteLock rwLock;
void readFunction() {
    rwLock.lockForRead(); // 读锁
    // 读取共享数据
    rwLock.unlock(); // 解锁
}

void writeFunction() {
    rwLock.lockForWrite(); // 写锁
    // 写入共享数据
    rwLock.unlock(); // 解锁
}

4.2 MFC多线程之Mutex

在这里简单展示火车票问题,理解了信号量之后其实Mutex的东西也是如出一辙。

#include "windows.h"
#include <iostream.h>//线程头文件
 
DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
int index=0;
int ticket=100;
HANDLE hMutex;//一个互斥量
void main()
{
    HANDLE hThread1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
    HANDLE hThread2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    hMutex=CreateMutex(NULL,FALSE,NULL);
    Sleep(4000);
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{

    while(TRUE)
    {
        WaitForSingleObject(hMutex,INFINITE);//等待互斥量的信号
        if(ticket>0)
        {
            Sleep(1);
            cout<<"Thread1 sell  ticket"<<ticket--<<endl;
        }
        else
            break;
        ReleaseMutex(hMutex);
    }
    return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
    
    while(TRUE)
    {
        
        WaitForSingleObject(hMutex,INFINITE);
        if(ticket>0)
        {
            Sleep(1);
            cout<<"Thread2 sell  ticket"<<ticket--<<endl;
        }
        else
            break;
        ReleaseMutex(hMutex);//释放
    }
    return 0;
}

5 对比

(1)Qt多线程同步:
Qt实现线程同步,主要包括:
互斥锁(QMutex)
读写锁(QReadWriteLock)
信号量(QSemaphore)
等待条件(QWaitCondition)。
(2)MFC多线程同步:
MFC实现线程同步,主要包括:
临界区(Critical Section)
互斥量(Mutex)
事件(Event)
信号量(Semaphore)

6 总结

本文介绍了Qt/MFC中的多线程进阶同步应用,介绍了Qt/MFC共同多线程同步方法互斥锁(QMutex/Mutex)、信号量(QSemaphore/Semaphore)通过经典的生产者和消费者问题,充分理解多线程的同步方法,通过总结理解同步机制,可为高速数据采集,数据库多用户设计等应用场景提供同步方案。后续则继续总结分享Qt应用开发中的其他应用,开发不易珍惜每一分原创和劳动成果,同时注意平时开发过程中的经验积累总结。


http://www.kler.cn/a/560708.html

相关文章:

  • SpringBoot之自定义简单的注解和AOP
  • Ubuntu22.04 - etcd的安装和使用
  • [Android]APP自启动
  • Linux-Ansible基础模块
  • 监控与告警系统Prometheus
  • 一.Vue中的条件渲染
  • ELK之elasticsearch基本使用教程
  • 鸿蒙NEXT开发-文件服务上传下载
  • GitHub免密操作与跨服务器通行:SSH密钥一站式配置指南
  • 从零开始玩转TensorFlow:小明的机器学习故事 5
  • 再论Spring MVC中Filter和HandlerInterceptor的优先级
  • 工具方法 - 合规性矩阵
  • 登录-10.Filter-登录校验过滤器
  • 【Python爬虫(64)】从“听”开始:Python音频爬虫与语音数据处理全解析
  • 微信小程序——访问服务器媒体文件的实现步骤
  • 考研/保研复试英语问答题库(华工建院)
  • 网络安全-Mysql注入知识点
  • java基础面试-Java 内存模型(JMM)相关介绍
  • 《深度剖析Linux 系统 Shell 核心用法与原理_666》
  • [AI相关]问问DeepSeek如何基于Python,moviePy实现视频字幕功能