Windows with C++-----Windows 中线程和I/O 的演变

https://www.codeproject.com/Articles/11976/Win32-Thread-Pool

池的概念:资源重用,因为资源的创建费时费力,可重用的话,会带来很大的资源和效率提升。

上面的例子是:对于win32 线程池的封装的一个示例代码

当你开始一个新的项目,是否问过自己一个问题:这个程序是否是计算密集型或I/O 密集型?这是应该的,我发现,大部分的情况下,总是它们两个中的一个。您可能正在开发一个分析库,该库交付了大量数据并使一堆处理器忙碌,同时将其全部归结为一组聚合。或者你的代码可能会花费大部分时间等待事件发生,数据通过网络到达,用户点击某些内容或执行一些复杂的six-figered 手势(六指琴魔?)。此时,程序中的线程没有做太多的工作。当然,有些情况下,程序可能同时存在高I/O 和 高计算。SQL Server 数据库引擎就是这样的程序,但这对于今天的计算机编程中并不常见。通常,你的计划的任务是协调他人的工作。它可能是SQL 数据库通信的Web 服务器或客户端,将一些计算推送到GPU 或呈现一些内容供用户进行交互。鉴于所有这些不同的场景,如何确定程序所需的线程功能,以及哪些并发构建块是必须的或有用的?这是一个难以回答的问题,当你接近一个新项目时,你需要分析一些东西。但是,了解Windows和C ++中线程的演变是有帮助的,这样您就可以根据可用的实际选择做出明智的决策。

当然,线程对用户没有任何直接价值。如果你使用比别人的程序两倍的数量的线程,并不会使你的程序更帅气。使你用这些线程做到了什么效果真正的影响用户。为了说明这些想法以及线程随着事件的推移而演变的方式,让我举一个从文件中读取一些数据的例子。我将跳过C 和 C++ 库,因为它们对I/O 的支持主要是面向同步或阻塞I/O ,除非你想构建一个简单的控制台程序,否则这些库通常引不起关注。当然,使用它们并没有错。

单线程

从ReadFile 函数开始。在我可以开始读取文件内容之前,我需要一个文件的句柄,该句柄由及其强大的CreateFile 函数。

auto fn = L"C:\\data\\greeting.txt";
auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,
  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT(f);

这里CreateFile 的大部分参数不言自明,我们这里关心的是第二个,指示你需要从内核得到的I/O 行为,这里的FILE_ATTRIBUTE_NORMAL 意思为,普通的同步I/O ,之后,我们可以进行读取操作:

char b[64];
DWORD c;
VERIFY(ReadFile(f, b, sizeof(b), &c, nullptr));
printf("> %.*s\n", c, b);

这里,我们简单的进行了文件的打开和读取操作,这可能是最简单的文件操作,最后当然不能忘了CloseHandle(f)以释放文件资源。ReadFile 函数的最后一个参数是用于异步I/O 的,我们待会儿会进行介绍

双线程

上面的程序的I/O 很简单,对于很多的小程序比如,尤其console 的程序来说,都很有用,但规模太小。下面我们来创建一种,两个线程同时读取文件的情况(可能为了支持多用户,自己提需求)。

auto t = CreateThread(nullptr, 0, [] (void *) -> DWORD
{
  CreateFile/ReadFile/CloseHandle
  return 0;
},
nullptr, 0, nullptr);
ASSERT(t);
WaitForSingleObject(t, INFINITE);

CreateThread函数返回一个句柄,表示一个线程,然后我使用WaitForSingleObject函数等待。 读取文件时线程本身会阻塞。 通过这种方式,我可以让多个线程串联执行不同的I / O操作。 然后,我可以调用WaitForMultipleObjects等待所有线程完成。 还记得调用CloseHandle来释放内核中与线程相关的资源。

但是,这种技术可伸缩性差,当文件、用户太多,开销极大。 需要明确的是,并不是多个未完成的读操作都无法扩展。 恰恰相反。 但是线程和同步开销会破坏程序的可伸缩性。

回到一个线程

