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

Linux C/C++编程——线程

线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度。
系统调度的最小单元是线程、而并非进程。
线程包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
同一进程中的各个线程,可以共享该进程所拥有的资源。
线程和子进程
子进程的缺点:
  • 进程间切换开销大。
  • 进程间通信较为麻烦。

多线程的有点:

  •  同一进程的多个线程间切换开销比较小。
  • 同一进程的多个线程间通信容易。
其它的概念,例如:并发并行什么的,具体查阅操作系统。

一、线程 

1. 创建线程

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

函数参数和返回值含义如下:

        thread: pthread_t 类型指针,当 pthread_create() 成功返回时,新创建的线程的线程 ID 会保存在参数 thread 所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
        attr: pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区,pthread_attr_t 数据类型定义了线程的各种属性。如果将参数 attr 设置为 NULL,那么表示将线程的所有属性设置为默认值,以此创建新线程。
        start_routine: 参数 start_routine 是一个函数指针,指向一个函数,新创建的线程从 start_routine()函数开始运行,该函数返回值类型为 void * ,并且该函数的参数只有一个 void * ,其实这个参数就是pthread_create() 函数的第四个参数 arg
        arg: 传递给 start_routine() 函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量。将参数 arg 设置为 NULL ,表示不需要传入参数给 start_routine() 函数。

案例1:pthread_create() 创建线程

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());return (void *)0;
}
int main(void)
{pthread_t tid;int ret;ret = pthread_create(&tid, NULL, new_thread_start, NULL);if (ret) {fprintf(stderr, "Error: %s\n", strerror(ret));exit(-1);}printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());sleep(1);exit(0);
}

编译的时候,需要加上-lpthread。因为 pthread 不在 gcc 的默认链接库中。

gcc -o pthread_create pthread_create.c -lpthread

2. 终止线程

案例2 pthread_exit() 终止线程

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>static void *new_thread_start(void *arg)
{printf("新线程 start\n");sleep(1);printf("新线程 end\n");/* 终止线程 */pthread_exit(NULL);
}int main(void)
{pthread_t tid;int ret;ret = pthread_create(&tid, NULL, new_thread_start, NULL);if (ret){fprintf(stderr, "Error: %s\n", strerror(ret));exit(-1);}printf("主线程 end\n");pthread_exit(NULL);exit(0);
}

最三行的输出,会延迟一段时间输出。

就算主线程调用终止,整个进程也不会结束,即新线程还在继续运行。

3. 回收线程

在父、子进程当中,父进程可通过 wait() 函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;
而在线程当中,也需要如此,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
        thread: pthread_join() 等待指定线程的终止,通过参数 thread (线程 ID )指定需要等待的线程;
        retval: 如果参数 retval 不为 NULL ,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit() 退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到 *retval 所指向的内存区域;
        如果目标线程被 pthread_cancel() 取消,则将 PTHREAD_CANCELED 放在 *retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL
返回值: 成功返回 0 ;失败将返回错误码。
案例3: pthread_join()等待线程终止
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{printf("新线程 start\n");sleep(2);printf("新线程 end\n");pthread_exit((void *)10);
}
int main(void)
{pthread_t tid;void *tret;int ret;ret = pthread_create(&tid, NULL, new_thread_start, NULL);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}ret = pthread_join(tid, &tret);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}printf("新线程终止, code=%ld\n", (long)tret);exit(0);
}

4. 取消线程

案例4:pthread_cancel()取消线程

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>static void *new_thread_start(void *arg)
{printf("新线程--running\n");for ( ; ; )sleep(1);return (void *)0;
}int main(void)
{pthread_t tid;void *tret;int ret;/* 创建新线程 */ret = pthread_create(&tid, NULL, new_thread_start, NULL);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}sleep(1);/* 向新线程发送取消请求 */ret = pthread_cancel(tid);if (ret) {fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));exit(-1);}/* 等待新线程终止 */ret = pthread_join(tid, &tret);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}printf("新线程终止, code=%ld\n", (long)tret);exit(0);
}
当主线程发送取消请求之后,新线程便退出了,而且退出码为 -1,也就是PTHREAD_CANCELED

二、线程同步

线程同步是为了对共享资源的访问进行保护。
案例1  两个线程并发访问同一全局变量
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>static int g_count = 0;
static void *new_thread_start(void *arg)
{int loops = *((int *)arg);int l_count, j;for (j=0; j<loops; j++){l_count = g_count;l_count++;g_count = l_count;}return (void *)0;
}static int loops;
int main(int argc, char *argv[])
{pthread_t tid1, tid2;int ret;/* 获取用户传递的参数 */if(2>argc)loops =10000000;elseloops = atoi(argv[1]);/* 创建 2 个新线程*/ret = pthread_create(&tid1, NULL, new_thread_start, &loops);if(ret){fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}ret = pthread_create(&tid2, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}/* 等待线程结束 */ret = pthread_join(tid1, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}ret = pthread_join(tid2, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}/* 打印结果 */printf("g_count = %d\n", g_count);exit(0);}

