65.9K
CodeProject 正在变化。 阅读更多。
Home

单线程并发模型设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (7投票s)

2009年7月12日

CPOL

20分钟阅读

viewsIcon

34337

downloadIcon

376

一个支持多组件单线程并发的轻量级库。

引言

本文正如其标题所示,描述了一种为具有许多组件的复杂程序提供所谓“单线程并发”的方法。首先,我们将讨论并发并区分其不同类型。特别是,我们将看到“并发”不是“多线程”的同义词,并且在许多情况下(实际上是大多数情况),单线程并发是可行的。

我在这篇文章中想要描述的方法是我几年前实现的,从那时起,我用它编写了几十个应用程序,其中一些相当复杂。而且,它被证明非常方便和灵活。

我一直觉得有必要写一篇关于这个主题的文章,实际上,我很久以前就应该发布它了。直到今天,**我从未见过正确设计的具有并发功能的复杂应用程序**。这听起来可能有些夸大,但不幸的是,这是可悲的事实。我曾在几家公司工作过,无论在哪里,我都看到过被线程严重超载的微型“怪物”,却没有过多考虑同步、死锁预防等问题。

并发

首先,让我们在不深入理论的情况下,将“并发”和“多线程”解耦。

有些操作需要很长时间才能完成,在此期间,我们希望能够做其他事情;这就是需要并发的地方。让我们看看导致操作耗时的两个主要原因:

  1. 涉及大量计算的操作。它们实际上需要**CPU时间**。
  2. 依赖网络/硬件/用户交互等的操作。它们需要时间是因为它们需要**等待**某些事情发生。

对于第一类操作,多线程是首选。这是因为在多处理器(MP)系统上,几个线程实际上可以同时执行。这样,可以实现更高的整体性能。

然而,对于第二类操作,多线程是无用的:创建多个线程来等待某些条件发生(例如网络事件或用户交互)并不能帮助这些条件更快地发生。事实上,单个线程可以等待任何指定的条件发生,并为其中任何一个做必要的事情。

这实际上是最常见的情况:99%的应用程序在99%的时间内什么都不做(至少它们应该如此)。为此类应用程序创建多个线程只是为了将它们全部置于挂起状态,这不利于整个系统的性能(每个线程上下文对系统都有显著的开销,上下文切换等)。此外,多线程应用程序需要一定的技巧才能正确编写,以避免各种死锁、同步问题或竞态条件。因此,这里首选单线程方法。

然而,单线程方法对于具有不同组件的复杂应用程序来说,实现起来是一个挑战。当所有这些组件在不同的线程中工作时——没有问题,每个组件在自己的线程中做它想做的事情,整个事情“并行”工作(如前所述,实际上大多数时候它根本不工作)。然而,如果我们希望这些组件在单个线程中工作——这意味着特定组件不允许调用等待函数(例如`WaitForXXXX`),因为这样做会禁用其他组件。相反,它应该像一个状态机一样工作——响应某个事件并立即返回控制。而且,如果它确实调用了等待函数——它应该等待此线程中所有组件的**所有**事件。此外,它应该将控制权传递给其事件被发送信号的组件。

总之,我们来说明哪些情况下多线程是首选:

  • 需要大量的CPU时间。
  • 需要调用笨拙的同步(阻塞)API。
  • 将组件转换为状态机过于复杂。这理论上可以通过使用**纤程(fibers)**实现,但我们在此不讨论。

在所有其他情况下,应首选单线程。

本文的目的是展示一个**ThreadCtx**库,它实现了上述设计,从而使多个组件可以在单个线程中工作,而**无需**直接交互。我们将看到它是一个非常轻量级的库,并且施加的限制最小。最重要的是它不是一个会夺取你控制权的_运行时_。你实际上一直都掌控着程序的执行。这个库唯一限制的是阻止组件响应其事件,并且所有组件都保证会收到它们的通知并且不会相互阻塞。为了使其正常工作,所有组件**必须**通过`ThreadCtx`进行所有调度和等待,并且**绝不能**直接调用等待函数。其余的由ThreadCtx库负责。

等待?等待什么?

