我有一个基于消息泵线程池结构的应用程序。只要存在可以阻止的动作,它就会被实现为“完成/触发evnet回调”动作,因此它不会使正在执行的线程停止。
虽然这种技术适用于大多数情况,但有些情况会变得非常不方便并使代码过于复杂。
我希望能够以透明的方式在等待时继续处理事件,而不会将功能分解为等待前/后的部分。
我该怎么做?
我有两个选择:
两种选择都有其缺陷,仅举几例:
1:
选项2最终可以创建越来越多的线程。
当然,可能还有其他我没有想到的选择。
编辑:语言是C ++,因此无法以简单(可移植?)的方式逐步推卸出功能。平台是Windows(API),虽然我不认为它是相关的。
答案 0 :(得分:2)
对于便携式C ++,这是行不通的,但既然你提到你的平台是Windows,为什么不使用MsgWaitForMultipleObjects?它的目的是让你完全按照你的问题所说的做法 - 在等待的同时保持消息。
答案 1 :(得分:0)
编辑:你提到不想“将功能分解为等待前/后的部分。”
你在用什么语言发展?如果它有连续性(C#中为yield return
)那么它提供了一种编写看似程序的代码的方法,但是在阻塞操作完成回调之前可以很容易地暂停代码。
这是一篇关于这个想法的文章:http://msdn.microsoft.com/en-us/magazine/cc546608.aspx
<强>更新强>
不幸的是,语言是C ++
这将是一个伟大的T恤口号。
好的,您可能会发现将顺序代码构建为状态机很有帮助,因此它具有中断/恢复功能。
e.g。你的痛苦需要写两个函数,一个启动的函数和一个作为完成事件的处理函数的函数:
void send_greeting(const std::string &msg)
{
std::cout << "Sending the greeting" << std::endl;
begin_sending_string_somehow(msg, greeting_sent_okay);
}
void greeting_sent_okay()
{
std::cout << "Greeting has been sent successfully." << std::endl;
}
你的想法是等待:
void send_greeting(const std::string &msg)
{
std::cout << "Sending the greeting" << std::endl;
waiter w;
begin_sending_string_somehow(msg, w);
w.wait_for_completion();
std::cout << "Greeting has been sent successfully." << std::endl;
}
在该示例中,waiter
重载operator(),因此它可以作为回调,wait_for_completion
以某种方式挂起,直到它看到operator()被调用。
我假设begin_sending_string_somehow
的第二个参数是一个模板参数,可以是任何不接受参数的可调用类型。
但正如你所说,这有弊端。每当一个线程像这样等待时,你就添加了另一个潜在的死锁,你也消耗了整个线程及其堆栈的“资源”,这意味着必须在其他地方创建更多的线程才能完成工作,这与线程池的整个观点相矛盾。
相反,写一个类:
class send_greeting
{
int state_;
std::string msg_;
public:
send_greeting(const std::string &msg)
: state_(0), msg_(msg) {}
void operator()
{
switch (state_++)
{
case 0:
std::cout << "Sending the greeting" << std::endl;
begin_sending_string_somehow(msg, *this);
break;
case 1:
std::cout << "Greeting has been sent successfully."
<< std::endl;
break;
}
}
};
该类实现函数调用操作符()
。每次调用它时,它都会执行逻辑中的下一步。 (当然,这是一个微不足道的例子,现在主要是状态管理噪声,但在一个更复杂的例子中有四个或五个状态,它可能有助于澄清代码的顺序性质。)
<强>问题:强>
如果事件回调函数签名具有特殊参数,则需要添加另一个operator()
重载,该重载将参数存储在额外字段中,然后调用无参数重载。然后它开始变得混乱,因为这些字段在初始状态下可以在编译时访问,即使它们在运行时在该状态下没有意义。
如何构建和删除类的对象?该对象必须存活直到操作完成或被放弃...... C ++的核心陷阱。我建议实施一般方案来管理它。创建一个“需要删除的东西”列表,并确保在某些安全点自动发生这种情况,即尽可能地尽可能接近GC。离你越远,你将会泄漏更多的记忆。
答案 2 :(得分:0)
在不了解您的具体应用程序的情况下(即消息需要多长时间处理等等),将会有很多手段:
这是托管或非托管C ++吗?
您使用的是哪个ThreadPool?
我认为平台有点相关,因为线程池的本质很重要。
例如:
如果您对线程池使用(Completion Ports)(即CreateIoCompletionPort)。您可以控制并发运行的线程数(因此最终创建总线程数)。如果将最大并发线程数设置为4. Windows将尝试仅允许4个线程同时运行。如果所有4个线程都忙于处理并且您排队第5个项目,则窗口将不允许该项目运行,直到4个中的一个完成(重新使用该线程)。该规则被破坏的唯一时间是线程被阻塞(即等待I / O),然后允许更多线程运行。
了解完成端口以及平台相关的原因非常重要。在不涉及内核的情况下实现这样的事情是非常困难的。了解忙线程和被阻塞线程之间的区别需要访问线程状态。完成端口在进入内核的上下文切换次数方面也非常有效。
回到你的问题:
看起来您应该有一个线程来处理/分发消息,并且通过将工作者推送到线程池来处理消息处理。让Completion端口处理负载平衡和并发。消息处理循环永远不会阻塞,可以继续处理消息。
如果收到的邮件的速度远远超过了你处理它们的能力,那么你可能不得不注意你的队列大小并在它变得太大时阻塞。
答案 3 :(得分:0)
看来你的问题是根本的,与C ++无关。其他语言可能更好地隐藏堆栈使用,但只要你没有从Foo()返回,你需要Foo()的调用堆栈。如果你还在执行Bar(),那也需要一个callstack。
线程是一种很好的方法,因为每个线程都有自己的callstack。 Continuations是一种智能但复杂的方法来保存callstacks,所以在可用的地方也可以选择。但是,如果你不想要那些,你将不得不使用一个callstack。
使用一个callstack进行Daling需要解决重入问题。在这里,关于什么是可能的,没有通用的答案。通常,您将拥有一组消息M1..Mx,它们由函数F1 ... Fy处理,具有一些特定于应用程序且可能依赖于状态的映射。使用可重入的消息循环,您可能在收到Mj时执行Fi。现在问题是该做什么。并非所有函数F1 ... Fn都可以调用;特别是Fi本身可能无法调用。然而,一些其他功能也可能是不可用的,例如,因为他们共享资源。这取决于应用程序。
如果Mj的处理需要任何这些不可用的功能,你必须推迟它。你能接受队列中的下一条消息吗?同样,这依赖于实现,甚至可能与消息类型和内容有关。如果消息足够独立,则可以不按顺序执行它们。这很快变得相当复杂 - 要确定是否可以接受队列中的第N个消息,您必须检查它是否可以相对于前面的N-1个消息无序执行。
语言可以通过不隐藏依赖关系来帮助您,但最终您必须做出明确的决定。没有银弹。
答案 4 :(得分:0)
您的问题是正确同步线程吗?如果这是你的问题,为什么不使用互斥?它可以用一个界面包裹起来。实际上,您可以使用PIMPL惯用法使互斥锁可移植。
http://msdn.microsoft.com/en-us/library/system.threading.mutex(VS.71).aspx