1. 互斥锁

所谓的信号量、互斥锁什么的,考完408后,都十分熟悉,但是都停留在伪代码阶段。

1.1 初始化互斥锁

宏初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init() 函数初始化互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
        mutex: 参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;
        attr: 参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性, 若将参数 attr 设置为 NULL ,则表示将互斥锁的属性设置为默认值。

1.2 互斥锁加锁和解锁

调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互斥锁,
而调用函数 pthread_mutex_unlock() 可以对互斥锁解锁、释放互斥锁。
案例2: 使用互斥锁保护全局变量的访问
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>static pthread_mutex_t mutex;
static int g_count=0;static void *new_thread_start(void *arg)
{int loops = *((int *)arg);int l_count, j;for(j = 0;j<loops;j++){/*上锁*/pthread_mutex_lock(&mutex);l_count = g_count;l_count++;g_count = l_count;/*解锁*/pthread_mutex_unlock(&mutex);}return (void *)0;
}static int loops;
int main(int argc, char *argv[])
{pthread_t tid1, tid2;int ret;/* 获取用户传递的参数 */if (2 > argc)loops = 10000000; //没有传递参数默认为 1000 万次elseloops = atoi(argv[1]);/* 初始化互斥锁 */pthread_mutex_init(&mutex, NULL);/* 创建 2 个新线程 */ret = pthread_create(&tid1, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}ret = pthread_create(&tid2, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}/* 等待线程结束 */ret = pthread_join(tid1, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}ret = pthread_join(tid2, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}/* 打印结果 */printf("g_count = %d\n", g_count);exit(0);
}

1.3 非阻塞加锁

当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁。
调用 pthread_mutex_trylock()加锁失败,不会阻塞。
案例3:非阻塞加锁
 while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁l_count = g_count;l_count++;g_count = l_count;pthread_mutex_unlock(&mutex);//互斥锁解锁

1.4 销毁互斥锁

/* 销毁互斥锁 */pthread_mutex_destroy(&mutex)

1.5 死锁

// 线程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 线程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);

A锁1,B锁2,导致A要锁2的时候阻塞,B要锁1的时候阻塞。

1.6 互斥锁的属性

互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 中类型:
  • PTHREAD_MUTEX_NORMAL 一种标准的互斥锁类型,不做任何的错误检查或死锁检测。
  • PTHREAD_MUTEX_ERRORCHECK 此类互斥锁会提供错误检查。
  • PTHREAD_MUTEX_RECURSIVE 此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁。
  • PTHREAD_MUTEX_DEFAULT 此类互斥锁提供默认的行为和特性 。
使用方式:
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
/* 初始化互斥锁属性对象 */
pthread_mutexattr_init(&attr);
/* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, &attr);
......
/* 使用完之后 */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);

2. 条件变量

几种模式不赘述了,考过408的都懂,详细看操作系统。