在讨论`ThreadCtx`的API和实现之前,我们先讨论一些关于等待条件和函数的具体细节。有大量的Win32函数可以将线程置于挂起状态(即等待),直到满足某些条件。首先,我们将对这些函数进行分类,并查看它们实际等待什么。

  1. 定时器(`Sleep`,许多其他等待函数也接受超时)
  2. Win32可等待句柄(`WaitForSingleObject`)
  3. Win32窗口消息(`GetMessage`、`WaitMessage`等)
  4. APC完成(`SleepEx`)
  5. I/O完成端口(`GetQueuedCompletionStatus`)

最后一种类型(I/O完成端口)是一种高级的可等待对象。它专门设计用于多线程,我们在此不讨论。

因此,从操作系统的角度来看,一个线程可以等待上述四种条件中的一种(或多种)。幸运的是,有Win32 API函数可以结合不同类型的条件进行等待,这样当我们确实需要等待多种不同类型的条件时,我们总可以使用适当的API函数:

  • `Sleep` - 超时
  • `WaitForMultipleObjects` - 超时、可等待句柄
  • `MsgWaitForMultipleObjects` - 超时、可等待句柄、消息
  • `SleepEx` - 超时、APC
  • `WaitForMultipleObjectsEx` - 超时、可等待句柄、APC
  • `MsgWaitForMultipleObjectsEx` - 超时、可等待句柄、消息、APC

所以,理论上,等待这些事件中的任何一个都没有问题,并且需要等待这些事件的几个对象可以在一个线程中得到服务。然而:

  • `WaitForMultipleObjects`(及类似函数)对句柄数量有限制,即`MAXIMUM_WAIT_OBJECTS`,目前定义为64。但这可以绕过:`ThreadCtx`可以创建内部线程来等待更多,或者`WaitForMultipleObjects`可以以timeout=0多次调用所有可等待句柄,如果所有对象都没有被信号通知,则调用`Sleep(0)`或`SwitchToThread()`。因此,对于`ThreadCtx`的API设计,此限制无关紧要。它应该保证任意数量的可等待句柄的正确调度,而`ThreadCtx`的消费者应该记住,如果可等待句柄过多,整体性能可能会下降;如果需要许多同步对象,则应首选另一种机制。例如,具有许多异步I/O对象(如套接字)的应用程序应使用APC机制(没有此限制)来跟踪I/O完成。
  • 等待函数只能接收一个超时(而几个对象需要用不同的定时器进行调度)。有API函数可以启用多个定时器(例如`SetTimer`、`CreateWaitableTimer`等),但它们会施加其他限制。因此,`ThreadCtx`将负责在内部管理定时器,并确保以适当的(最近的)超时调用等待函数。

ThreadCtx 设计

如我们所说,对象将不会直接调用等待函数;相反,它们将使用`ThreadCtx` API来完成。`ThreadCtx`的每线程状态将存储在线程**TLS**中。对于不熟悉TLS的人来说:它是一种“线程局部存储”,类似于全局变量;然而,它们对于每个线程都是唯一的。TLS是一种非常轻量级且快速的机制;从性能角度来看,访问TLS变量与访问常规变量的性能相当。

`ThreadCtx`的主要API元素是`Schedule`。它是一个基类,代表任何可等待的条件,对于四种条件类型(定时器、Win32句柄、Win32消息、APC)中的每一种,都有一个适当的派生类,带有适当的额外方法。

class ThreadCtx {
public:
    class Schedule
    {
    public:

        bool Wait();

        bool get_Signaled() const;
        void put_Signaled(bool bVal);
        __declspec(property(get=get_Signaled,put=put_Signaled)) bool _Signaled;

        virtual void OnSchedule(); // default sets Signaled

    };
};

