You’re doing it wrong…(QThread with SIGNAL-SLOT)

You’re doing it wrong…

We use IRC extensively to communicate amongst ourselves as well as with the community. I hang out on the #qt channel on the Freenode network and help people with questions when I can. A common question that I see (and that makes me cringe at the same time) has to do with understanding threading with Qt and how to make some code they’ve written work. People show their code, or examples based on their code, and I often end up thinking:

You’re doing it wrong


I know this is a bit of a bold thing to say, perhaps a bit provocative, but at the same time, I can’t help but think that the (hypothetical) class below is an incorrect application of object-oriented principles as well as incorrect usage of Qt.

class MyThread : public QThread
{
public:
    MyThread()
    {
        moveToThread(this);
    }

    void run();

signals:
    void progress(int);
    void dataReady(QByteArray);

public slots:
    void doWork();
    void timeoutHandler();
};

My #1 biggest gripe with this code ismoveToThread(this); I see so many people using this without understanding what it does. What does it do, you ask? ThemoveToThread() function tells Qt to ensure that event handlers, and by extension signals and slots, are called from the specified thread context. QThreadis the thread interface, so we’re telling the thread to run code “in itself”. We’re also doing thisbefore the thread is running as well. Even though this seems to work, it’s confusing, and not how QThread was designed to be used (all of the functions in QThread were written and intended to be called from the creating thread, not the thread that QThread starts).

My impression is that moveToThread(this); creeps into people’s code because they saw some blog somewhere that used it. A quick web search turns up several of these blogs, all of which follow the pattern in the class above:

  1. subclass QThread
  2. add signals and slots to do work
  3. test code, see that the slots aren’t called “from the right thread”
  4. ask google, find moveToThread(this); and comments that “it seems to work when I add this”

In my opinion, the problems started at step 1. QThread was designed and is intended to be used as an interface or a control point to an operating system thread, not as a place to put code that you want to run in a thread. We object-oriented programmers subclass because we want to extend or specialize the base class functionality. The only valid reasons I can think of for subclassing QThread is to add functionality that QThread doesn’t have, e.g. perhaps providing a pointer to memory to use as the thread’s stack, or possibly adding real-time interfaces/support. Code to download a file, or to query a database, or to do any other kind of processing should not be added to a subclass of QThread; it should be encapsulated in an object of it’s own.

Usually, this means simply changing your class to inherit from QObject instead of QThread and, possibly, changing the class name. QThread has astarted() signal that you can connect to when you need to perform some initialization. To actually have your code run in the new thread context, you need to instantiate a QThread and assign your object to that thread using themoveToThread() function. Even though you are still using moveToThread() to tell Qt to run your code in a specific thread context, we are keeping the thread interface separate. If necessary, it is now possible to have multiple instances of your class assigned to a single thread, or multiple instances of many different classes assigned to a single thread. In other words, it’s unnecessary to tie a single instance of a class to a single thread.

I take much of the blame for the confusion that comes with writing threaded Qt code. The original QThread class was abstract, so subclassing was necessary. It wasn’t until Qt 4.4 that QThread::run() gained a default implementation. Previously, the only way to use QThread was to subclass. With the addition of thread affinity and support for signal and slot connections between objects of different affinity, suddenly we have a convenient way of working with threads. We like convenience, we want to use it. Unfortunately, I realized to late that forcing people to subclass QThread actually made it harder than it needed to be.

I also take the blame for not getting up-to-date examples and documentation made to show people how to get the convenience with the minimum amount of headaches. For now, the best resource I can point at is
a blog I wrote several years ago.

Disclaimer: everything you see above is of course opinion. I’ve worked a lot on these classes, and have a fairly clear idea of how to use them and how not to use them.

 

转自:http://blog.qt.digia.com/blog/2010/06/17/youre-doing-it-wrong/

 

参考译文:

英文原文:http://labs.qt.nokia.com/2010/06/17/youre-doing-it-wrong/

我们之间经常使用IRC(网络聊天工具?)进行交流,社区也同样是这样。我在上 Freenode 网络的 QT 频道时会帮那些有问题的人解答。一个普遍的问题(也是我厌烦的问题)是如何理解QT多线程,还有如何使他们的代码工作。他们将他们的代码,或者基于他们代码的实例展示给我看,而我经常会回复他们一句:

你做错了。。。

我知道这个回答是有些无礼的,甚至是有些讽刺的。但我不禁想到,下面的这个类(假想)是一个不正确的多线程原则的应用,也是不正确的QT运用。

class MyThread : public QThread

{

public:

    MyThread()

    {

        moveToThread(this);

    }

    void run();

signals:

    void progress(int);

    void dataReady(QByteArray);

public slots:

    void doWork();

    void timeoutHandler();

};

我最不能忍受的代码是moveToThread(this); 我看到非常多的人在不理解这句话做了什么的情况下使用它。它做了什么?moveToThread函数让QT确保事件句柄和扩展的信号和槽已经被指定的线程上下文调用,因为QThread是一个线程接口,所以我们让线程的代码运行在“它自己里面”,我们也会在线程运行之前做这样的事。即使这样似乎能工作,但它令人迷惑,而且这也不是QThread设计的使用方式(所有QThread的函数编写时是希望在线程创建时被调用,而不是QThread类初始化时)。 我想,moveToThread(this);如此“恐怖的” (creeps) 写入到人们的代码中是因为他们看到有些博客也这么用。简单的网络搜索就会出现好几篇这样的博客,大家基本都是按照下面的方法做的:

  1. 继承QThread
  2. 添加一些消息和槽来完成工作
  3. 测试代码,发现那些槽并不是从“正确的线程”中调用的
  4. 问Google,找到moveToThread(this);方法,然后加了些注释:“当我添加这个的时候它似乎工作得很好

在我看来,问题出在第一步。QThread 是被设计来作为接口或者操作系统线程控制点(a control point to an operating system thread)使用的,不是一个放置你的多线程代码的地方。我们面向对象程序员继承一个类是为了扩展或者特殊化(specialize )基类的方法。我想,继承QThread的唯一正当的理由是添加QThread没有的方法,例如,也许要提供一个内存指针来实现线程栈,或者可能是添加实时接口支持。一段下载文件,查询数据库,或者任何其他处理的代码不应该继承QThread。它应该独立(encapsulated)在它自己的对象中。

通常,这意味着简单的从QThread修改你的代码到QObject,然后,可能需要修改你的类名。当你需要做些初始化时,你可以连接QThread的started信号。你需要实例化一个QThread并使用moveToThread(this);函数指派你的对象到此线程中,让你的代码真正运行在新的线程上下文中。即使你仍然是使用moveToThread函数让QT在指定的线程上下文中运行你的代码,但是我们保持了线程接口的独立性。如果有必要,现在就可能是时候将你的类的多个实例指派到一个单独的线程中,或者多个类的多个实例指派到一个单独的线程中了,换句话说,每个线程绑定一个实例也许是不必要的。(可以减少不必要的线程创建,译者注)

我常常埋怨那些混淆的线程相关的QT代码的编写。原始的QThread类是抽象的,所以继承是必要的。直到QT4.4,QThread::run()实现了一个默认的定义。在这之前,使用QThread唯一的方法是继承它。但是随着线程相关的添加和不同类别的对象之间的信号和槽的连接,我们突然有了一个方便的线程使用方法。我们喜欢简单,所以我们希望使用它。但不幸的是已经晚了。迫使人们继承QThread已经使它变得比预想的更难以改变。 我也埋怨没有一份最新的示例代码或者文档来告诉人们如何理由便捷的方法使他们从头疼中解脱出来。到现在为止,我发现的最好的资源是 一篇我以前写的博客

免责声明:上面你看到的所有内容是显而易见的观点。我做了很多关于这些类的工作,我很清楚的知道如何来使用或者如何不使用它们。

转自:http://www.cnblogs.com/tankery/archive/2011/03/09/2004561.html