该问题的一个解决方案是通过异步过程调用(APC)使用称为可警告I / O的东西。在此模型中,您的程序依赖于内核与每个线程关联的APC队列。 APC有内核和用户模式两种类型。也就是说,排队的过程或函数可能属于用户模式下的程序,甚至属于某些内核模式驱动程序。后者是内核允许驱动程序在线程的用户模式地址空间的上下文中执行某些代码以便它可以访问其虚拟内存的简单方法。但是这个技巧也可供用户模式程序员使用。因为I / O在硬件方面基本上是异步的(因此在内核中是这样),所以开始读取文件的内容并在最终完成时让内核队列成为APC是有意义的。

首先,必须更新传递给CreateFile函数的标志和属性,以允许文件提供重叠的I / O,以便内核不对文件上的操作进行序列化。术语异步和重叠在Windows API中可互换使用,它们的含义相同。无论如何,在创建文件句柄时必须使用FILE_FLAG_OVERLAPPED常量:

auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,
  OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);

同样,这段代码片段的唯一区别是我用FILE_FLAG_OVERLAPPED替换了FILE_ATTRIBUTE_NORMAL常量,但运行时的差异很大。为了实际提供内核可以在I / O完成时排队的APC,我需要使用备用的ReadFileEx函数。虽然ReadFile可用于启动异步I / O,但只有ReadFileEx允许您在完成时提供要调用的APC。然后,线程可以继续并执行其他有用的工作,可能启动其他异步操作,而I / O在后台完成。

再次,由于C ++ 11和Visual C ++,lambda可用于表示APC。诀窍是APC可能想要访问新填充的缓冲区,但这不是APC的参数之一,并且因为只允许无状态lambda,所以不能使用lambda来捕获缓冲区变量。解决方案是将缓冲区悬挂在OVERLAPPED结构上,可以这么说。因为APC可以使用指向OVERLAPPED结构的指针,所以您可以简单地将结果转换为您选择的结构。图1提供了一个简单的例子。

struct overlapped_buffer
{
  OVERLAPPED o;
  char b[64];
};
overlapped_buffer ob = {};
VERIFY(ReadFileEx(f, ob.b, sizeof(ob.b), &ob.o, [] (DWORD e, DWORD c,
  OVERLAPPED * o)
{
  ASSERT(ERROR_SUCCESS == e);
  auto ob = reinterpret_cast<overlapped_buffer *>(o);
  printf("> %.*s\n", c, ob->b);
}));
SleepEx(INFINITE, true);

除了OVERLAPPED指针外,APC还提供了一个错误代码作为其第一个参数,并将复制的字节数作为其第二个参数。在某些时候,I / O完成,但为了使APC运行,必须将相同的线程置于可警告状态。最简单的方法是使用SleepEx函数,它在APC排队后立即唤醒线程并在返回控制之前执行任何APC。当然,如果队列中已经存在APC,则线程可能根本不会被挂起。您还可以检查SleepEx的返回值,以找出导致它恢复的原因。您甚至可以使用零值而不是INFINITE来刷新APC队列,然后再进行无延迟。

然而,使用SleepEx不是那么有用,并且很容易导致不道德的程序员轮询APC,这从来都不是一个好主意。如果您从单个线程使用异步I / O,那么这个线程也可能是您程序的消息循环。无论哪种方式,您还可以使用MsgWaitForMultipleObjectsEx函数来等待不仅仅是APC,并为您的程序构建更具吸引力的单线程运行时。 APC的潜在缺点是它们可能会引入一些具有挑战性的重新入侵错误,因此请记住这一点。

每个处理器一个线程

当您为程序找到更多内容时,您可能会注意到程序的线程正在运行的处理器变得更加繁忙,而计算机上剩余的处理器正在等待某些事情要做。尽管APC是执行异步I / O的最有效方式,但它们具有明显的缺点,即只能在启动操作的同一线程上完成。接下来的挑战是开发一种可以将其扩展到所有可用处理器的解决方案。您可能会想到自己正在进行的设计,也许可以协调多个具有可警告消息循环的线程之间的工作,但是您可以实现的任何事情都不会接近I / O完成端口的绝对性能和可伸缩性,这在很大程度上是因为它与内核的不同部分深度集成。