正如我们所看到的,`Schedule`具有以下特点:

  1. `Signaled`属性。当设置时,这意味着可等待条件已满足,但尚未处理(稍后详细介绍)。
  2. `OnSchedule`虚函数。当`ThreadCtx`检测到可等待条件满足时,会调用此函数。如果被重写,它可以在原地处理事件。如果它设置了`Signaled`属性(就像默认实现所做的那样),这意味着原地处理不足以解决问题,控制权应返回给相应的组件(稍后详细介绍)。
  3. `Wait`函数。这是一个阻塞函数(也是唯一一个阻塞函数),它等待此`Schedule`所代表的条件。如果此条件满足,函数返回`true`。但是,此函数实际上会考虑所有其他`Schedule`的可等待条件,如果其中任何一个在之前满足,并且其原地处理不足(`Signaled`已设置),则`Wait`函数返回`false`。

现在,更详细的解释。主要函数是`Wait`。实际上,这是`ThreadCtx`唯一参与的地方。此函数执行以下操作:

  1. 检查是否有已发信号的调度。如果有,则转到步骤(5)。
  2. 查看当前线程中实例化的**所有**调度,并执行任何请求条件的等待(通过上面讨论的Win32 API等待函数之一)。
  3. 等待函数返回后,识别已满足的条件,并处理相应的调度(调用相应的`OnSchedule`)。
  4. 转到步骤(1)。
  5. 如果调用`Wait`的调度处于已发送信号状态,则将其重置(将`Signaled`设置为`false`)并返回true。否则,返回`false`。

这听起来可能很复杂,但实际上这里没有什么复杂的。其思想是,`Wait`尝试等待被调用调度的条件。如果它返回`true`,则条件满足,调用者可以继续执行。但是,如果`Wait`返回`false`,则表示条件未满足,但**无法再等待**,因为另一个条件已满足但尚未处理。

`OnSchedule`可以被重写。在那里,当它被调用时,你可以在不设置`Signaled`属性的情况下做一些事情。这将意味着你已经处理了条件,并允许某个调度(无论哪个)的`Wait`调用者继续等待该条件。这种行为使得在等待其他事情发生时做一些事情(即后台处理)成为可能。

正如我们所说,共有四种条件类型。它们定义如下:

class ThreadCtx {
public:
    class ScheduleHandle
        :public Schedule
    {
    public:
        void put_Handle(HANDLE hVal);
        HANDLE get_Handle() const { return m_hHandle; }
        __declspec(property(get=get_Handle,put=put_Handle)) HANDLE _Handle;
    };

    class ScheduleTimer
        :public Schedule
    {
    public:
        void KillTimer() { _Timeout = INFINITE; }

        ULONG get_Timeout() const;
        void put_Timeout(ULONG);
        __declspec(property(get=get_Timeout,put=put_Timeout)) ULONG _Timeout;

        bool IsTimerSet() const { return 0 != m_Key; }
    };

    struct NeedApc {
        NeedApc();
        ~NeedApc();
    };