—————————————————————————————————————————————————————————————————————
以下为网友回复:

  • dude says:

    Mind giving us an example of what “to do” rather than what “not to do” ?

  • Rupert Finklesteinsays:

    Brad, were you a woman, I would have brute forced the equivalent of “Open Sesame” out of you, beautiful man.

  • Will Stokes says:

    For the record I’m doing the exact same thing as scorp1us. I subclass QThread, reimplement the run method, and process data I store in private variables. I add public functions to the thread class to interact with it (e.g. schedule jobs), which is done by first getting a lock on a mutex for interacting with the data. I try to lock-unlock mutex’s as little as possible and for as short a time as possible. In some cases I have threads where jobs that come in can evict older jobs that are still in the queue. In such cases I can’t see how I could do what I do without subclassing QThread. So, I’d like to ask, why is the approach the Qt docs have advocated, and the one that I and scorp1us have been using, “wrong”? What is “wrong” about it? Since I’ve never used moveToThread (I don’t think I ever even noticed it before!) that there is nothing actually wrong about the approach I’ve always taken. I suspect there are different ways to accomplish the same thing, and recent changes in Qt mean for simpler threading contexts it’s possible to accomplish multi-threading without subclassing QThread.

  • MangoCat says:

    Ironic, in our larger (multi-programmer) apps, we use a “RegisteredThread” subclass of QThread. RegisteredThreads respond to a shutdown signal so they can all be stopped gracefully before program exit, it’s a fairly simple layer:

    RegisteredThread::RegisteredThread( const QString &name, QObject *parent ) : QThread( parent )
    { setObjectName( name );
    initRegisteredThread( parent );
    }

    void RegisteredThread::initRegisteredThread( QObject *parent )
    { shutdownNow = false;
    shutdownWhenReady = false;
    connect( &threadRegistry, SIGNAL( shutdown( int ) ), SLOT( shutdown( int ) ) );
    if ( !parent )
    { start();
    moveToThread( this );
    }
    }

    I just recently added that “moveToThread(this)” into the constructor code because, more often than not, it’s what the programmers want to do when they “create a new thread.”

    If QThread is truly meant as a thread manager, it would seem more appropriate to roll it up in QApplication. Back in the “old days” having to declare a separate “worker object” to be instantiated within the subclassed “run()” just seemed like tail chasing. The present system still seems a little awkward no matter how you approach it, but if I subclass RegisteredThread and use the default constructor, then my new class’ slots are executed in the new thread without further coding. Wasn’t that the old motto?

    Create more, code less.

  • Daniel says:

    Agree with scorp1us and Will Stokes: never even noticed moveToThread before, but things work great with subclassing and signals/slots. If you are subclassing and have to blindly use moveToThread to “solve” your problem, then the real problem is that your code / design is screwed up. Don’t see how the new suggested architecture is going to magically solve things for people with those sorts of problems.

    P.S. I hate that sleep is protected, it often comes in useful in quick-and-dirty test code and prototypes.

  • Philippe says:

    Hack to bypass Qt protected sleep:

    class QWrapperThread : public QThread
    {
    public:
    static void Sleep(unsigned long msecs) { QThread::msleep(msecs); }
    };

    inline void qSleep(uint t) { QWrapperThread::Sleep(t); }

  • NuShrike says:

    Funny how all my previous comments have been censored. Anyways, using moveToThread() imo is also a sign of bad design. Not allowing public usage of sleep() is so trivial in comparison.

  • aep says:

    I take the blame for linking to half correct blog entries out of laziness. I wrote my own correct entry now, because i didn’t realize you already did. Next time just poke me, so we can correct the channels factoid;)

  • Daniel Molkentinsays:

    @Philippe: This will fail on platforms where the compiler mangles the visibility into the symbol name, such as MSVC.

  • Philippe says:

    @Daniel: really? This does work on both Visual C++ 2008 and latest XCode/GCC.

    After all, this is strictly legal C++.

  • Harald Fernengelsays:

    @Daniel: The symbol is not renamed/removed, but simply called from a subclass. This works on all compilers. Philippe’s code is correct.

  • Wow, I go away for the weekend and come back to lots of good comments:P

    To set the record straight: I’m not saying sub-classing QThread is wrong. This is also how Java does it (sort of), and many people have been doing this with Qt for a long time.

    What I’m saying is “wrong” is subclassing QThread, adding signal and slot code to that subclass, and then calling moveToThread(this); in the constructor of that subclass. This causes people lots of confusion from my experience and observation on the #qt channel and the qt-interest mailing list.

    And yes, the current docs are still a bit lacking, something that I am fully aware of (and take responsibility for). I’m planning on sitting down and fleshing them out, showing both ways of using QThread (with and without subclassing).

  • Danny says:

    Every Qt book I’ve read (including the official one from Trolltech), the online Qt documentation and the course notes from KDAB specifically subclass from QThread and override run().

  • docman says:

    Epic fail…

    I thought Qt had a rockstar documentation dude on the team to deal with the docs, and the documentation part is something they’re proud of. I’m not so sure that the docs are their holy grail anymore. At least write in bold red letters “Example of how to misuse the Qthread” right next to the sample code so coders aren’t misguided.

  • Rick says:

    I think there’s not a big deal here. As far as I understand the poblem is not inheriting from QThread. Actually in the past you had to do that. Problem is inheriting from QThread _and_ defining new slots _and_ expecting them to run in the context of this QThread. It doesn’t, so people use the moveToThread kludge. I personally never did that but appearently there are wrong examples on the Net so this provacative blog entry is written.

    Now, stop acting like users of your library are dumb and give me my public sleep functions. Thank you:)

  • Roland Wolf says:

    History:
    Starting with Qt 4.4 QThread was not abstract anymore and QThread::run called exec(). This made it possible to instantiate a plain QThread and call the start() method which results in a new thread with an idling event loop. This means, Qt4.4 made it also possible to run a slot of any QObject derrived class in the context of the newly created thread by using QObject::moveToThread() and connect the slot to the started() signal of QThread.

    Summary:
    Subclassing QThread used to be the only way to create a thread with Qt till Qt 4.3
    Subclassing QThread is still a good way to create a thread.
    Subclassing QThread is still the only way to create a thread if no running event loop is wanted in the new thread.
    QObject::moveToThread() can be used to let a slot run in the context of another thread. This needs additional plumbing however.

  • Guillaume Lachaudsays:

    Ok great article, I have always been in trouble with threads in Qt. So does this mean that this code is enough to get me rid of freezing interface while connecting to a database ?

    BDDThread = new QThread();
    database->moveToThread(BDDThread);
    BDDThread->start();
    database->open();

    I have no warnings but the interface is still freezing (developping for Symbian on the Simulator).

    Thanks for clearing things out, the documentation should be updated regarding this point

  • Daniel says:

    Thanks to this I realized I was subclassing QThread incorrectly as my main thread (GUI) would block when it shouldn’t be. However trying to do it the “correct” way I found that my signals and slots no longer work.

    I have in my MainWindow.h header:

    public:
    MyObject o; //subclass of QObject(parent =0)
    QThread thread;

    In my MainWindow.cpp constructor:

    //setup connections between “this” (GUI widgets) and slots in “o”

    o.moveToThread(thread);
    o.start();

    If I comment out “o.moveToThread(thread);” my signals/slots work. When I move MyObject o to QThread thread, they no longer work. Am I missing something obvious? (Q_OBJECT macro is obv. declared in MyObject since signals/slots work prior to moving to thread).

  • Daniel says:

    Thanks to this I realized I was subclassing QThread incorrectly as my main thread (GUI) would block when it shouldn’t be. However trying to do it the “correct” way I found that my signals and slots no longer work.

    I have in my MainWindow.h header:

    public:
    MyObject o; //subclass of QObject(parent =0)
    QThread thread;

    In my MainWindow.cpp constructor:

    //setup connections between “this” (GUI widgets) and slots in “o”

    o.moveToThread(&thread);
    o.start();

    If I comment out “o.moveToThread(&thread);” my signals/slots work. When I move MyObject o to QThread thread, they no longer work. Am I missing something obvious? (Q_OBJECT macro is obv. declared in MyObject since signals/slots work prior to moving to thread).

  • One of the problems I see with this, many times, there are objects that if they are not part of the parent/child chain all owned by they same thread, you get errors.

    For example, QSqlDatasbase and QSqlQuery.

    Normally, I take care of this, by my QThread NOT allocating any objects, till the RUN method, as opposed to initializing things in the constructor.

    With this method, Im not sure when to do it..

  • aep says:

    Since some people appear still to be confused how to do it *right*:

    http://blog.exys.org/entries/2010/QThread_affinity.html

  • NuShrike says:

    Famous words now if you link it to Steve Job’s “You’re holding it wrong”.

  • Cata says:

    I have a question, being pretty new on Qt I don’t understand how events will be handled if I don’t subclass QThread.
    Let’s say I have a worker class “Worker” and a worker thread and in my Worker class re-implements eventFilter method. When I try to do something like this:
    Worker *workerObject = new Wroker();
    QThread workerThread;
    workerObject->moveToThread(&workerThread);
    workerThread.installEventFilter(workerObject);
    workerThread.start();

    I get “QObject::installEventFilter(): Cannot filter events for objects in a different thread.”
    After reading this post I assume that this should be the best way to go but even if I change thread affinity for my workerObject to workerThread by calling moveToThread method it doesn’t seem to work.
    Instead if I subclass QThread and have my Worker class inherit QThread, reimplement “event” method and moveToThread(this) in the ctor it works like a charm and I can handle events.
    Please shed some light on this.

  • Cata says:

    It seems that I misunderstood the event handling in Qt, and the solution is obvious, I just needed to re-implement “event” method in my Worker class and don’t need an event filter at all. Sorry for rushing in with questions before being properly informed.

  • Sebastian Barthsays:

    I like this new way to use QThreads!
    But there is a big problem with deleting worker classes:

    On the old way I did this:
    Code:
    // In the subclass of QThread “WorkerThread”:
    void run()
    {
    Worker worker;
    worker.connect(…..);

    exec();
    }
    Code:
    // In the creating object that belongs to another thread (maybe the main thread):
    void createThread()
    {
    this->workerThread = new WorkerThread(this);
    }

    Here the worker object are created inside the run() method and so on the new thread. When the created thread has finished, the run() method were left and on this way the worker object were deleted automatically since it was a local object ob the run() method.

    But now, on the right way, I have to create both, the Qthreads AND the worker object, inside the creating context with new. Otherwise they will be deleted when I leave the scope of the creation context:

    Code:
    // In the creating object that belongs to another thread (maybe the main thread):
    void createThread()
    {
    this->workerThread = new QThread();
    this->worker = new Worker();
    this->worker->connect(…);
    this->worker->moveToThread(this->workerThread);
    }

    Now, when the instanciating object gets deleted, I have to take care that the QThread object and the worker object gets deleted as well but at the earliest when I have stopped the started thread.

    I can use the parent child system (new QThread(this)) to take care that the QThread will be deleted when the creating object gets deleted. But I cannot use it for the worker class since it is on another thread!

    It’s very hard to deal with this structure from outside if you want to use the “new style of using QThreads”. You have to care about all started worker objects, their QThreads and their started threads to be deleted at the correct time!
    For this I have two suggestions that will solve this problems:

    1. It should be allowed for QThreads to have chrildren that are moved to them.

    2. The destructor of a QThread should send a quit() signal to “itself” and waits for its started thread to be finished. At the earliest when this thread has stopped, it starts to delete his childrens.

    In other words: The QThread is the bridge between threads in Qt. To be consequent on the Qt structure / coding style with its parent-child paradigm, the QThread should be the owner (and deleter) of its children that are handled by its created thread.
    If this is not going to be changed, the Qt parent-child structure is inconsequent.

    What do you think about this?
    Any ideas, problems, suggestions?

    Sebastian Barth

  • NuShrike says:

    So this is wrong then?

    class SizeLoaderThread : public QThread
    {
    Q_OBJECT
    public:
    SizeLoaderThread()
    {
    moveToThread(this);
    }
    }

    Guess where I found it … “photos” ofhttp://qt.gitorious.org/qt-labs/scenegraph

    So please, clean the Qt house before calling out everybody else.

  • Christophe Meessensays:

    This works indeed very well and makes programming with threads much simpler.

    My QThread object is now a member variable of the live object. In its constructor I now have

    // Set this object to use thread event loop
    moveToThread(&m_thread);

    // Make sure object is notified when thread is started
    connect( &m_thread, SIGNAL(started()), this, SLOT(started()));

    // Start thread
    m_thread.start();

    The public slot methods are the actions that may be invoked asynchronously by the main thread. Signals will be sent back on completion. This makes interaction very simple.

    There is apparently still a problem with this since I can’t prevent the main thread to call the slot method directly (without using a signal). The main thread will then execute the method synchronously which may not be the desired effect.

    I suspect there could be a way to detect if the caller of the live object method is the m_thread instance or another thread. If it is another thread, it should then signal the method invocation and return so that the method is executed only by the m_thread object. Is there a way to invoke a private slot without a connection ?

  • Ton van den Heuvelsays:

    I would really like to see a response of the author to Sebastian Barth’s comment, as I’m struggling with the same problem right now.

  • Christophe Meessensays:

    See my code example in the EDIT section of my question on StackOverflow.

    It shows an example of a live object with graceful termination when the destructor is called and a doTask() method that can be called directly by any thread and that will delegate its execution to the live object’s thread.

    http://stackoverflow.com/questions/3297456/invoke-slot-method-without-connection

    This is a good example (if correct) on how to add background task support in Qt.

    I used this in a Qt program controlling an X Ray scanner. The live object is for instance the detector driver object. Acquiring an image is a typical background task. Qt is a very impressive and powerful tool.

  • Jesse says:

    I think NuShrike makes a good point. I’ve been using this example:

    http://qt.gitorious.org/qt-labs/scenegraph/blobs/master/examples/photos/photos.cpp

    …for a while. My thought was that this was the “official” way to make it happen.

  • Jon says:

    If I want a thread with an event loop I *have* to subclass QThread, right? (since exec() is protected).

    Or is there is another ‘preferred’ way for this as well?

  • Andre says:

    Any chance that the documentation will actually be fixed? The docs for the current Qt 4.7 snapshot still look unchanged in this area.

  • Mathias says:

    This is pretty old by now but I’ve just found it and I have to say:Thank you! Now QThread makes so much more sense to me. My suggestion:Don’t call it QThread if it isn’t a thread. Call it QThreadControl and I would’ve been fine from the start. (Would’ve been too if I had read the Qt-sources earlier).

  • Andrew says:

    After scouring various links posted by Brad, I’ve come created an example. I think that everyone probably has this figured out already but just in case…
    Credit: My example is based on Brad’s .tar.gz example
    found here.


    class WriteFile : public QObject
    {
    Q_OBJECT

    public:
    WriteFile(QObject *parentIN = 0) : parent(parentIN)
    {
    ConnectSignalSlots();
    }

    void DoAll()
    {
    this->moveToThread(&threadObj);
    threadObj.start();
    }

    void ConnectSignalSlots()
    {
    QObject::connect(&threadObj, SIGNAL(started()), this, SLOT(CallFileWrite()));
    QObject::connect(this, SIGNAL(fileWrite(QString,QString)), this, SLOT(writingFile(QString,QString)));
    QObject::connect(this, SIGNAL(FinishWritingThread()), &threadObj, SLOT(quit()));
    QObject::connect(&threadObj, SIGNAL(finished()), this, SLOT(ThreadFinishedCheck()));
    }

    QThread threadObj;
    QObject *parent;

    public slots:
    void writingFile(const QString &textOUT, const QString &altTextOUT)
    {
    //do all the file writing
    printf("Yup.. this is writing stuff: %d, and %s.\n", textOUT.toStdString(), altTextOUT.toStdString());
    emit FinishWritingThread();
    }
    void CallFileWrite()
    {
    emit fileWrite("apples and bananas", "Karmin is hot");
    }
    void ThreadFinishedCheck()
    {
    printf("The thread has successfully been terminated.\n");
    }

    signals:
    void fileWrite(const QString &textIN, const QString &altTextIN);
    void FinishWritingThread();
    };

    int main()
    {
    WriteFile testThreadClass;
    testThreadClass.DoAll();
    }

  • Andrew says:

    Apologies for the incorrect “printf” usage.

  • Ricardo says:

    useless!
    :(
    T_T
    x_X

    I’ve tried using moveToThread(), not using it, subclassing and not subclassing… that slot wont get executed… God I miss java:(

    • JKSH says:

      Post your issue at the forums (http://qt-project.org/forums/); someone should be able to help you

      As a starting point, you need to do things in this order:
      1. Create the QThread
      2. Move the QObject to the QThread
      3. Connect the QObject’s slot to a signal
      4. Start the QThread
      5. Emit the signal that’s connected to the slot

  • Markus Goetz says:

    FYI, Olivier posted a reply to this blog post:-)
    “You were not doing so wrong”

    http://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html

  • Melvin says:

    Having read this I thought it was extremely informative. I
    appreciate you taking the time and effort to put this article together.
    I once again find myself spending way too much time both reading and leaving comments.
    But so what, it was still worth it!

  • 7 says:

    Last week I was doing some multithreading, following the “community guidelines” and noticed that threads and workers weren’t getting destroyed at all, and was just about to ask around the forum, but this article pretty much answers my questions.

     

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值