Qt - 一文理解QThread多线程(万字剖析整理)
目录
- 为什么需要多线程
- QThread使用方法
- new QThread Class & Override run()
- new Object Class & moveToThread(new QThread)
- connect
- 事件循环
- 源码分析
- 如何正确退出线程
- 堆栈大小
- 优先级
- 线程间通讯
- 线程同步
- 互斥锁
- 读写锁
- 信号量
- 条件变量
- 可重入与线程安全
- QObject的可重入性
- 开启多少个线程合理
- 参考鸣谢
为什么需要多线程
在开发存在界面交互的程序中,为了使一些耗时操作不造成卡顿;我们一般会将这些耗时操作放到子线程中进行处理,常见的如一些同步通讯。
虽然已编写过几次多线程的程序,但是每次使用都感觉心里不踏实,借用 QThread
总结一下罢。
QThread使用方法
Qt 中使用多线程,必然绕不开的是 QThread
。建议先过一遍 QThread Class 文档。
文档中演示了两种使用方法:
new QThread Class & Override run()
/*------------------------------WorkerThread-----------------------------------*/
class WorkerThread : public QThread
{Q_OBJECT
public:explicit WorkerThread();
protected:void run();
signals:void resultReady(const QString &s);
};void WorkerThread::run(){/* ... here is the expensive or blocking operation ... */
}/*------------------------------MainWindow-----------------------------------*/
void MainWindow::startWorkInAThread()
{WorkerThread *workerThread = new WorkerThread();// Release object in workerThreadconnect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);workerThread->start();
}
需要 注意 的:
run()
中未调用exec()
开启even loop
,那么在run()
执行结束时,线程将自动退出。该案例中,
WorkerThread
存在于实例化它的旧线程中,仅有run()
中是在子线程中执行的。我们可以通过以下代码打印线程ID
进行验证:qDebug()<<"mythread QThread::currentThreadId()==" << QThread::currentThreadId();
这就存在一个尴尬的问题,如果在 WorkerThread
的 run()
中使用了 WorkerThread
的成员变量,而且 QThread
的其他方法也使用到了它,即我们从不同线程访问该成员变量,这时需要自行检查这样是否安全。
这个例子也说明了,QThread
实例本身并不是一个线程,正如 QThread Class
开篇点明这是一个 线程管理类 。
The QThread class provides a platform-independent way to manage threads.
注意:这种使用方法并不推荐,至于它为什么仍然出现在 QThread的文档里作为案例 “误导” 我们,这貌似是个历史问题。
在 Qt 4.4
版本以前的 QThread
类是个抽象类,要想编写多线程代码唯一的做法就是 继承 QThread
类。该论断在 Qt4.3
的 QThread Class 中可以印证。
但是之后的版本中,Qt
库完善了线程的亲和性以及信号槽机制,我们有了更为优雅的使用线程的方式,即 QObject::moveToThread()
。但是即使在 2020
的今天,网上仍然有不少教程教我们使用Qt多线程的旧方法;难怪 Bradley T. Hughes
在 2010
专门写了篇 You’re doing it wrong…,为此我只能表示:
new Object Class & moveToThread(new QThread)
下面介绍推荐做法。
/*--------------------DisconnectMonitor-------------------------*/
class DisconnectMonitor : public QObject
{Q_OBJECTpublic:explicit DisconnectMonitor();signals:void StartMonitor(long long hanlde);void StopMonitor();// if Controller disconnect emit this signalvoid Disconnect();private slots:void slot_StartMonitor(long long hanlde);void slot_StopMonitor();// State machinevoid Monitor();private:long long ControllerHanlde;QTimer *MonitorTimer;
};DisconnectMonitor::DisconnectMonitor()
{// New a Timer monitor controller by timingMonitorTimer = new QTimer;ControllerHanlde = 0;connect(MonitorTimer,&QTimer::timeout,this,&DisconnectMonitor::Monitor);connect(this,&DisconnectMonitor::StartMonitor,this,&DisconnectMonitor::slot_StartMonitor);connect(this,&DisconnectMonitor::StopMonitor,this,&DisconnectMonitor::slot_StopMonitor);MonitorTimer->start(TAKETIME);
}void DisconnectMonitor::Monitor(){// if not Controller -> returnif(0 == ControllerHanlde){return;}//else Listeningelse{int state = IsConnect(ControllerHanlde);if (0 != state){emit Disconnect();}}
}/*---------------------------Controller----------------------------*/
class Controller : public QObject
{Q_OBJECTQThread workerThread;
public:Controller() {DisconnectMonitor *monitor = new DisconnectMonitor;monitor->moveToThread(&workerThread);connect(workerThread, &QThread::finished, monitor, &QObject::deleteLater);connect(monitor,SIGNAL(Disconnect()),this,SLOT(DisconnectManage()));workerThread.start();}~Controller() {workerThread.quit();workerThread.wait();}
private slots:void DisconnectManage();
};
这里通过 moveToThread()
将 Object
对象移到到新线程中,如此一来整个 monitor
都将在子线程中运行(其实这句话是有问题的,这是一个感性的理解)。
我们在 DisconnectMonitor
中定义了一个定时器用以实现定时检测。由于不能跨线程操作DisconnectMonitor
中的定时器,我们在类创建时就开启定时器,在超时事件中实现定时监听,如果检测到设备断开了,就发送 Disconnect()
信号。
使用 movetoThread()
需要注意的是:
上面的案例中,并不能认为 monitor 的控制权归属于新线程!它仍然属于主线程,正如一位博主所说【在哪里创建就属于哪里】。movetoThread()的作用是将槽函数在指定的线程中调用。仅有槽函数在指定线程中调用,包括构造函数都仍然在主线程中调用!!!
DisconnectMonitor
须继承自 顶层 父类Object
,否则不能移动。如果
Thread
为nullptr
,则该对象及其子对象的所有事件处理都将停止,因为它们不再与任何线程关联。调用
movetoThread()
时,移动对象的所有计时器将被重置。 计时器首先在当前线程中停止,然后在targetThread中重新启动(以相同的间隔),这时定时器属于子线程。若在线程之间不断移动对象可能会无限期地延迟计时器事件。
QObject Class
中 特别提醒:movetoThread()
是线程不安全的,它只能见一个对象“推”到另一个线程,而不能将对象从任意线程推到当前线程,除非这个对象不再与任何线程关联。
connect
connect
函数原型如下:
static QMetaObject::Connection connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,const QObject *receiver, const QMetaMethod &method,Qt::ConnectionType type = Qt::AutoConnection);
inline QMetaObject::Connection connect(const QObject *sender, const char *signal,const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
我们经常使用 connect
,但是确很少留意最后一个参数 Qt::ConnectionType
Qt::AutoConnection
默认连接类型,如果信号接收方与发送方在同一个线程,则使用Qt::DirectConnection
,否则使用Qt::QueuedConnection
;连接类型在信号 发射时 决定。Qt::DirectConnection
信号所连接至的槽函数将会被立即执行,并且是在发射信号的线程;倘若槽函数执行的是耗时操作、信号由UI线程
发射,则会 阻塞 Qt的事件循环,UI会进入 无响应状态 。Qt::QueuedConnection
槽函数将会在接收者的线程被执行,此种连接类型下的信号倘若被多次触发、相应的槽函数会在接收者的线程里被顺次执行相应次数;当使用QueuedConnection
时,参数类型必须是Qt基本类型,或者使用qRegisterMetaType()
进行注册了的自定义类型。Qt::BlockingQueuedConnection
和Qt::QueuedConnection
类似,区别在于发送信号的线程在槽函数执行完毕之前一直处于阻塞状态;收发双方必须不在同一线程,否则会导致 死锁 。Qt::UniqueConnection
执行方式与AutoConnection
相同,不过关联是唯一的。(如果相同两个对象,相同的信号关联到相同的槽,那么第二次connect
将失败)
注意:
如果接受者线程中有一个事件循环,那么当发送者与接受者在不同的线程中时,使用 DirectConnection
是不安全的;类似的,调用其他线程中的对象的任何函数也是不安全的。值得留意的是,QObject::connect()
函数本身是线程安全的。
事件循环
若使用默认的 run()
方法或自行调用 exec()
,则QThread将开启事件循环。QThread
同样提供了 exit()
函数和 quit()
槽。这赋予了QThread使用需要事件循环的非GUI类的能力(QTimer
、QTcpSocket
等)。也使得该线程可以关联任意一个线程的信号到指定线程的槽函数。如果一个线程没有开启事件循环,那么该线程中的 timeout()
将永远不会发射。
如果在一个线程中创建了OBject
对象,那么发往这个对象的事件将由该线程的事件循环进行分派。
我们可以手动使用 QCoreApplication::postEvent()
在任何时间先任何对象发送事件,该函数是线程安全的。
源码分析
看到这,对线程的创建尚有困惑,于是查找了一下 Qt 的源码。目前在 qthread_win.cpp
找到答案,至于程序是如何从 QThread -> qthread_win
尚不清楚。
使用时,我们均以 QThread->start()
开启线程:
/*-----------------------qthread_win.cpp---------------------------------*/
void QThread::start(Priority priority)
{Q_D(QThread);QMutexLocker locker(&d->mutex);if (d->isInFinish) {locker.unlock();wait();locker.relock();}if (d->running)return;d->running = true;d->finished = false;d->exited = false;d->returnCode = 0;d->interruptionRequested = false;/*NOTE: we create the thread in the suspended state, set thepriority and then resume the thread.since threads are created with normal priority by default, wecould get into a case where a thread (with priority less thanNormalPriority) tries to create a new thread (also with priorityless than NormalPriority), but the newly created thread preemptsits 'parent' and runs at normal priority.*/// 【1】判断当前环境,调用系统API创建线程 d->handle 为线程句柄#if defined(Q_CC_MSVC) && !defined(_DLL) // && !defined(Q_OS_WINRT)# ifdef Q_OS_WINRT# error "Microsoft documentation says this combination leaks memory every time a thread is started. " \"Please change your build back to -MD/-MDd or, if you understand this issue and want to continue, " \"edit this source file."# endif// MSVC -MT or -MTd buildd->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,this, CREATE_SUSPENDED, &(d->id));#else// MSVC -MD or -MDd or MinGW buildd->handle = CreateThread(nullptr, d->stackSize,reinterpret_cast<LPTHREAD_START_ROUTINE>(QThreadPrivate::start),this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));#endif // Q_OS_WINRT//创建线程失败if (!d->handle) {qErrnoWarning("QThread::start: Failed to create thread");d->running = false;d->finished = true;return;}//优先级int prio;d->priority = priority;switch (d->priority) {case IdlePriority:prio = THREAD_PRIORITY_IDLE;break;case LowestPriority:prio = THREAD_PRIORITY_LOWEST;break;case LowPriority:prio = THREAD_PRIORITY_BELOW_NORMAL;break;case NormalPriority:prio = THREAD_PRIORITY_NORMAL;break;case HighPriority:prio = THREAD_PRIORITY_ABOVE_NORMAL;break;case HighestPriority:prio = THREAD_PRIORITY_HIGHEST;break;case TimeCriticalPriority:prio = THREAD_PRIORITY_TIME_CRITICAL;break;case InheritPriority:default:prio = GetThreadPriority(GetCurrentThread());break;}if (!SetThreadPriority(d->handle, prio)) {qErrnoWarning("QThread::start: Failed to set thread priority");}if (ResumeThread(d->handle) == (DWORD) -1) {qErrnoWarning("QThread::start: Failed to resume new thread");}
}
核心为【1】我们先找找 _beginthreadex
原型,这是一个 Windows
系统 API
:
unsigned long _beginthreadex(
void *security, // 安全属性,NULL为默认安全属性
unsigned stack_size, // 指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
unsigned ( __stdcall *start_address )( void * ), // 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
void *arglist, // 传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
unsigned initflag, // 线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
unsigned *thrdaddr // 用于记录线程ID的地址
对应源码食用,可发现线程函数地址为 QThreadPrivate::start
,跟踪一下:
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg) noexcept
{// 强制转换QThread *thr = reinterpret_cast<QThread *>(arg);QThreadData *data = QThreadData::get2(thr);qt_create_tls();TlsSetValue(qt_current_thread_data_tls_index, data);data->threadId.storeRelaxed(reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId())));QThread::setTerminationEnabled(false);{QMutexLocker locker(&thr->d_func()->mutex);data->quitNow = thr->d_func()->exited;}data->ensureEventDispatcher();
#if !defined(QT_NO_DEBUG) && defined(Q_CC_MSVC) && !defined(Q_OS_WINRT)// sets the name of the current thread.QByteArray objectName = thr->objectName().toLocal8Bit();qt_set_thread_name(HANDLE(-1),objectName.isEmpty() ?thr->metaObject()->className() : objectName.constData());
#endif//发射 started 信号emit thr->started(QThread::QPrivateSignal());QThread::setTerminationEnabled(true);//调用QThread,run函数thr->run();finish(arg);return 0;
}
可以发现调用了 QThread
的 run()
方法。
而该方法默认开启事件循环:
void QThread::run()
{(void) exec();
}
这样我们的线程就跑起来了。
如何正确退出线程
首先,删除 QThread
对象并不会停止其管理的线程的执行。删除正在运行的 QThread
将导致 程序奔溃。在删除 QThread
之前我们需要等待 finish
信号。
对于未开启事件循环的线程,我们仅需让
run()
执行结束即可终止线程,常见的做法是通过bool
变量进行控制。由于我们的bool runenanble
被多线程访问,这里我们需要定义一个QMutex
进行加锁保护。至于加锁的效率问题,网上有大佬测出大概速度会降低1.5倍(Release模式)。void TestThread::stopThread(){mutex.lock();runenanble = false;mutex.unlock();}void TestThread::run(){runenanble = true;while(1){if(mutex.tryLock()){if(!runenable)break;else{/*dosomething*/}}} }
对于开启了事件循环的线程,正常的退出线程其实质是退出事件循环。
quit()/exit() + wait()
若线程中开始开启了EvenLoop
,耗时代码执行结束后,线程并不会退出。我们可调用quit()/exit() + wait()
实现退出。terminate()+ wait()
调用terminate()
后,将根据操作系统的调度,线程可能立即结束也可能不会,终止之后仍需使用wait()
。
由于线程可能在任何位置终止,强制结束线程是危险的操作,可能在修改数据数据时终止,可能导致线程状态无法清除,可能导致锁异常。因此并 不推荐使用。finished
仅依靠上面的方法退出线程,可能存在 内存泄漏 的情况。注意到官方案例中都使用了finished
信号:connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
如果类对象保存在 栈 上,自然销毁由操作系统自动完成;如果是保存在 堆 上,没有父对象的指针要想正常销毁,需要自行释放。
从Qt4.8
开始,我们就可以通过将finished()
信号链接至Object::deleteLater()
来释放刚刚结束的线程中的对象。上文例二的
QThread
并未new
出来,这样在析构时就需要调用Thread::wait()
,如果是堆分配的话, 可以通过deleteLater
来让线程自杀。QThread workerThread = new QThread(); connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);
注意:程序退出前,需要判断各线程是否已退出,如果不进行判断,很可能程序退出时会崩溃。如果线程的父对象是窗口对象,那么在窗体的析构函数中,还需要调用 wait()
等待线程完全结束再进行下面的析构。
堆栈大小
大多数操作系统都为线程堆栈设置了最大和最小限制。如果超出这些限制,线程将无法启动。
每个线程都有自己的栈,彼此独立,由编译器分配。一般在 Windows
的栈大小为 2M
,在 Linux
下是 8M
。
Qt 提供了获取以及设置栈空间大小的函数:stackSize()
、setStackSize(uint stackSize)
。其中 stackSize()
函数不是返回当前所在线程的栈大小,而是获取用 stackSize()
函数手动设置的栈大小。
优先级
没错,QThread
不再让线程间拼得你死我活,我们可以通过 setPriority()
设置线程优先级,通过 priority()
获取线程优先级。
Constant | Value | Description |
---|---|---|
QThread::IdlePriority | 0 | scheduled only when no other threads are running. |
QThread::LowestPriority | 1 | scheduled less often than LowPriority. |
QThread::LowPriority | 2 | scheduled less often than NormalPriority. |
QThread::NormalPriority | 3 | the default priority of the operating system. |
QThread::HighPriority | 4 | scheduled more often than NormalPriority. |
QThread::HighestPriority | 5 | scheduled more often than HighPriority. |
QThread::TimeCriticalPriority | 6 | scheduled as often as possible. |
QThread::InheritPriority | 7 | use the same priority as the creating thread. This is the default. |
此外,QThread
类还提供了 yieldCurrentThread()
静态函数,该函数是在通知操作系统“我这个线程不重要,优先处理其他线程吧”。当然,调用该函数后不会立马将 CPU 计算资源交出去,而是由操作系统决定。
QThread
类还提供了 sleep()
、msleep()
、usleep()
这三个函数,这三个函数也是在通知操作系统“在未来 time 时间内我不参与 CPU 计算”。
值得注意的是:usleep()
并 不能保证准确性 。某些OS可能将舍入时间设置为10us/15us
;在 Windows
上它将四舍五入为 1ms
的倍数。
线程间通讯
其实上文已经演示了两种方式:
共享内存
线程隶属于某一个进程,与进程内的其他线程一起共享这片地址空间。消息传递
借助Qt的信号槽&事件循环机制。
说个题外话,在 Android
中,UI操作只能在主线程中进行。这种情况在Qt中其实类似,那么当我们子线程需要更新UI控件时怎么处理呢?很简单发送信号让主线程更新即可~
线程同步
虽然使用多线程的思想是让程序尽可能并发执行,但是总有一些时候,线程必须停止以等待其他线程。例如两个线程同时写全局变量,由于写入操作相对于CPU不具备原子性,结果通常具有不确定性。
互斥锁
QMutex
,任意时刻至多有一个线程可以使用该锁,若一个线程尝试获取 mutex
,而此时 mutex
已被锁住。则这儿线程将休眠直到 mutex解锁
为止。互斥锁经常用于共享数据。
QMutex mutex;void thread1()
{mutex.lock();//dosomething()mutex.unlock();
}void thread2()
{mutex.lock();//dosomething()mutex.unlock();
}
读写锁
QReadWriteLock
,与 QMutex
类似,不过它允许多个线程对共享数据进行读取。使用它替代 QMutex
可提高多线程程序的并发度。
QReadWriteLock lock;void ReaderThread::run()
{...lock.lockForRead();read_file();lock.unlock();...
}void WriterThread::run()
{...lock.lockForWrite();write_file();lock.unlock();...
}
信号量
QSemaphore
,QMutex
的一般化,用于保护一定数量的相同的资源。典型的是 生成者-消费者。
QSemaphore sem(5); // sem.available() == 5sem.acquire(3); // sem.available() == 2
sem.acquire(2); // sem.available() == 0
sem.release(5); // sem.available() == 5
sem.release(5); // sem.available() == 10sem.tryAcquire(1); // sem.available() == 9, returns true
sem.tryAcquire(250); // sem.available() == 9, returns false
条件变量
QWaitCondition
,它允许一个线程在一些条件满足的情况下唤醒其他线程。
QWaitCondition Class
中列举一个接收按键并唤醒去其他线程进行处理的 demo:
forever {mutex.lock();keyPressed.wait(&mutex);++count;mutex.unlock();do_something();mutex.lock();--count;mutex.unlock();
}forever {getchar();mutex.lock();// Sleep until there are no busy worker threadswhile (count > 0) {mutex.unlock();sleep(1);mutex.lock();}keyPressed.wakeAll();mutex.unlock();
}
可重入与线程安全
在查看 Qt Class
文档时,有时候我们会看到线程安全和可重入的标记,什么是线程安全,什么是可重入?
线程安全
表示该函数可被多个线程调用,即使他们使用了共享数据,因为该共享数据的所有实例都被序列化了。可重入
一个可重入的函数可被多个线程调用,但是只能是使用自己数据的情况下。
如果每个线程使用一个类的不同实例,该类的成员函数可以被多个线程安全地调用,那么该类被称为可重入的;如果所有线程使用该类的相同实例,该类的成员函数也可以被多个线程安全地调用,那么该类是线程安全的。
QObject的可重入性
QObject
是可重入的。它的大多数 非GUI子类,如 QTimer
、QTcpSocket
也都是可重入的,可以在多线程中使用。值得注意的是,这些类被设计成在单一线程中进行创建和使用,在一个线程中创建一个对象,然后在另一个线程中调用这个对象的一个函数是无法保证一定可以工作的。需要满足以下三个条件:
QObject
的子对象必须在创建它的父对象的线程中创建。这意味这不要将QThread
对象(this)
作为在该线程中创建的对象的父对象。- 事件驱动对象只能在单一线程中使用。例如:不可以在对象所在的线程以外的其他线程中启动一个定时器或连接套接字。
- 必须保证在删除
QThread
对象以前,删除在该线程中创建的所有对象。
对于大部分 GUI类
,尤其是 QWidget及其子类,都是不可重入的,我们只能在主线程中使用。QCoreApplication::exec()
也必须在主线程中调用。
开启多少个线程合理
线程的切换是要消耗系统资源的,频繁的切换线程会使性能降低。线程太少的话又不能完全发挥 CPU 的性能。
一般后端服务器都会设置最大工作线程数,不同的架构师有着不同的经验,有些业务设置为 CPU 逻辑核心数的4倍,有的甚至达到32倍。
在 Venkat Subramaniam
博士的 《Programming Concurrency on the JVM》 这本书中提到关于最优线程数的计算,即:
线程数量=可用核心数/(1−阻塞系数)线程数量 = 可用核心数/(1 - 阻塞系数) 线程数量=可用核心数/(1−阻塞系数)
可用核心数就是所有逻辑 CPU 的总数,这可以用 QThread::idealThreadCount()
静态函数获取,比如双核四线程的 CPU 的返回值就是4。
但是阻塞系数比较难计算,这需要用一些性能分析工具来辅助计算。如果只是粗浅的计算下线程数,最简单的办法就是 CPU 核心数 * 2 + 2 。更为精细的找到最优线程数需要不断的调整线程数量来观察系统的负载情况。
参考鸣谢
Qt Creator快速入门
Qt5.9 C++开发指南
Qt多线程编程爬坑笔记
Qt使用多线程的一些心得——1.继承QThread的多线程使用方法
Qt使用多线程的一些心得——2.继承QObject的多线程使用方法
Qt 多线程编程之敲开 QThread 类的大门
QThread源码浅析
Qt - 一文理解QThread多线程(万字剖析整理)相关推荐
- QT之深入理解QThread
QT之深入理解QThread 理解QThread之前需要了解下QThread类,QThread拥有的资源如下(摘录于QT 5.1 帮助文档): 在以上资源中,本文重点关注槽:start():信号:st ...
- Python Qt GUI设计:QTimer计时器类、QThread多线程类和事件处理类(基础篇—8)
目录 1.QTimer计时器类 2.QThread多线程类 3.事件处理类 一般情况下,应用程序都是单线程运行的,但是对于GUI程序来说,单线程有时候满足不了需求.例如,如果需要执行一个特别耗时的操作 ...
- qt线程如何接收linux信号,Linux-Qt使用QThread多线程isRunning标志量问题
---恢复内容开始--- 摘要 Qt帮助文档中是这样介绍的: bool QThread::isRunning () constReturnstrue if the thread is running; ...
- QCM学习—基于QT自制上位机(多线程)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言:应用程序在某些情况下需要处理比较复杂的逻辑,例如常规的图传上位机,如果在传输图片跑到较高码流或对图像执行一些处理任务是 ...
- 一文理解Netty模型架构
转载自 一文理解Netty模型架构 本文基于Netty4.1展开介绍相关理论模型,使用场景,基本组件.整体架构,知其然且知其所以然,希望给读者提供学习实践参考. 1 Netty简介 Netty是 一 ...
- QThread多线程编程分析
QThread多线程编程分析 传统图形界面应用程序都只有一个线程执行,并且一次执行一个操作.如果用户调用一个比较耗时的操作,就会冻结界面响应.一个解决方法是按照事件处理的思路:调用 Void QApp ...
- 一文理解C语言中的volatile修饰符
一文理解C语言中的volatile修饰符 2019/12/2 FesianXu 前言 volatile修饰符是在嵌入式开发和多线程并发编程中常见的修饰符,理解其对于实践过程非常有帮助,此文参考了[1] ...
- 深入理解Linux多线程
深入理解Linux多线程 目录 Linux线程概念 什么是线程 二级页表 线程的优点 线程的缺点 线程异常 线程用途 Linux进程VS线程 Linux线程控制 POSIX线程库 创建线程 线程等待 ...
- Qt帮助文档使用方法
Qt 帮助文档非常细致而全面,应有尽有,是非常不错的自学教材.因为 Qt 帮助文档太多,所以难以都翻译成中文,即使翻译了一部分,翻译花的时间太多,翻译更新的时效性也难以保证,所以还是得看英文帮助为主. ...
- 一文理解CatBoost!
1. CatBoost简介 CatBoost是俄罗斯的搜索巨头Yandex在2017年开源的机器学习库,是Boosting族算法的一种.CatBoost和XGBoost.LightGBM并称为GBDT ...
最新文章
- 阿里AI labs发布两大天猫精灵新品,将与平头哥共同定制智能语音芯片
- elasticsearch mapping之fields
- 06.德国博士练习_08_query_dsl
- 零基础初学c语言常见的10个错误
- 未来计算机技术的发展趋势有哪些,未来计算机发展的5种趋势
- python 类的使用基础
- iOS-Senior19-FMDB第三方应用
- php求1到100的素数之和,php 质数计算 求100以内质数和
- i.max6 e9 android系统添加3G模块支持 上
- TFS工作项模板自定义指南
- PUE 1.2,总投资达36.4亿,17600个机柜!天和防务拟建陕西最大数据中心
- kubernetes国内镜像代理
- Ubuntu下安装VS Code遇到的小问题
- 无人机利用视觉slam实现位置估计
- errorCode 1045,state 28000: Access denied for user 'mysql'@'localhost' (using password: YES)
- Oracle卸载的全过程
- 测试用例怎么写?不会测试用例的看过来,Web测试所涉及的主要测试点
- 基于omi的omim-tag组件
- U盘的资料删除了还能恢复吗?U盘误删除文件恢复技巧分享
- 拉新拓客难?捷径体系健身房系统一招就够了!