虽然APC允许在单个线程上完成异步I / O操作,但完成端口允许任何线程开始I / O操作并使结果由任意线程处理。完成端口是您在将其与任意数量的文件对象,套接字,管道等相关联之前创建的内核对象。完成端口公开一个排队接口,当I / O完成时,内核可以将完成数据包推送到队列,并且您的程序可以在任何可用线程上将数据包出列并根据需要对其进行处理。如果需要,您甚至可以将自己的完成数据包排队。主要的困难是绕过令人困惑的API。图2为完成端口提供了一个简单的包装类,清楚地说明了函数的使用方式以及它们之间的关系。

Figure 2 The Completion Port Wrapper
class completion_port
{
  HANDLE h;
  completion_port(completion_port const &);
  completion_port & operator=(completion_port const &);
public:
  explicit completion_port(DWORD tc = 0) :
    h(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, tc))
  {
    ASSERT(h);
  }
  ~completion_port()
  {
    VERIFY(CloseHandle(h));
  }
  void add_file(HANDLE f, ULONG_PTR k = 0)
  {
    VERIFY(CreateIoCompletionPort(f, h, k, 0));
  }
  void queue(DWORD c, ULONG_PTR k, OVERLAPPED * o)
  {
    VERIFY(PostQueuedCompletionStatus(h, c, k, o));
  }
  void dequeue(DWORD & c, ULONG_PTR & k, OVERLAPPED *& o)
  {
    VERIFY(GetQueuedCompletionStatus(h, &c, &k, &o, INFINITE));
  }
};

主要的困惑在于CreateIoCompletionPort函数执行的双重任务,首先实际创建一个完成端口对象,然后将其与重叠的文件对象相关联。完成端口创建一次,然后与任意数量的文件关联。从技术上讲,您可以在一次调用中执行这两个步骤,但这仅在您将完成端口与单个文件一起使用时才有用,乐趣在哪里?

创建完成端口时,唯一的考虑因素是指示线程计数的最后一个参数。这是允许同时使完成数据包出列的最大线程数。将此值设置为零意味着内核将允许每个处理器使用一个线程。

添加文件在技术上称为关联;要注意的主要事项是指示与文件关联的键的参数。因为您无法像使用OVERLAPPED结构那样在句柄末尾挂起额外信息,所以该键提供了一种方法,可以将某些特定于程序的信息与文件相关联。每当内核将与此文件相关的完成数据包排队时,也会包含此密钥。这一点特别重要,因为文件句柄甚至不包含在完成包中。(这里的键就是上面的ULONG_PTR& k)

正如我所说,您可以排队自己的完成包。 在这种情况下,您提供的值完全取决于您。 内核并不关心,也不会尝试以任何方式解释它们。 因此,您可以提供虚假的OVERLAPPED指针,完整的数据包将存储在完成数据包中。

但是,在大多数情况下,一旦异步I / O操作完成,您将等待内核对完成数据包进行排队。 通常,程序为每个处理器创建一个或多个线程,并在无限循环中调用GetQueuedCompletionStatus或我的dequeue包装函数。 您可以将特殊控制完成数据包排队 - 每个线程一个 - 当您的程序需要结束并且您希望这些线程终止时。 与APC一样,您可以从OVERLAPPED结构中挂起更多信息,以将额外信息与每个I / O操作相关联:

completion_port p;
p.add_file(f);
overlapped_buffer ob = {};
ReadFile(f, ob.b, sizeof(ob.b), nullptr, &ob.o);

这里我再次使用了原来的ReadFile 函数,但是我在它的最后一个参数提供了OVERLAPPED 结构。一个等待线程可能像下面将完成包从队列中去除。

DWORD c;
ULONG_PTR k;
OVERLAPPED * o;
p.dequeue(c, k, o);
auto ob = reinterpret_cast<overlapped_buffer *>(o);

线程池

如果您已经关注我的专栏一段时间,您会记得我去年花了五个月详细介绍了Windows线程池。 您也不足为奇,使用I / O完成端口实现相同的线程池API,提供相同的工作排队模型,但无需自行管理线程。 它还提供了许多功能和便利,使其成为直接使用完成端口对象的有吸引力的替代方案。 如果您还没有这样做,我建议您阅读这些专栏以快速了解Windows线程池API。相关资料:

 bit.ly/StHJtH.