    struct NeedMsg {
        NeedMsg();
        ~NeedMsg();
    };
  • `ScheduleHandle`是一个Win32可等待句柄条件。当`Handle`设置为非`NULL`值时,它会附加到调度机制,以便`Wait`函数能够感知它。
  • `ScheduleTimer`是一个单次定时器,具有Win32等待函数的标准精度(而不是高精度多媒体定时器)。当`_Timeout`设置为除`INFINITE`以外的值时,它会附加到调度机制,以便`Wait`函数能够感知它。
  • `NeedApc`表示希望等待一个排队的APC完成。每当这个对象在线程中实例化时,`ThreadCtx`就会知道这一点,因此随后调用的`Wait`函数将选择支持APC的Win32等待函数。**注意**:`NeedApc`不是从`Schedule`派生的。这是因为APC事件的处理由操作系统自动完成(APC例程被调用)。
  • `NeedMsg`——与`NeedApc`相同。它表示希望等待消息队列中的Windows消息,这会告诉`Wait`函数选择支持消息的Win32等待函数。每当消息队列中检测到消息时,它们会自动调度(通过`DispatchMessage`),实际的消息处理由相应的窗口完成。

下面,我们将通过几个例子来阐明这一点。

ThreadCtx::ScheduleHandle abortSched;
abortSched._Handle = handleThreadStop; // this will be set
// if we should abort (by another thread for instance)

// Say, we have timeout for the I/O:
ThreadCtx::ScheduleTimer timeoutSched;
timeoutSched._Timeout = 5000;

CallIo();

// Perform some network I/O
void CallIo()
{
    // Create a socket, associate it with the event
    // handle (WSAEventSelect), and issue an operation
    // ...
    // Now wait for its completion
    ThreadCtx::ScheduleHandle socketIoSched;
    socketIoSched._Handle = handleSocketEvent;
    // this will be set once the I/O is completed

    if (!socketIoSched.Wait())
        return; // abort/timeout

    // Ok, the condition is satisfied (I/O completed).
    // Continue as planned...
}

在此示例中,`CallIo`执行一些阻塞操作,该操作可能因不同条件而被中止:要么是超时到期,要么是中止事件被发出信号。最重要的是,`CallIo`不必了解所有“中止”条件;它只是尝试执行其操作,中止会自动发生。

// In addition we want to do something while I/O is pending
class PeriodicalWork :public ThreadCtx::ScheduleTimer
{
public:
    virtual void OnSchedule()
    {
        // Do something
        // ...

        _Timeout = 3000; // reschedule our execution
    }
};

class SomeEventHandler :public ThreadCtx::ScheduleHandle
{
public:
    virtual void OnSchedule()
    {
        // Do something
        // ...
    }
};

PeriodicalWork worker1;
worker1._Timeout = 2000; // first scheduling will occur after 2 sec,
// then it'll reschedule itself for 3 sec

SomeEventHandler worker2;
worker2._Handle = /* ... */;

// Pass control to CallIo, our 'workers' will do things in background 
CallIo();

在此示例中,两个对象可以在另一个阻塞操作进行时处理某些事件(Win32可等待句柄和定时器),而该操作无需了解这一点。

PeriodicalWork worker1;
worker1._Timeout = 2000; // first scheduling will occur after 2 sec,
// then it'll reschedule itself for 3 sec

SomeEventHandler worker2;
worker2._Handle = /* ... */;

ThreadCtx::Schedule term;
term._Handle = /* ... */;
term.Wait();

在此示例中,我们初始化了几个对象,然后等待终止句柄被发送信号。在此期间,所有对象都将处理其事件。

一些提醒

这种设计在某种程度上与Windows消息系统相似。也就是说,你可以创建几个窗口并“运行消息循环”,它是一个`GetMessage`和`DispatchMessage`的循环(可选`TranslateMessage`、空闲处理和其他事情),所有窗口都将收到它们的消息,它们的窗口过程将处理它们。这样的窗口可以异步处理一些事件,彼此之间没有直接交互。此外,窗口可以使用每窗口定时器执行一些定时任务。此外,还有一些阻塞调用,例如`MessageBox`、`DialogBoxParam`、`GetOpenFileName`等。它们在MFC中被`AfxMessageBox`、`DoModal`等包装。这些都是阻塞调用;然而,它们也运行消息循环,因此线程创建的所有窗口也处理它们的消息。

这与我们的设计类似,尽管相当笨拙。创建一个窗口对象(在某种程度上)等同于在我们的设计中创建一个`Schedule`;窗口过程的角色现在由虚函数`OnSchedule`扮演。主要区别在于,当你创建一个窗口时,你会收到世界上所有的消息,其中大部分你只是传递给`DefWindowProc`,而我们的调度只接收一个特定的通知。

调用某个“阻塞”函数(例如`DialogBoxParam`)等同于我们调度的`Wait`函数。正如在Windows API中调用`DialogBoxParam`不会阻止其他窗口接收其消息一样,在我们的API中,在一个`Schedule`上调用`Wait`也不会禁用所有其他`Schedule`。

**注意**:我们的`Wait`可能会被中止(如果另一个`Schedule`被调度并且其`Signaled`属性被设置),这在Windows中也是这样设计的!在Windows中,当你调用`GetMessage`时,如果遇到的消息是`WM_QUIT`,它将返回0,并且按照惯例,相应的消息循环应该停止。你调用`PostQuitMessage`函数,它会自动“中止”阻塞调用。这意味着,`DialogBoxParam`或`MessageBox`通常返回传递给`EndDialog`的结果;但是,如果遇到`WM_QUIT`消息,它们将返回-1以指示“中止的阻塞操作”。

在Windows 3.11时代,事情就是这样运作的。事实上,任何人都可以通过执行一些长时间的操作(例如繁重的计算)或仅为特定窗口调用`GetMessage`(实际上,我不知道为什么允许这样做)来阻止其他人执行,但这被认为是不好的,当然也不受欢迎,每个人都意识到了这一点。当Win32带着所有线程、同步对象、APC队列和许多只等待特定事物的不同等待函数到来时,事情变得一团糟。实际上,等待任何东西的函数(`MsgWaitForMultipleObjectsEx`)直到Windows 98才添加。

我们的设计并非全新。它只是创建了一个本应随Win32而来的约定。此外,与笨拙的消息子系统不同,我们对事物的行为方式有非常明确的定义。而且,我们没有一个笨重的窗口对象来负责所有事情,而是拥有轻量级的原语,每个原语负责一件特定的事情。

ThreadCtx 实现

该实现非常轻量级。与调用任何等待函数(涉及内核模式事务)相比,其额外开销可以忽略不计。

`ThreadCtx`对象必须在每个可能访问它的线程中实例化(即使用`Schedule`的线程)。例如,它可以在线程“开始”时在栈上创建。在构造函数中,它将其指针保存在TLS中,并且涉及`Schedule`的每个函数都会访问它。

**注意**:这可以可选地更改。`ThreadCtx`对象可以在每个线程中按需创建。(也就是说,对`Schedule`的第一次调用会自动为给定线程创建`ThreadCtx`)。这种方法也需要每线程的去初始化。

`ThreadCtx`包含以下内容:

  1. 指向 N`ScheduleHandle` 对象的指针列表。每个`ScheduleHandle`对象在将其`Handle`设置为非`NULL`值时会自动插入到此列表中,并在将其设置为`NULL`时从该列表中删除(显然,它确保在析构函数中从该列表中删除)。
  2. `ScheduleTimer` 对象的二叉树。当`ScheduleTimer`对象的`_Timeout`设置为除`INFINITE`以外的值时,它会插入到该树中,其中树的键是此定时器的“唤醒”时间(`GetTickCount()` + `_Timeout`)。通过这种方式,在调用任何等待函数之前,`ThreadCtx`可以在对数时间内识别最近的定时器(这当然很快)。它正确处理了计数器回绕,并检测到已超时的定时器。
  3. 已实例化的`NeedMsg`对象计数器和已实例化的`NeedApc`对象计数器。这些对象在构造函数/析构函数中增加/减少这些计数器。通过这种方式,在调用等待函数之前,`ThreadCtx`知道等待是否应该支持消息/APC(相应的计数器非零)。
  4. 处于`Signaled`状态的调度计数器。状态改变时,每个调度都会自动增加/减少此计数器。因此,`ThreadCtx`在调用等待函数之前总是知道是否存在已发送信号的调度。

当你调用`Schedule::Wait`时,它会访问`ThreadCtx`并在一个循环中调用等待函数并调度适当的`Schedule`对象,直到其中一个设置了`Signaled`属性。每次调用等待函数都按以下顺序进行:

  1. 在二叉树中找到最早的定时器。如果此定时器已超时,则将其从树中删除,安排它,然后返回。如果它未超时,则计算其超时时间。而且,如果树为空,则超时时间为`INFINITE`。
  2. 检查已实例化的`NeedMsg`对象和`NeedApc`对象的计数器。根据此,选择适当的等待函数。
  3. 在一个数组中准备所有Win32可等待句柄,该数组可用作所选等待函数的参数。如果对象数量不超过`MAXIMUM_WAIT_OBJECTS`限制,则使用所有对象调用等待函数。否则,多次调用它(每次将不同的句柄放入数组中),超时时间为0,如果等待函数在超时时返回,则调用`Sleep(0)`。
  4. 现在,分析等待函数的返回值。如果它返回句柄或超时,则调度相应的`Schedule`。如果它返回Win32消息,则执行`PeekMessage`/`DispatchMessage`循环。

可选地,可以修改`ThreadCtx`以执行APC完成和Windows消息的附加步骤。例如,我们可能希望为窗口消息(或每线程消息处理器)设置一个全局的“预翻译器”。

与UI混合使用

理论上,这是可以实现的。这意味着整个应用程序可以单线程完成,包括UI。为了使其正常工作,需要避免直接调用阻塞函数,例如`GetMessage`和`WaitMessage`。特别是,程序的消息循环应该重写:

// Traditional message loop:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0)
DispatchMessage(&msg);

// ThreadCtx-aware loop. Like in the previous example
// the program termination is triggered by PostQuitMessage:
//
// NOTE: The ThreadCtx class must be adjusted to have
// a global 'messages pre-translator': ScheduleMsg.
class TermSched :public ThreadCtx::ScheduleMsg
{
public:
    virtual void TranslateMsg(MSG& msg)
    {
        if (WM_QUIT == msg.message)
            _Signaled = true;
    }
};

TermSched sched;
sched.Wait(); // Message loop executes here


// Another variant of ThreadCtx-aware.
// Program termination triggered directly:
LRESULT CALLBACK MainWndProc(UINT uMsg, /* ... */)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        // PostQuitMessage(0); - no more relevant
        globalTermSched._Signaled = true;
        return 0;
    // ...
}

ThreadCtx::NeedMsg careMsg; // otherwise messages queue is ignored

ThreadCtx::Schedule globalTermSched;
globalTermSched.Wait(); // Message loop executes here

注意:在最后一种情况下,我们使用了基类`Schedule`,而不是其派生类。此调度没有固有的(内置的)可等待条件,它只能被显式地发送信号。它的主要用途是执行可以被显式中断的“消息循环”。

所以理论上,包括UI在内的一切都可以在单个线程中完成。然而,在具有繁重UI的大型项目中,要保证所有阻塞函数都以`ThreadCtx`的方式执行而不是直接执行,可能是一个挑战。大型UI项目通常使用某些运行时(例如MFC)编写,这些运行时完全隐藏了消息循环。理论上,你可以重写消息循环(在MFC中,可以重写`CWinThread::Run`、`CWinThread::PumpMessage`),但仍然有风险,天知道有多少这样的地方。而且,很容易搞砸:一次`MessageBox`调用就足以关闭整个机制,直到用户关闭消息框。

另一个限制是,无法使用可能执行其消息循环的标准UI组件。这涉及到所有标准Windows对话框,例如`GetOpenFileName`、`ChooseFont`、`ChooseColor`,各种ActiveX控件,以及`scanf`、`getch`(即easy-win)等“控制台”函数。

理论上,有方法可以“截取”这些函数调用(例如J. Richter的替换可执行模块中导入节地址的方法),并以`ThreadCtx`感知的方式执行所需操作。但所有这些方法都很棘手,并且不保证始终有效。

此外,“富”UI的项目往往很重。也就是说,UI在某些情况下实际上消耗大量CPU时间来处理。所以:

  • 将“简单”(且控制良好)的UI与其他内容混合没有问题。
  • 对于“富”UI,尤其是与某些运行时一起使用时,可能会很复杂。最好不要混合。

编写复杂对象

到目前为止,我们已经按原样使用调度,或者继承它们以重写`OnSchedule`以执行某些内容的就地处理。但是,我们所有的调度都是原语——每个调度只处理一个特定事件。如果我们以依赖的方式处理不同类型的各种事件,该怎么办?有一些模式可以做到这一点:

// This macro gives a 'child' object an inline function that
// returns a reference to its containing object.
#define NESTED_OBJ(outer_class, this_var, outer_name) \
    outer_class& Get##outer_name() { return * (outer_class*) 
        (((char*) this) - offsetof(outer_class, this_var)); } \
    __declspec(property(get=Get##outer_name)) outer_class& ##outer_name; \


// This macro implements a child object of the specified type, which
// overrides its OnSchedule function and redirects it to the specified member
// function of its containing object
#define NESTED_SCHEDULE(outer_class, this_var, sched_class, outer_func) \
    class SchedNested_##this_var :public sched_class { \
        NESTED_OBJ(outer_class, this_var, _parent) \
        virtual void OnSchedule() { \
            _parent.outer_func(); \
        } \
    } this_var;

// Using those macros let's write an object that responds on 4 events:
class ComplexWorker
{
    // ...
    NESTED_SCHEDULE(ComplexWorker, m_schedMyEvent1, Thread::ScheduleHandle, ProcessEvt1)
    NESTED_SCHEDULE(ComplexWorker, m_schedMyEvent2, Thread::ScheduleHandle, ProcessEvt2)
    NESTED_SCHEDULE(ComplexWorker, m_schedMyTimer1, Thread::ScheduleTimer, ProcessTimer1)
    NESTED_SCHEDULE(ComplexWorker, m_schedMyTimer2, Thread::ScheduleTimer, ProcessTimer2)

public:
    // ...

    void ProcessEvt1()
    {
        // process this event, eventually decide to re-schedule other schedules
        m_schedMyTimer1.KillTimer();
        m_schedMyTimer2._Timeout = 1000;
  }

    // ...
};

这可能看起来很复杂;然而,这里没有什么复杂的。神奇的宏`NESTED_SCHEDULE`声明了一个从你作为参数传递的调度类型(宏的第三个参数)派生出来的类,它重写了`OnSchedule`,并在其中调用了外部对象的函数(第四个参数)。另一个神奇之处在于声明的类如何拥有指向外部对象的指针。这是由于`NESTED_OBJ`宏,它根据编译时的偏移量为内部对象提供了指向外部对象的指针。

补充说明

首先,值得一提的是,重入没有限制。这意味着,您可以在另一个`Wait`(可能在另一个`OnSchedule`中执行,依此类推)的上下文中执行的`OnSchedule`中调用`Wait`。但是,您应该小心重入,因为程序流可能会变得复杂。另外,请注意,如果一个`Wait`在另一个`Wait`内部执行,并且外部`Wait`的`Schedule`在内部`Wait`之前被发出信号,则内部`Wait`将停止等待并返回`false`。因此,重入是一个复杂的问题,如果使用,应进行严格控制。

正确的模式应该是状态机。也就是说,大多数对象根本不应该调用`Wait`。它们应该在`OnSchedule`中完成所需的一切并立即返回。每个线程可以实现一个“算法”对象,但不能更多。

另外值得注意的是,整个机制是异常安全的。这意味着,当抛出异常时,它会自由地穿过所有`Wait`/`OnSchedule`作用域,并直接传播到捕获块。而且,如果栈上声明的调度在途中,它们会被销毁(像所有C++对象一样),并从调度机制中移除(在析构函数中)。所以没有问题。

所有调度都可以分配在栈上或堆上,这无关紧要。当然,使用栈内存要快得多,而且由于调度是非常轻量级的对象,最好将它们分配在栈上。

应用程序可以是多线程的,每个线程都可以使用`ThreadCtx`库(它维护每线程状态)。然而,对特定调度的所有操作**必须**在同一个线程中完成!在多个线程中处理同一个`Schedule`对象是非常不道德且绝对不可能的。

为了在DLL中使用`ThreadCtx`,它**必须**进行修改。它使用TLS——因此它需要全局TLS索引。目前,这是通过使用`__declspec(thread)`语义来修饰`ThreadCtx`的每线程指针来实现的。但这对于DLL来说无法正常工作。相反,对TLS的访问必须是显式的。也就是说,必须显式分配一个全局TLS索引(`TlsAlloc`)。它必须与DLL共享。

结论

嗯,这篇文章首次阅读时可能看起来很复杂,尤其是实现部分。但实际上,它并不那么复杂。看看实现代码:它只有大约350行代码!

还有一件事:如果您真的打算查看代码级别——我建议阅读以下文章:

顺便说一句,这很好地展示了上述容器类的灵活性和轻量级特性。

你也可以不深入了解实现细节,只是尝试使用它。我希望提供的例子足以理解使用`ThreadCtx`的原理。

正如我所说,我使用这种设计编写了许多应用程序和Win32服务,事实证明它非常实用。我将尽快发布一些演示。

欢迎批评。

© . All rights reserved.