In the previous chapter we briefly explained how to use the QThread implementation thread. Now let's start with a detailed description of how to write multithreaded programs "correctly". Most of our content here comes from a wiki of QT, and interested children's shoes can be seen in the original.
Before we introduce, we need to recognize two terms:
- reentrant (reentrant): If multiple threads can invoke all functions of a class at the same time and ensure that each function call refers to a unique data, the class is called reentrant (reentrant means that Functions in the referenced class can is called simultaneously by multiple threads, provided this each invocation of the F Unctions reference unique data.). Most C + + classes are reentrant. Similarly, a function is called reentrant, and if the function allows multiple threads to be called at the same time, each invocation can only use its own data. Global variables are not data that is unique to a function, but are shared. In other words, this means that the consumer of a class or function must use some additional mechanism, such as a lock, to control the serialization of access to an instance of an object or to shared data.
- thread Safety (thread-safe): If multiple threads can invoke all functions of a class at the same time, even if each function call refers to a shared data, this class is thread-safe (Threadsafe means that Functions in the referenced class can is called simultaneously by multiple threads even when each invocation references sh ared data.). If multiple threads can access the shared data of a function at the same time, it is called a thread-safe function.
Further, for a class, if different instances can be used by different threads at the same time without being affected, it is said that the class is reentrant, and if all the member functions of this class can be called at the same time by different threads without being affected, even if these calls are for the same object, then we say that the class is thread safe. As you can see, thread-safe semantics are stronger than reentrant. Next, we'll start with the event. As we said before, Qt is event-driven. In Qt, an event is represented by a normal object ( QEvent or its subclasses). This is a big difference between an event and a signal: an event is always represented by an object of one type, and for a particular object, the signal does not have such a target object. All QObject subclasses can QObject::event() control the object of an event by overriding the function.
Events can be generated by programs, or they can be generated outside the program. For example:
QKeyEventand QMouseEvent objects represent the interaction of a keyboard or mouse, usually generated by the system's window manager;
QTimerEventEvents are sent to a timer timeout QObject , and timer events are usually issued by the operating system;
QChildEventSent to one when a child object is added or deleted QObject , which is emitted by the Qt application itself.
It is important to note that unlike signals, events are not distributed as a result. The event is then added to a queue (where the queue meaning is the concept in the data structure, FIFO), and the queue is called the event queue. The event dispatcher traverses the event queue and, if there is an event in the event queue, sends the event to its target object. This loop is called the event loop. The pseudo-code description for the event loop is roughly as follows:
C + +
| 1234567 |
while (is_active) {While (! Event_queue_is_empty) { dispatch_next_event(); } wait_for_more_events(); } |
As stated earlier, calling a QCoreApplication::exec() function means entering the main loop. We interpret the event loop as an infinite loop, until QCoreApplication::exit() it QCoreApplication::quit() is called, and the event loop actually exits.
Inside the pseudo-code while , the entire event queue is traversed, the events found from the queue are sent, and the wait_for_more_events() function blocks the event loop until a new event is generated. We carefully consider this code, and the wait_for_more_events() new events that are obtained in the function should be generated outside the program. Because all internal events should be processed in the event queue. Therefore, we say that the event loop wait_for_more_events() goes into hibernation in the function and can be awakened in the following situations:
- The action of the Window manager (keyboard, mouse button press, interact with the window, etc.);
- Socket action (network-readable data, or socket non-blocking write, etc.);
- Timer
- Events emitted by other threads (we will explain this in more detail later in this article).
In a Unix-like system, a window manager (such as X11) notifies an application of window activity through Sockets (Unix Domain or TCP/IP), because the client interacts with the X server through this mechanism. If we decide to implement socketpair(2) cross-thread event distribution based on internal functions, then the window's management activity needs to be awakened:
- Socket sockets
- Timers Timer
This is what the select(2) system call does: it monitors a set of descriptors for window activity and, if there is no activity for a certain amount of time, it emits a timeout message (this timeout is configurable). What Qt does is to select() convert the return value into an object of the appropriate QEvent subclass and put it into the event queue. Well, now you know the internal mechanism of the event loop.
As for why the event loop is needed, we can simply list a list:
- Drawing and interaction of components :
QWidget::paintEvent() QPaintEvent Called when an event is emitted. The event can be QWidget::update() emitted through an internal call or a window manager, such as a hidden window. All interactive events (keyboards, mice) are similar: These events require an event loop to be emitted.
- Timers : Long story short, they will be emitted at times
select(2) like other similar calls, so you need to allow Qt to implement these calls by returning the event loop.
- Network : All low-level network classes (
QTcpSocket , QUdpSocket and QTcpServer so on) are asynchronous. When you call a read() function, they simply return the data that is available, and when you call write() the function, they simply write the written list to the schedule and execute it later. True Read and write is performed only when the event loop is returned. Note that these classes also have synchronous functions ( waitFor functions that begin with), but they are not recommended because they block the event loop. Advanced classes, for example, QNetworkAccessManager do not provide synchronization APIs at all, so event loops must be required.
With the event loop, you'll want to block it. There may be a lot of reasons for blocking it, for example I would like to have QNetworkAccessManager synchronous execution. Before explaining why you should never block an event loop , we need to understand what is "blocking". Let's say we have a button Button that sends a signal when the button is clicked. This signal is Worker connected to an object that Worker performs a time-consuming operation. When the button is clicked, we observe the function call stack from top to bottom:
| 12345678 |
main(int, char **) qapplication::exec() [... ]qwidget::event(qevent *) Button::mousepressevent(qmouseevent *) Button::clicked() [... ]Worker::doWork() |
We main() start the event loop in the function, which is the common QApplication::exec() function. When the window manager detects a mouse click, Qt discovers and converts it to QMouseEvent an event, which is sent to the function of the component event() . This process is implemented through QApplication::notify() functions. Note that our button does not overwrite the event() function, so the implementation of its parent class will be executed, that is, the QWidget::event() function. This function discovers that the event is a mouse click event, and then calls the corresponding event handler, which is the Button::mousePressEvent() function. We rewrite this function to Button::clicked() signal that this signal will call the Worker::doWork() slot function. We have elaborated on this mechanism in the previous Events section, if you do not understand this part of the mechanism, please refer to the previous section.
What is the worker event loop doing while working hard? Perhaps you have guessed the answer: do nothing! The event loop issues a mouse-down event and waits for the event handler to return. At this point, it is always blocked until the Worker::doWork() function ends. Note that we used the word "blocking", that is, the so-called blocking event loop , meaning that no event was distributed.
When the event is stuck, the component does not update itself (because the object is still in the QPaintEvent queue), there is no other interaction (or the same reason), and the timer does not time out and The network interaction will become slower until it stops . In other words, the activities of the various dependent event loops that we have analyzed in the preceding events will stop. At this point, you need the window Manager to detect that your application is no longer handling any events, and then tell the user that your program is out of response . That's why we need to handle events quickly and return to the event loop as quickly as possible.
Now, here's the point: we can't avoid time-consuming operations in business logic, so what can we do to take time-consuming actions without blocking the event loop? There are generally three solutions: first, we move the task to another thread (as we saw in the previous chapter, but now we skip this part); second, we manually force the event loop to run. To force the event loop to run, we need to call the function over and over in a time-consuming task QCoreApplication::processEvents() . QCoreApplication::processEvents()The function emits all events in the event queue and returns to the caller immediately. Think about it, what we're doing here is simulating an event loop.
Another solution we mentioned in the previous chapters: using QEventLoop classes to re-enter the new event loop. By calling the QEventLoop::exec() function, we re-enter the new event loop and QEventLoop::quit() send a signal to the slot function to exit the event loop. Take the previous example:
C + +
| 123456 |
qeventloop eventloop; Connect(netWorker, &networker::finished, &eventloop, &qeventloop::quit); qnetworkreply *reply = netWorker, get(URL); replymap. Insert(reply, fetchweatherinfo); eventloop. EXEC(); |
QNetworkReplyThe blocking API is not provided and requires an event loop. We achieve this by a partial QEventLoop : When the network response is complete, the local event loop exits.
As we have emphasized earlier, it is very careful to enter the event loop through "other entrances": because it leads to recursive invocation! Now we can see why this leads to recursive invocation. Look back at the example of the button. When we Worker::doWork() call the function in the slot function QCoreApplication::processEvents() , the user taps the button again, and the slot function Worker::doWork()又 is called at once:
| 123456789101112131415 |
main(int, char **) qapplication::exec() [... ]qwidget::event(qevent *) Button::mousepressevent(qmouseevent *) Button::clicked() [... ]Worker::doWork() //<strong> First call </strong> qcoreapplication::processevents() //<strong> issue all events manually </strong> [... ]qwidget::event(qevent * ) //<strong> user clicked the button again ...</strong> Button::mousepressevent(qmouseevent *) Button::clicked() //<strong> sends out a signal ...</strong> [... ]Worker::doWork() //<strong> recursive into the slot function! </strong> |
Of course, there is a solution to this situation: we can QCoreApplication::processEvents() pass in arguments when the function QEventLoop::ExcludeUserInputEvents is called, meaning that you do not distribute the user input events again (these events remain in the event queue).
Fortunately, there is no problem with deleting events (that is QObject::deleteLater() , events that are added to the event queue by a function). This is because the delete event is handled by another mechanism. Delete events are processed only if the event loop has a relatively small "nesting", rather than the deleteLater() loop that called the function. For example:
C + +
| 1234 |
qobject *object = new Span class= "crayon-v" >qobject object->deletelater qdialog dialog dialog. Exec |
This code does not create a wild pointer (note that QDialog::exec() the call is nested within the deleteLater() event loop where the call is located). It QEventLoop is similar by entering the local event loop. In Qt 4.7.3, the only exception is to call a function directly without an event loop, and then the deleteLater() first event loop that goes in gets the event and then deletes the object directly. This is reasonable, however, because Qt would not have known the "external" event loop that would perform the delete operation, so the first event loop would have deleted the object directly.
Qt Learning Path: threading and Event loops