至少,您可以使用TrySubmitThreadpoolCallback函数来获取线程池以在内部创建其中一个工作对象,并立即提交回调以供执行。它没有比这简单得多:

TrySubmitThreadpoolCallback([](PTP_CALLBACK_INSTANCE, void *)
{
  // Work goes here!
},
nullptr, nullptr);

如果您需要更多控制,您当然可以直接创建工作对象并将其与线程池环境和清理组关联。 这也将为您提供最佳性能。 当然,这个讨论是关于重叠的I / O,而线程池就是为此提供I / O对象。 我不会花太多时间在这上面,因为我已经在2011年12月的专栏“Thread Pool Timers and I / O”中详细介绍了它。

https://msdn.microsoft.com/magazine/hh580731

但下面的代码,给了一个新的示例

OVERLAPPED o = {};
char b[64];
auto io = CreateThreadpoolIo(f, [] (PTP_CALLBACK_INSTANCE, 
  void * b,   void *, ULONG e, ULONG_PTR c, PTP_IO)
{
  ASSERT(ERROR_SUCCESS == e);
  printf("> %.*s\n", c, static_cast<char *>(b));
},
b, nullptr);
ASSERT(io);
StartThreadpoolIo(io);
auto r = ReadFile(f, b, sizeof(b), nullptr, &o);
if (!r && ERROR_IO_PENDING != GetLastError())
{
  CancelThreadpoolIo(io);
}
WaitForThreadpoolIoCallbacks(io, false);
CloseThreadpoolIo(io);

鉴于CreateThreadpoolIo允许我将一个额外的上下文参数传递给排队的回调,我不需要将缓冲区从OVERLAPPED结构中挂起,尽管如果需要我当然可以这样做。 这里要记住的主要事项是必须在开始异步I / O操作之前调用StartThreadpoolIo,并且如果I / O操作失败或完成内联,则必须调用CancelThreadpoolIo,可以这么说。

快速和流体线程?(fast and Fluid Thread)

将线程池的概念提升到新的高度,Windows应用商店应用程序的新Windows API也提供了一个线程池抽象,尽管它的功能要少得多。 幸运的是,没有什么能阻止您使用适合您的编译器和库的备用线程池。 是否能让它超越友好的Windows Store curators是另一回事。 不过,Windows Store应用程序的线程池值得一提,它集成了Windows API for Windows Store应用程序所体现的异步模式。 使用C ++ / CX扩展提供了一个相对简单的API,用于异步运行某些代码:

ThreadPool::RunAsync(ref new WorkItemHandler([] (IAsyncAction ^)
{
  // Work goes here!
}));

从语法上讲,这非常简单。如果编译器可以从lambda自动生成C ++ / CX委托 - 至少在概念上 - 就像今天的函数指针一样,我们甚至可以希望在未来版本的Visual C ++中这会变得更简单。尽管如此,这种相对简单的语法仍然存在很大的复杂性。在较高的层次上,ThreadPool是一个静态类,从C#语言借用一个术语,因此无法创建。它提供了一些静态RunAsync方法的重载,就是这样。每个至少需要一个委托作为其第一个参数。在这里,我用lambda构建委托。 RunAsync方法还返回IAsyncAction接口,提供对异步操作的访问。

为方便起见,这种方法非常有效,可以很好地集成到异步编程模型中,该模型遍及Windows应用商店应用的Windows API。例如,您可以在并行模式库(PPL)任务中包装由RunAsync方法返回的IAsyncAction接口,并实现类似于我在9月和10月专栏中所描述的可组合性级别,“追求高效和可组合的异步”系统”:https://msdn.microsoft.com/magazine/jj618294 以及:回归到具有可恢复功能的未来“https://msdn.microsoft.com/magazine/jj658968

However, it’s useful and somewhat sobering to realize what this seemingly innocuous code really represents. At the heart of the C++/CX extensions is a runtime based on COM and its IUnknown interface. Such an interface-based object model can’t possibly provide static methods. There has to be an object for there to be an interface, and some sort of class factory to create that object, and indeed there is.