案例4:无条件变量,生产者---消费者

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_avail = 0;/* 消费者 线程*/
static void *consumer_thread(void *arg)
{for ( ; ; ){pthread_mutex_lock(&mutex);while (g_avail > 0){g_avail--;  //消费pthread_mutex_unlock(&mutex);}return (void *)0;}
}/* 主线程(生产者)*/
int main(int argc, char *argv[])
{pthread_t tid;int ret;/*初始化互斥锁*/pthread_mutex_init(&mutex,  NULL);/* 创建新线程*/ret = pthread_create(&tid, NULL, consumer_thread, NULL);if(ret){fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}for( ; ; ){pthread_mutex_lock(&mutex);g_avail++;  //生产者pthread_mutex_unlock(&mutex);}exit(0);
}
上述代码虽然可行,但由于新线程中会不停的循环检查全局变量 g_avail 是否大于 0 ,故而造成 CPU 资源的浪费。

2.1 条件变量初始化

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

2.2 条件变量的通知和等待

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

案例5:使用条件变量,生产者---消费者

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源
/* 消费者线程 */
static void *consumer_thread(void *arg)
{for ( ; ; ) {pthread_mutex_lock(&mutex);//上锁while (0 >= g_avail)pthread_cond_wait(&cond, &mutex);//等待条件满足while (0 < g_avail)g_avail--; //消费pthread_mutex_unlock(&mutex);//解锁}return (void *)0;
}
/* 主线程(生产者) */
int main(int argc, char *argv[])
{pthread_t tid;int ret;/* 初始化互斥锁和条件变量 */pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);/* 创建新线程 */ret = pthread_create(&tid, NULL, consumer_thread, NULL);
if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}for ( ; ; ) {pthread_mutex_lock(&mutex);//上锁g_avail++; //生产pthread_mutex_unlock(&mutex);//解锁pthread_cond_signal(&cond);//向条件变量发送信号}exit(0);
}

三、Qt 的多线程

1. 多线程实现

Qt 有 QThread 类,下面是案例。

案例1:多线程打印数字

1. 声明控件

//threaddlg.hclass ThreadDlg : public QDialog
{Q_OBJECTpublic:ThreadDlg(QWidget *parent = 0);~ThreadDlg();
private:QPushButton *startBtn;QPushButton *stopBtn;QPushButton *quitBtn;
}
//threaddlg.cppThreadDlg::ThreadDlg(QWidget *parent): QDialog(parent)
{setWindowTitle(tr("线程"));startBtn = new QPushButton(tr("开始"));stopBtn = new QPushButton(tr("停止"));quitBtn = new QPushButton(tr("退出"));QHBoxLayout *mainLayout = new QHBoxLayout(this);mainLayout->addWidget(startBtn);mainLayout->addWidget(stopBtn);mainLayout->addWidget(quitBtn);
}

2. 打印功能

//workthread.h#include <QThread>
class WorkThread : public QThread
{Q_OBJECT
public:WorkThread();
protected:void run();
};

 WorkThread 类继承自 QThread 类,run()函数需要重新实现。

//workthread.cpp#include "workthread.h"
#include <QtDebug>
WorkThread::WorkThread()
{}
void WorkThread::run()
{while(true){for(int n=0;n<10;n++)qDebug()<<n<<n<<n<<n<<n<<n<<n<<n;}
}

run() 函数:不断打印0~9数字,且每一个数字重复8次。

3. 槽函数

WorkThread

//threaddlg.h
private:WorkThread *workThread[MAXSIZE];
public slots:void slotStart();						//槽函数用于启动线程void slotStop();						//槽函数用于终止线程

指向工作线程的私有指针数组 workThread,记录了所启动的全部线程。 

连接槽函数:

//threaddlg.cppconnect(startBtn,SIGNAL(clicked()),this,SLOT(slotStart()));connect(stopBtn,SIGNAL(clicked()),this,SLOT(slotStop()));connect(quitBtn,SIGNAL(clicked()),this,SLOT(close()));

 “开始”槽函数的实现:

//threaddlg.cppvoid ThreadDlg::slotStart()
{for(int i=0;i<MAXSIZE;i++){workThread[i]=new WorkThread();	//(a)}for(int i=0;i<MAXSIZE;i++){workThread[i]->start();			//(b)}startBtn->setEnabled(false);stopBtn->setEnabled(true);
}

(a)创建指定数目的线程

(b)调用 QThread 基类的 start() 函数,此函数将启动 run() 函数。从此线程开始运行。

void ThreadDlg::slotStop()
{for(int i=0;i<MAXSIZE;i++){workThread[i]->terminate();workThread[i]->wait();}startBtn->setEnabled(true);stopBtn->setEnabled(false);
}

“停止”槽函数实现:

void ThreadDlg::slotStop()
{for(int i=0;i<MAXSIZE;i++){workThread[i]->terminate();    //aworkThread[i]->wait();         //b}startBtn->setEnabled(true);stopBtn->setEnabled(false);
}

(a)调用 QThread 基类的 terminate() 函数,依次终止数组中的 WorkThread 类实例。

(b)调用 QThread 基类的 wait() 函数,使得线程阻塞等待直到退出或超时。

4. 测试

//threaddlg.h#define MAXSIZE 1

线程数为1的时候:

但是改成线程数为5的时候,依然是这个结果。。。

2. 多线程控制

实现线程的互斥与同步常用类有:

QMutex、QMutexLocker、QReadWriteLocker、QReadLocker、QWriteLocker、QSemaphore 和 QWaitCondition。

class Key
{
public:key()   {key=0;}int createKey() {++key; return key;}int value()const {return key;}
private:int key;
}

上面代码中,每创建一个Key类,key的值就会递增,也就是不重复的Key类。

但是在多线程环境下,这个类是不安全的,因为存在多个线程同时修改成员变量 key 的值。

在 C++ 中,“++”操作符不是原子操作。编译后,展开为以下3条命令

1)将变量值载入寄存器

2)将寄存器中的值加1

3)将寄存器中的值写回主存

设当前 key=0,如果 pth1 和 pth2 同时将0载入到寄存器,执行加1操作后写回主存。此时key=1,只是进行了一次加1操作。

2.1 QMutex 类

class Key
{
public:key()   {key=0;}int createKey() { mutex.lock(); ++key; return key; mutex.unlock();}int value()const { mutex.lock(); return key; mutex.unlock();}
private:int key;QMutex mutex;
}

在自加的命令进行互斥锁的加解锁。QMutex 类具有 lock()、unlock() 以及 tryLock() 函数等。

但是上面的代码,是有问题的。unlock() 操作不得已在 return 之后(两个函数都有 return key的操作),这样会导致 unlock() 操作永远无法执行。

2.2 QMutexLocker 类

简化了互斥量的处理。

class Key
{
public:key()   {key=0;}int createKey() { QmutexLocker locker(&mutex); ++key; return key;}int value()const { QmutexLocker locker(&mutex); return key; mutex.unlock();}
private:int key;QMutex mutex;
}

在构造函数中接收一个 QMutex 对象作为参数并将其锁定,在析构函数中解锁这个互斥量,这样就不会有之前的问题。

2.3 信号量

又要碰到生产者——消费者模型了。这边是利用信号量的方式:

//main.cpp#include <QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <stdio.h>const int DataSize=1000;
const int BufferSize=80;
int buffer[BufferSize];    //a
QSemaphore freeBytes(BufferSize);    //b
QSemaphore usedBytes(0);    //c

(a)生产者向 buffer 写入数据,直到最大。然后从起点重新写入,也就覆盖已经存在的数据。消费者读取 buffer 的数据,每个 int 字长都被看成一个资源,当然会更改单位。

(b) freeBytes 信号量控制可被生产者填充的缓冲区部分。

(c)usedBytes 信号量控制可被消费者读取的缓冲区部分。

 Producer 类继承自 QThread 类,作为生产者。

//main.cppclass Producer : public QThread
{
public:Producer();void run();
};

生产者的构造函数,以及 run() 函数:

Producer::Producer()
{
}void Producer::run()
{for(int i=0;i<DataSize;i++){freeBytes.acquire();    //abuffer[i%BufferSize]=(i%BufferSize);    //busedBytes.release();    //c}
}

(a)生产者线程首先获取一个空闲单元,如果消费者占用整个缓冲区,那么该函数的调用会被阻塞,直到消费者读取数据为止。当然可以用 tryAcquire(n) 函数。

(b)一旦生产者获取了空闲单元,就使用氮气的缓冲区单元序号填充这个缓冲区单元。

(c)调用该函数,将可用资源+1.也就是生产者生产了数据,且消费者可以读取。

消费者就不赘述了:

class Consumer : public QThread
{
public:Consumer();void run();
};Consumer::Consumer()
{
}void Consumer::run()
{for(int i=0;i<DataSize;i++){usedBytes.acquire();fprintf(stderr,"%d",buffer[i%BufferSize]);if(i%16==0&&i!=0)fprintf(stderr,"\n");freeBytes.release();}fprintf(stderr,"\n");
}
int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);Producer producer;Consumer consumer;producer.start();consumer.start();producer.wait();consumer.wait();return a.exec();
}

2.4 等待与唤醒

生产者——消费者还有一个方式:使用 QWaitCondition 类。其实有点像理发师模型?

QWaitCondition bufferEmpty;
QWaitCondition bufferFull;
QMutex mutex;								//(a)
int numUsedBytes=0;							//(b)
int rIndex=0;								//(c)

 (a)使用互斥量保证对线程操作的原子性

(b)numUsedBytes 表示存在多少“可用字节”

(c)rIndex 用于表示当前所读取缓冲区的位置

Producer::Producer()
{
}
void Producer::run()
{for(int i=0;i<DataSize;i++)				//(a){mutex.lock();if(numUsedBytes==BufferSize)			//(b)bufferEmpty.wait(&mutex);			//(c)buffer[i%BufferSize]=numUsedBytes;	//(d)++numUsedBytes;						//增加numUsedBytes变量bufferFull.wakeAll();				//(e)mutex.unlock();}
}

(a)for 循环中的所有语句都要进行保护

(b)检查缓冲区是否被填满

(c)如果被填满了,则等待 缓冲区有位置(等待消费者消费)。

(d)如果没有被填满,则想缓冲区写入一个数值。

(e)最后唤醒等待,也就是跟别人说有数据可以获取

消费者就不赘述了:

void Consumer::run()
{forever{mutex.lock();if(numUsedBytes==0)bufferFull.wait(&mutex);			printf("%ul::[%d]=%d\n",currentThreadId(),rIndex,buffer[rIndex]);rIndex=(++rIndex)%BufferSize;			--numUsedBytes;							bufferEmpty.wakeAll();					mutex.unlock();}printf("\n");
}

相关文章:

Linux C/C++编程——线程

线程是允许应用程序并发执行多个任务的一种机制&#xff0c;线程参与系统调度。 系统调度的最小单元是线程、而并非进程。 线程包含在进程之中&#xff0c;是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流&#xff08;或者说是执行路线、执行流&#xff09;…...

【Spring Boot 中 `@Value` 注解的使用】

文章目录 一、前言二、Value 注解简介三、Value 注解的常见用法1. 读取 application.properties 或 application.yml 配置值&#xff08;1&#xff09;配置文件示例&#xff08;2&#xff09;Java 代码示例&#xff08;3&#xff09;测试输出 2. 使用 Value 设置默认值3. 读取系…...

【CAD二次开发】调试无法进入断点提示无可用源问题(非空心断点)

问题截图&#xff1a;显示无可用源&#xff0c;关闭后F5走完后&#xff0c;启动的调试就中断了 操作是&#xff1a;打开Cad&#xff0c;打开dwg后&#xff0c;执行命令&#xff0c;就出现以上截图问题。 问题来源&#xff1a;通常是由于 AutoCAD 的 纤程模式&#xff08;Fiber&…...

Ubuntu下Docker部署Misskey:打造你的去中心化社交平台

引言 在信息爆炸的时代&#xff0c;人们对于社交平台的需求日益增长&#xff0c;同时也更加注重数据的隐私和自由。Misskey作为一个开源的去中心化社交平台&#xff0c;为用户提供了一个全新的选择。本文将详细介绍如何在Ubuntu Linux环境下&#xff0c;利用Docker快速部署Mis…...

【Vue3】01-vue3的基础 + ref reactive

首先确保已经有了ES6的基础 本文介绍 vue 的基础使用以及 两种响应数据的方式。 目录 1. 创建一个vue应用程序 2. Vue模块化开发 3. ref 和 reactive 的区别 1. 创建一个vue应用程序 所需的两个文件&#xff1a; https://unpkg.com/vue3/dist/vue.global.js https://un…...

C++实现rabbitmq生产者消费者

RabbitMQ是一个开源的消息队列系统&#xff0c;它实现了高级消息队列协议&#xff08;AMQP&#xff09;&#xff0c; 特点 可靠性&#xff1a;通过持久化、镜像队列等机制保证消息不丢失&#xff0c;确保消息可靠传递。灵活的路由&#xff1a;提供多种路由方式&#xff0c;如…...

Simple-BEV的bilinear_sample 作为view_transformer的解析,核心是3D-2D关联点生成

文件路径models/view_transformers 父类 是class BiLinearSample(nn.Module)基于https://github.com/aharley/simple_bev。 函数解析 函数bev_coord_to_feature_coord的功能 将鸟瞰图3D坐标通过多相机&#xff08;针孔/鱼眼&#xff09;内外参投影到图像特征平面&#xff0…...

Rust嵌入式开发环境搭建指南(基于Stm32+Vscode)

Rust嵌入式开发环境搭建指南(基于Stm32+Vscode) 部分目录如下所示: 目录 简介Rust开发环境安装STM32开发工具链安装VSCode环境配置VSCode插件安装调试器配置项目创建与配置常见问题与解决方案简介 本文档旨在指导开发者如何搭建基于Rust语言的STM32嵌入式开发环境。相比传…...

springboot操作redis集群,注意事项

整合redis可查看博文 springboot 整合redis_springboot整合redis csdn-CSDN博客 集群中操作注意事项 1 多键操作失败&#xff1a; 当使用multiGet等需要同时访问多个键的方法时&#xff0c;如果没有使用Hash Tags&#xff0c;这些键可能会被分配到不同的槽中。如果这些槽位于…...

计算机技术系列博客——目录页(持续更新)

1.1 博客目录专栏 1.1.1 博客文章导航 计算机技术系列博客——目录页 1.1.2 网页资源整理 2.1 计算机科学理论 2.2 软件工程技术 2.2.1.1 编程语言 Java Java语言基础 (1) Java基础知识总结01——Java基础篇 (2) Java基础知识总结02——集合框架篇 (3) Java基础知识总结03—…...

@maptalks/gl-layers中的VectorTileLayer的setStyle属性的全部line配置

maptalks/gl-layers中的VectorTileLayer的setStyle属性的全部line配置 关于 maptalks/gl-layers 中 VectorTileLayer 的 setStyle 方法 在 maptalks/gl-layers 库中&#xff0c;VectorTileLayer 提供了一个灵活的方式来设置矢量瓦片图层的样式。通过调用 setStyle 方法&#xf…...

sql小记,20250319

ps:基于sqlserver 一、绩效管理系统表设计 1.表设计 Users用户表&#xff1a;包含id&#xff0c;用户名&#xff0c;密码。 AppraisalBases评价(职位基数)表&#xff1a;包含职位id&#xff0c;职位年终奖基数 AppraisalCoeffcients评价系数表&#xff1a;包含类别id, 类别&…...

【亚马逊云科技】大模型选型实战(挑选和测评对比最适合业务的大模型)

文章目录 前言1、实验内容2、手册内容 一、环境准备二、Prompt 实战与模型配置2.1 基于 Amazon Bedrock 对比测试不同模型的逻辑推理效果2.2 基于 Amazon Bedrock 对比测试不同模型知识问答能力2.3 Prompt 实战结果分析 三、基于 Amazon Bedrock Evaluations 进行模型评测与自动…...

Linux 用户与组管理实战:经验分享与最佳实践

在 Linux 系统管理中&#xff0c;用户和组的管理是保障系统安全和资源分配的重要环节。本文将深入介绍如何创建和管理用户与组&#xff0c;包括 UID、GID 的设置&#xff0c;主组与附加组的分配&#xff0c;以及常见问题的排查和解决。本文还结合实际操作经验&#xff0c;总结了…...

详解如何通过Python的BeautifulSoup爬虫+NLP标签提取+Dijkstra规划路径和KMeans聚类分析帮助用户规划旅行路线

系统模块&#xff1a; 数据采集模块&#xff08;爬虫&#xff09;&#xff1a;负责从目标网站抓取地点数据&#xff08;如名称、经纬度、描述等&#xff09; 数据预处理模块&#xff08;标签算法&#xff09;&#xff1a;对抓取到的地点数据进行清洗和分类。根据地点特征&…...

Java Stream两种list判断字符串是否存在方案

这里写自定义目录标题 背景初始化方法一、filter过滤方法二、anyMatch匹配 背景 在项目开发中&#xff0c;经常遇到筛选list中是否包含某个子字符串&#xff0c;有多种方式&#xff0c;本篇主要介绍stream流的filter和anyMatch两种方案&#xff0c;记录下来&#xff0c;方便备…...

C语言-指针变量和变量指针

指针 预备知识 内存地址 字节&#xff1a;字节是内存的容量单位&#xff0c;英文名Byte&#xff0c;1Byte8bits 地址&#xff1a;系统为了便于区分每一个字节面对它们的逐一进行编号&#xff08;编号是唯一的&#xff09;&#xff0c;称为内存地址&#xff0c;简称地址。int…...

CMS漏洞-WordPress篇

一.姿势一&#xff1a;后台修改模板拿WebShell 1.使用以下命令开启docker cd /www/wwwroot / vulhub / wordpress / pwnscriptum docker - compose up - d 如果发现不能开启&#xff0c;可以检查版本和端口 2.访问网址登录成功后 外观 &#x1f449;编辑 &#x1f449;404.…...

初识Brainstorm(matlab)

Brainstorm是一款开源应用程序&#xff0c;专门用于分析脑部记录数据&#xff1a;MEG、EEG、fNIRS、ECoG、深部电极等。该应用程序免费&#xff0c;而且不需要Matlab许可证。Brainstorm主要优势是简单直观的图形界面&#xff0c;不需要任何编程知识。具体内容&#xff0c;可查看…...

2025年智能系统、自动化与控制国际学术会议(ISAC 2025)

重要信息 2025 International Conference on Intelligent Systems, Automation and Control 2025年3月28-30日 | 中国西安理工大学 | 会议官网&#xff1a; www.icisac.org 简介 在国家大力推动高质量发展与创新驱动战略的背景下&#xff0c;智能制造与自动化控制行业正迎…...

GGUF、Transformer、AWQ 详解与关系梳理

GGUF、Transformer、AWQ 详解与关系梳理 一、核心概念解析 Transformer 定义 &#xff1a;2017 年 Google 提出的基于自注意力机制的神经网络架构&#xff0c;是大语言模型的通用基础架构。功能 &#xff1a;用于文本生成、翻译、问答等任务&#xff0c;如 BERT、GPT 系列、…...

学习笔记|arduino uno r3|DS1307时钟芯片|Atmega328P| 设置时间|读取时间|无源晶振:DS1307时钟芯片实验

目录 芯片pinout&#xff1a; 实验器件&#xff1a; 实验连线 解决AVR 架构不支持 printf() 方法 使用GetTimeAndDate.ino设置时间&#xff1a; 使用SetTimeAndDate.ino设置时间&#xff1a; 芯片pinout&#xff1a; DS1307 是美国 DALLAS 公司推出的 I 总线接口实时时钟芯…...

Linux--进程创建

进程创建 写时拷贝&#xff08;时间换空间&#xff09; 更新页表项权限为只读----子进程写入----触发系统错误系统缺页中断&#xff0c;系统开始检测&#xff0c;系统判断写入区域是数据区还是代码区&#xff0c;如果是代码区就终结进程&#xff0c;如果是数据区就进行写时拷贝…...

MySQL 创建用户,建库,建表

以下是在 MySQL 中创建用户、数据库、表的详细操作步骤&#xff1a; 一、登录 MySQL -- 使用 root 用户登录&#xff08;需替换为实际密码&#xff09; mysql -u root -p输入密码后回车&#xff0c;进入 MySQL 命令行界面。 二、创建数据库 -- 创建名为 test_db 的数据库&a…...

成都国际数字影像产业园,文创产业运营新典范深度解析​

成都国际数字影像产业园位于成都市蓉北商圈金牛片区福堤路99号&#xff0c;是金牛区政府与树莓集团携手打造的省级“文化科技”融合示范园区。该产业园已成为西南地区乃至全国数字影像产业的一颗璀璨明珠&#xff0c;其成功运营模式堪称文创产业运营的新典范。 产业定位与资源…...

33、如果 std::vector 的元素是指针,需要注意什么?

对 std::vector 元素为指针的情况&#xff0c;需要注意以下几点&#xff1a; 内存管理&#xff1a; 如果 std::vector 存储的是原始指针&#xff0c;那么仅仅清空 vector 或者让 vector 被销毁&#xff0c;并不会释放指针所指向的内存。因此&#xff0c;需要确保在 vector 被销…...

Docker 速通(总结)

Docker 命令 镜像 docker build: 从 Dockerfile 构建镜像。docker pull: 从 Docker Hub 或其他注册表拉取镜像。docker push: 将镜像推送到 Docker Hub 或其他注册表。docker images: 列出本地镜像。docker rmi: 删除本地镜像。 容器 docker run: 创建并启动一个新的容器。…...

算法训练篇06--力扣611.有效三角形的个数

目录 1.题目链接&#xff1a;611.有效三角形的个数 2.题目描述&#xff1a; 3.解法一&#xff1a;(暴力解法)(会超时)&#xff1a; 4.解法二(排序双指针) 1.题目链接&#xff1a;611.有效三角形的个数 2.题目描述&#xff1a; 给定一个包含非负整数的数组 nums &#xf…...

Gin框架学习

一.介绍 Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter&#xff0c;速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上Gin。 下载 go get -u github.com/gin-gonic/gin 二.Gin示例 学习的时候&#xff0c;写在…...

青少年编程与数学 02-011 MySQL数据库应用 07课题、表的操作

青少年编程与数学 02-011 MySQL数据库应用 07课题、表的操作 一、数据库表&#xff08;Table&#xff09;二、创建表语法格式示例注意事项 三、字段的命名规则基本规则命名规范建议示例 四、字段数据类型数值类型字符串类型日期和时间类型其他类型 五、选择合适的数据类型1. **…...

【详细解决】pycharm 终端出现报错:“Failed : 无法将“Failed”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。

昨天在终端一顿操作后突然打开pycharm时就开始报错&#xff1a; 无法将“Failed”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff0c;请确保路径正确&#xff0c;然后再试一次。 所在位置 行:1 字符: 1 Failed to act…...

AcWing 839:模拟堆 ← multiset + unordered_map

【题目来源】 https://www.acwing.com/problem/content/841/ 【题目描述】 维护一个集合&#xff0c;初始时集合为空&#xff0c;支持如下几种操作&#xff1a; 1. I x&#xff0c;插入一个数 x&#xff1b; 2. PM&#xff0c;输出当前集合中的最小值&#xff1b; 3. DM&#…...

cmake教程

CMake 是一个跨平台的自动化构建系统&#xff0c;广泛用于管理软件构建过程。它使用 CMakeLists.txt 文件来配置项目的构建过程&#xff0c;并生成适用于不同编译器和操作系统的构建文件&#xff08;如 Makefile、Visual Studio 项目文件等&#xff09;。以下是一个简单的 CMak…...

小蓝的括号串1(栈,蓝桥云课)

问题描述 小蓝有一个长度为 nn 的括号串&#xff0c;括号串仅由字符 ( 、 ) 构成&#xff0c;请你帮他判断一下该括号串是否合法&#xff0c;合法请输出 Yes &#xff0c;反之输出 No 。 合法括号序列&#xff1a; 空串是合法括号序列。 若 ss 是合法括号序列&#xff0c;则 (…...

软考系统架构设计师考试学习和考试的知识点大纲,覆盖所有考试考点

以下是软考系统架构设计师考试的知识点大纲&#xff0c;覆盖所有官方考点&#xff0c;分为基础知识、核心技术、系统设计、案例分析、论文写作五大模块&#xff0c;帮助系统性学习和备考&#xff1a; 一、基础知识模块 计算机组成与体系结构 计算机硬件组成&#xff08;CPU、内…...

车载以太网网络测试-18【传输层-DOIP协议-1】

目录 1 摘要2 DOIP协议的概述2.1 DOIP协议背景2.2 ISO 13400概述 3 DOIP报文的帧结构以及实例3.1 DOIP报文帧结构3.2 实例示例 总结 1 摘要 在汽车网络通信中&#xff0c;诊断扮演了非常重要的角色&#xff0c;无论是故障诊断、整车下线配置&#xff0c;还是ECU的软件更新、远…...

密码学(Public-Key Cryptography and Discrete Logarithms)

Public-Key Cryptography and Discrete Logarithms Discrete Logarithm 核心概念&#xff1a;离散对数是密码学中一个重要的数学问题&#xff0c;特别是在有限域和循环群中。它基于指数运算在某些群中是单向函数这一特性。也就是说&#xff0c;给定一个群 G G G和一个生成元 …...

自然语言处理|深入解析 PEGASUS:从原理到实践

一、引言 在信息爆炸的时代&#xff0c;互联网上的文本数据以极快的速度增长。无论是新闻资讯、学术论文、社交媒体动态&#xff0c;还是各类报告文档&#xff0c;我们每天接触到的文字信息量巨大。如何快速、准确地提取关键内容成为一项重要任务。文本摘要技术通过将长篇文本…...

矩阵指数的定义和基本性质

1. 矩阵指数的定义 矩阵指数 e A t e^{\boldsymbol{A}t} eAt 定义为幂级数的形式&#xff1a; e A t ∑ k 0 ∞ ( A t ) k k ! e^{\boldsymbol{A}t} \sum_{k0}^\infty \frac{(\boldsymbol{A}t)^k}{k!} eAtk0∑∞​k!(At)k​ 当 A \boldsymbol{A} A 为 n n n \times n …...

react 技术栈请问该如何优化 DOM 大小

针对 React 应用中 DOM 大小过大 的问题&#xff0c;以下是详细的优化方案和具体操作步骤&#xff0c;帮助你提升 Lighthouse 性能评分和用户体验&#xff1a; 一、问题根源分析 DOM 大小过大&#xff08;如超过 1500 个节点或深度超过 32 层&#xff09;会导致&#xff1a; …...

redis,tar.gz安装后,接入systemctl报错解决

1. WARNING Memory overcommit must be enabled! 这种报错&#xff0c;有两种解决方法 1.1 修改系统参数 编辑 /etc/sysctl.conf 文件&#xff0c;设置 overcommit_memory 为 1 vm.overcommit_memory 11.2 修改redis的最大使用内存 修改配置文件 redis.conf maxmemory 1g…...

YOLOv5部署全场景问题解决方案手册(2025版)

YOLOv5部署全场景问题解决方案手册&#xff08;2025版&#xff09; 文章目录 YOLOv5部署全场景问题解决方案手册&#xff08;2025版&#xff09;[TOC]一、环境配置问题1.1 CUDA与PyTorch版本冲突1.2 依赖库缺失问题 二、模型转换问题2.1 ONNX导出失败2.2 TensorRT转换问题 三、…...

Canal 解析与 Spring Boot 整合实战

一、Canal 简介 1.1 Canal 是什么&#xff1f; Canal 是阿里巴巴开源的一款基于 MySQL 数据库增量日志解析&#xff08;Binlog&#xff09;中间件&#xff0c;它模拟 MySQL 的从机&#xff08;Slave&#xff09;行为&#xff0c;监听 MySQL 主机的二进制日志&#xff08;Binl…...

AI第一天 自我理解笔记--微调大模型

目录 1. 确定目标&#xff1a;明确任务和数据 2. 选择预训练模型 3. 数据预处理 (1) 数据清洗与格式化 (2) 划分数据集 (4) 数据加载与批处理 4. 构建微调模型架构 (1) 加载预训练模型 (2) 修改模型尾部&#xff08;适配任务&#xff09; (3) 冻结部分层&#xff08;…...

SpringBoot日志

目录 一、日志的用途 二、日志的使用 1.打印日志 2.在程序中得到日志对象 3.使用日志对象打印日志 4.日志格式说明 5.日志框架的了解 门面模式&#xff08;外观模式&#xff09; 6.日志级别 7.日志配置 配置日志级别 日志持久化 配置日志文件分割 配置日志格式 三、…...

Redis数据结构详解--列表

Redis 列表是简单的字符串列表&#xff0c;按照插入顺序排序&#xff0c;常用命令&#xff1a; LPUSH key value1 [value2...] 在列表头部插入一个或多个值RPUSH key value1 [value2...] 在列表尾部插入一个或多个值LPOP key 移除并获取列表头部第一个元素RPOP key 移除并获取…...

[项目]基于FreeRTOS的STM32四轴飞行器: 六.2.4g通信

基于FreeRTOS的STM32四轴飞行器: 六.2.4g通信 一.Si24Ri原理图二.Si24R1芯片手册解读三.驱动函数讲解五.移植2.4g通讯&#xff08;飞控部分&#xff09;六.移植2.4g通讯&#xff08;遥控部分&#xff09;七.通讯模块的完成&#xff08;遥控部分&#xff09;七.通讯模块的完成&a…...

ElasticSearch 7.x 集群 + Kibana 部署完全指南(5节点)

ElasticSearch 7.x 集群 Kibana 部署完全指南&#xff08;5节点&#xff09; 一、基础环境准备 1. 系统要求 CentOS 7/Ubuntu 18.04JDK 11&#xff08;Elasticsearch 7自带JDK&#xff09;内存&#xff1a;建议每个节点≥8GB磁盘&#xff1a;≥50GB&#xff08;根据数据量调…...

前端项目中应该如何选择正确的图片格式

在前端项目中选择正确的图片格式是优化页面性能、提升用户体验的关键步骤之一。以下是常见图片格式的特点、适用场景及选择建议&#xff0c;帮助你在不同场景下做出最优决策&#xff1a; 一、常见图片格式对比 格式特点适用场景不适用场景JPEG- 有损压缩&#xff0c;文件小- 不…...

Python实现WYY音乐下载

一、需求背景 WYY音乐作为国内主流音乐平台,其歌曲资源丰富但下载接口存在多重加密保护。本文将通过Python结合JS逆向技术,解析其核心加密逻辑,实现免费歌曲的下载功能。 二、技术难点分析 1. 接口加密机制 通过抓包分析可知,网易云核心接口使用两次加密: 第一次:获取…...