The Windows Runtime defines something called a runtime class that’s very much akin to a traditional COM class. If you’re old-school, you could even define the class in an IDL file and run it through a new version of the MIDL compiler specifically suited to the task, and it will generate .winmd metadata files and the appropriate headers.

A runtime class can have both instance methods and static methods. They’re defined with separate interfaces. The interface containing the instance methods becomes the class’s default interface, and the interface containing the static methods is attributed to the runtime class in the generated metadata. In this case the ThreadPool runtime class lacks the activatable attribute and has no default interface, but once created, the static interface can be queried for, and then those not-so-static methods may be called. Figure 4 gives an example of what this might entail. Keep in mind that most of this would be compiler-generated, but it should give you a good idea of what it really costs to make that simple static method call to run a delegate asynchronously.

Figure 4 The WinRT Thread Pool
class WorkItemHandler :
  public RuntimeClass<RuntimeClassFlags<ClassicCom>,
  IWorkItemHandler>
{
  virtual HRESULT __stdcall Invoke(IAsyncAction *)
  {
    // Work goes here!
    return S_OK;
  }
};
auto handler = Make<WorkItemHandler>();
HSTRING_HEADER header;
HSTRING clsid;
auto hr = WindowsCreateStringReference(
  RuntimeClass_Windows_System_Threading_ThreadPool, 
  _countof(RuntimeClass_Windows_System_Threading_ThreadPool)
  - 1, &header, &clsid);
ASSERT(S_OK == hr);
ComPtr<IThreadPoolStatics> tp;
hr = RoGetActivationFactory(
  clsid, __uuidof(IThreadPoolStatics),
  reinterpret_cast<void **>(tp.GetAddressOf()));
ASSERT(S_OK == hr);
ComPtr<IAsyncAction> a;
hr = tp->RunAsync(handler.Get(), a.GetAddressOf());
ASSERT(S_OK == hr);

This is certainly a far cry from the relative simplicity and efficiency of calling the TrySubmitThreadpoolCallback function. It’s helpful to understand the cost of the abstractions you use, even if you end up deciding that the cost is justified given some measure of productivity. Let me break it down briefly.

The WorkItemHandler delegate is actually an IUnknown-based IWorkItemHandler interface with a single Invoke method. The implementation of this interface isn’t provided by the API but rather by the compiler. This makes sense because it provides a convenient container for any variables captured by the lambda and the lambda’s body would naturally reside within the compiler-generated Invoke method. In this example I’m simply relying on the Windows Runtime Library (WRL) RuntimeClass template class to implement IUnknown for me. I can then use the handy Make template function to create an instance of my WorkItemHandler. For stateless lambdas and functions pointers, I’d further expect the compiler to produce a static implementation with a no-op implementation of IUnknown to avoid the overhead of dynamic allocation.

To create an instance of the runtime class, I need to call the RoGet­ActivationFactory function. However, it needs a class ID. Notice that this isn’t the CLSID of traditional COM but rather the fully qualified name of the type, in this case, Windows.System.Threading.ThreadPool. Here I’m using a constant array generated by the MIDL compiler to avoid having to count the string at run time. As if that weren’t enough, I need to also create an HSTRING version of this class ID. Here I’m using the WindowsCreateStringReference function, which, unlike the regular WindowsCreateString function, doesn’t create a copy of the source string. As a convenience, WRL also provides the HStringReference class that wraps up this functionality. I can now call the RoGetActivationFactory function, requesting the IThreadPoolStatics interface directly and storing the resulting pointer in a WRL-provided smart pointer.

I can now finally call the RunAsync method on this interface, providing it with my IWorkItemHandler implementation as well as the address of an IAsyncAction smart pointer representing the resulting action object.

It’s then perhaps not surprising that this thread pool API doesn’t provide anywhere near the amount of functionality and flexibility provided by the core Windows thread pool API or the Concurrency Runtime. The benefit of C++/CX and the runtime classes is, however, realized along the boundaries between the program and the runtime itself. As a C++ programmer, you can be thankful that Windows 8 isn’t an entirely new platform and the traditional Windows API is still at your disposal, if and when you need it.

下面这部分有点看不懂了,我回去看看那个线程池的文章,回来再看吧

发布了93 篇原创文章 · 获赞 13 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/qq_18218335/article/details/84312616