在高流量场景中使用 ASP.NET 中的 ThreadPool.QueueUserWorkItem

发布于 2024-08-02 08:04:59 字数 960 浏览 4 评论 0 原文

我一直认为,即使在 ASP.NET 中,使用 ThreadPool 执行(比方说非关键的)短期后台任务也被认为是最佳实践,但后来我遇到了 这篇文章似乎另有暗示 - 论点是您应该让 ThreadPool 来处理 ASP.NET 相关请求。

因此,到目前为止,我一直在执行小型异步任务:

ThreadPool.QueueUserWorkItem(s => PostLog(logEvent))

文章建议改为显式创建线程,类似于:

new Thread(() => PostLog(logEvent)){ IsBackground = true }.Start()

第一种方法具有受管理和限制的优点,但有潜力(如果文章正确)然后后台任务会与 ASP.NET 请求处理程序争夺线程。第二种方法释放线程池,但代价是不受限制,因此可能会占用太多资源。

所以我的问题是,文章中的建议正确吗?

如果您的网站流量如此之大,以至于您的线程池已满,那么最好采用带外,还是满的线程池意味着您已经达到了资源的限制,在这种情况下,您不应该尝试启动自己的线程吗?

澄清:我只是在小型非关键异步任务(例如远程日志记录)的范围内询问,而不是需要单独流程的昂贵工作项目(在这些情况下,我同意您需要更强大的解决方案)。

I've always been under the impression that using the ThreadPool for (let's say non-critical) short-lived background tasks was considered best practice, even in ASP.NET, but then I came across this article that seems to suggest otherwise - the argument being that you should leave the ThreadPool to deal with ASP.NET related requests.

So here's how I've been doing small asynchronous tasks so far:

ThreadPool.QueueUserWorkItem(s => PostLog(logEvent))

And the article is suggesting instead to create a thread explicitly, similar to:

new Thread(() => PostLog(logEvent)){ IsBackground = true }.Start()

The first method has the advantage of being managed and bounded, but there's the potential (if the article is correct) that the background tasks are then vying for threads with ASP.NET request-handlers. The second method frees up the ThreadPool, but at the cost of being unbounded and thus potentially using up too many resources.

So my question is, is the advice in the article correct?

If your site was getting so much traffic that your ThreadPool was getting full, then is it better to go out-of-band, or would a full ThreadPool imply that you're getting to the limit of your resources anyway, in which case you shouldn't be trying to start your own threads?

Clarification: I'm just asking in the scope of small non-critical asynchronous tasks (eg, remote logging), not expensive work items that would require a separate process (in these cases I agree you'll need a more robust solution).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

话少情深 2024-08-09 08:04:59

这里的其他答案似乎遗漏了最重要的一点:

除非您尝试并行化 CPU 密集型操作以便在低负载站点上更快地完成它,否则使用工作线程是没有意义的

这适用于由 new Thread(...) 创建的自由线程,以及 ThreadPool 中响应 的工作线程QueueUserWorkItem 请求。

是的,确实如此,您可以通过对太多工作项进行排队来使 ASP.NET 进程中的 ThreadPool 挨饿。它将阻止 ASP.NET 处理进一步的请求。文章中的信息在这方面是准确的;用于 QueueUserWorkItem 的同一线程池也用于处理请求。

但是,如果您实际上排队的工作项足以导致这种饥饿,那么您应该使线程池饥饿!如果您同时运行数百个 CPU 密集型操作,那么当计算机已经过载时,使用另一个工作线程来处理 ASP.NET 请求有什么好处呢?如果您遇到这种情况,您需要彻底重新设计!

大多数时候,我看到或听说在 ASP.NET 中不恰当地使用多线程代码,它并不是为了对 CPU 密集型工作进行排队。它用于对 I/O 密集型工作进行排队。 如果您想要执行 I/O 工作,那么您应该使用 I/O 线程(I/O 完成端口)。

具体来说,您应该使用任何库类支持的异步回调你正在使用。这些方法总是有非常清晰的标签;它们以 BeginEnd 一词开头。如Stream.BeginReadSocket.BeginConnectWebRequest.BeginGetResponse等。

这些方法确实使用ThreadPool,但它们使用IOCP,而IOCP不会干扰 ASP.NET 请求。它们是一种特殊的轻量级线程,可以被来自 I/O 系统的中断信号“唤醒”。在 ASP.NET 应用程序中,通常每个工作线程都有一个 I/O 线程,因此每个请求都可以有一个排队的异步操作。这实际上是数百个异步操作,而没有任何显着的性能下降(假设 I/O 子系统可以跟上)。它比您所需要的要多得多。

请记住,异步委托不会以这种方式工作 - 它们最终会使用工作线程,就像ThreadPool.QueueUserWorkItem一样。只有 .NET Framework 库类的内置异步方法才能够执行此操作。您可以自己做,但它很复杂,而且有点危险,可能超出了本讨论的范围。

在我看来,这个问题的最佳答案是不要在 ASP 中使用ThreadPool后台Thread实例。 NET。它与在 Windows 窗体应用程序中旋转线程完全不同,在 Windows 窗体应用程序中,您这样做是为了保持 UI 响应,而不关心它的效率如何。在 ASP.NET 中,您关心的是吞吐量,无论您是否使用 ThreadPool<,所有这些工作线程上的所有上下文切换都绝对会杀死您的吞吐量/code> 或不。

如果您发现自己在 ASP.NET 中编写线程代码,请考虑是否可以重写它以使用预先存在的异步方法,如果不能,请考虑您是否真的需要该代码完全在后台线程中运行。在大多数情况下,您可能会增加复杂性而没有任何净收益。

Other answers here seem to be leaving out the most important point:

Unless you are trying to parallelize a CPU-intensive operation in order to get it done faster on a low-load site, there is no point in using a worker thread at all.

That goes for both free threads, created by new Thread(...), and worker threads in the ThreadPool that respond to QueueUserWorkItem requests.

Yes, it's true, you can starve the ThreadPool in an ASP.NET process by queuing too many work items. It will prevent ASP.NET from processing further requests. The information in the article is accurate in that respect; the same thread pool used for QueueUserWorkItem is also used to serve requests.

But if you are actually queuing enough work items to cause this starvation, then you should be starving the thread pool! If you are running literally hundreds of CPU-intensive operations at the same time, what good would it do to have another worker thread to serve an ASP.NET request, when the machine is already overloaded? If you're running into this situation, you need to redesign completely!

Most of the time I see or hear about multi-threaded code being inappropriately used in ASP.NET, it's not for queuing CPU-intensive work. It's for queuing I/O-bound work. And if you want to do I/O work, then you should be using an I/O thread (I/O Completion Port).

Specifically, you should be using the async callbacks supported by whatever library class you're using. These methods are always very clearly labeled; they start with the words Begin and End. As in Stream.BeginRead, Socket.BeginConnect, WebRequest.BeginGetResponse, and so on.

These methods do use the ThreadPool, but they use IOCPs, which do not interfere with ASP.NET requests. They are a special kind of lightweight thread that can be "woken up" by an interrupt signal from the I/O system. And in an ASP.NET application, you normally have one I/O thread for each worker thread, so every single request can have one async operation queued up. That's literally hundreds of async operations without any significant performance degradation (assuming the I/O subsystem can keep up). It's way more than you'll ever need.

Just keep in mind that async delegates do not work this way - they'll end up using a worker thread, just like ThreadPool.QueueUserWorkItem. It's only the built-in async methods of the .NET Framework library classes that are capable of doing this. You can do it yourself, but it's complicated and a little bit dangerous and probably beyond the scope of this discussion.

The best answer to this question, in my opinion, is don't use the ThreadPool or a background Thread instance in ASP.NET. It's not at all like spinning up a thread in a Windows Forms application, where you do it to keep the UI responsive and don't care about how efficient it is. In ASP.NET, your concern is throughput, and all that context switching on all those worker threads is absolutely going to kill your throughput whether you use the ThreadPool or not.

Please, if you find yourself writing threading code in ASP.NET - consider whether or not it could be rewritten to use pre-existing asynchronous methods, and if it can't, then please consider whether or not you really, truly need the code to run in a background thread at all. In the majority of cases, you will probably be adding complexity for no net benefit.

巷子口的你 2024-08-09 08:04:59

根据 Microsoft ASP.NET 团队的 Thomas Marquadt 的说法,使用 ASP.NET ThreadPool (QueueUserWorkItem) 是安全的。

摘自文章

问)如果我的 ASP.NET 应用程序使用 CLR ThreadPool 线程,我是否会耗尽 ASP.NET(它也使用 CLR ThreadPool 来执行请求)?
..

<块引用>

A)总而言之,不用担心
ASP.NET 线程匮乏,如果
你认为这里有问题让
我知道,我们会处理的。

问)我应该创建自己的线程吗
(新话题)?这不是更好吗
对于 ASP.NET,因为它使用 CLR
线程池。

<块引用>

A)请不要。或者把它放在一个
不同的方式,不!如果你真的
聪明——比我聪明得多——然后你
可以创建自己的线程;
否则,想都别想。
以下是您应该这样做的一些原因
不频繁创建新线程:

  1. 与相比,它非常昂贵
    QueueUserWorkItem...顺便说一句,如果您可以编写比 CLR 更好的 ThreadPool,我鼓励您申请 Microsoft 的工作,因为我们肯定正在寻找像您这样的人!

Per Thomas Marquadt of the ASP.NET team at Microsoft, it is safe to use the ASP.NET ThreadPool (QueueUserWorkItem).

From the article:

Q) If my ASP.NET Application uses CLR ThreadPool threads, won’t I starve ASP.NET, which also uses the CLR ThreadPool to execute requests?
..

A) To summarize, don’t worry about
starving ASP.NET of threads, and if
you think there’s a problem here let
me know and we’ll take care of it.

Q) Should I create my own threads
(new Thread)? Won’t this be better
for ASP.NET, since it uses the CLR
ThreadPool.

A) Please don’t. Or to put it a
different way, no!!! If you’re really
smart—much smarter than me—then you
can create your own threads;
otherwise, don’t even think about it.
Here are some reasons why you should
not frequently create new threads:

  1. It is very expensive, compared to
    QueueUserWorkItem...By the way, if you can write a better ThreadPool than the CLR’s, I encourage you to apply for a job at Microsoft, because we’re definitely looking for people like you!.
我也只是我 2024-08-09 08:04:59

网站不应该绕过生成线程。

您通常会将此功能移至 Windows 服务中,然后与之通信(我使用 MSMQ 与它们通信)。

-- 编辑

我在这里描述了一个实现: 基于队列ASP.NET MVC Web 应用程序中的后台处理

- 编辑

要详细说明为什么这比线程更好:

使用 MSMQ,您可以与另一台服务器进行通信。您可以跨机器写入队列,因此,如果您出于某种原因确定后台任务过多地消耗了主服务器的资源,您只需简单地转移它即可。

它还允许您批量处理您尝试执行的任何任务(发送电子邮件/其他内容)。

Websites shouldn't go around spawning threads.

You typically move this functionality out into a Windows Service that you then communicate with (I use MSMQ to talk to them).

-- Edit

I described an implementation here: Queue-Based Background Processing in ASP.NET MVC Web Application

-- Edit

To expand why this is even better than just threads:

Using MSMQ, you can communicate to another server. You can write to a queue across machines, so if you determine, for some reason, that your background task is using up the resources of the main server too much, you can just shift it quite trivially.

It also allows you to batch-process whatever task you were trying to do (send emails/whatever).

三月梨花 2024-08-09 08:04:59

我绝对认为 ASP.NET 中快速、低优先级异步工作的一般做法是使用 .NET 线程池,特别是对于高流量场景,因为您希望资源受到限制。

此外,线程的实现是隐藏的 - 如果您开始生成自己的线程,则还必须正确管理它们。并不是说你做不到,但为什么要重新发明那个轮子呢?

如果性能成为问题,并且您可以确定线程池是限制因素(而不是数据库连接、传出网络连接、内存、页面超时等),那么您可以调整线程池配置以允许更多工作线程、更高的排队请求等等。

如果您没有性能问题,那么选择生成新线程以减少与 ASP.NET 请求队列的争用是典型的过早优化。

理想情况下,您不需要使用单独的线程来执行日志记录操作 - 只需使原始线程尽快完成操作,这就是 MSMQ 和单独的消费者线程/进程发挥作用的地方。我同意,这需要实现的工作量更大、工作量更大,但这里确实需要持久性 - 共享内存队列的波动性很快就会失去其受欢迎程度。

I definitely think that general practice for quick, low-priority asynchronous work in ASP.NET would be to use the .NET thread pool, particularly for high-traffic scenarios as you want your resources to be bounded.

Also, the implementation of threading is hidden - if you start spawning your own threads, you have to manage them properly as well. Not saying you couldn't do it, but why reinvent that wheel?

If performance becomes an issue, and you can establish that the thread pool is the limiting factor (and not database connections, outgoing network connections, memory, page timeouts etc) then you tweak the thread pool configuration to allow more worker threads, higher queued requests, etc.

If you don't have a performance problem then choosing to spawn new threads to reduce contention with the ASP.NET request queue is classic premature optimization.

Ideally you wouldn't need to use a separate thread to do a logging operation though - just enable the original thread to complete the operation as quickly as possible, which is where MSMQ and a separate consumer thread / process come in to the picture. I agree that this is heavier and more work to implement, but you really need the durability here - the volatility of a shared, in-memory queue will quickly wear out its welcome.

豆芽 2024-08-09 08:04:59

您应该使用 QueueUserWorkItem,并避免创建新线程,就像避免瘟疫一样。为了解释为什么您不会挨饿 ASP.NET,因为它使用相同的线程池,请想象一个非常熟练的杂耍演员用两只手保持六个保龄球瓶、剑或其他东西在飞行。为了直观地了解为什么创建自己的线程是不好的,想象一下在西雅图的高峰时段会发生什么,当时大量使用的高速公路入口坡道允许车辆立即进入交通,而不是使用交通灯并将入口数量限制为每隔几秒一个。最后,详细解释请参见此链接:

http://blogs.msdn.com/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net- applications.aspx

谢谢,
托马斯

You should use QueueUserWorkItem, and avoid creating new threads like you would avoid the plague. For a visual that explains why you won't starve ASP.NET, since it uses the same ThreadPool, imagine a very skilled juggler using two hands to keep a half dozen bowling pins, swords, or whatever in flight. For a visual of why creating your own threads is bad, imagine what happens in Seattle at rush hour when heavily used entrance ramps to the highway allow vehicles to enter traffic immediately instead of using a light and limiting the number of entrances to one every few seconds. Finally, for a detailed explanation, please see this link:

http://blogs.msdn.com/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx

Thanks,
Thomas

小霸王臭丫头 2024-08-09 08:04:59

那篇文章不正确。 ASP.NET 有自己的线程池(托管工作线程),用于服务 ASP.NET 请求。该池通常有几百个线程,并且与 ThreadPool 池分开,后者是处理器的较小倍数。

在 ASP.NET 中使用 ThreadPool 不会干扰 ASP.NET 工作线程。使用ThreadPool就可以了。

设置一个仅用于记录消息并使用生产者/消费者模式将日志消息传递给该线程的线程也是可以接受的。在这种情况下,由于线程是长期存在的,因此您应该创建一个新线程来运行日志记录。

对每条消息使用一个新线程绝对是矫枉过正。

如果您只谈论日志记录,另一种选择是使用像 log4net 这样的库。它在单独的线程中处理日志记录,并处理该场景中可能出现的所有上下文问题。

That article is not correct. ASP.NET has it's own pool of threads, managed worker threads, for serving ASP.NET requests. This pool is usually a few hundred threads and is separate from the ThreadPool pool, which is some smaller multiple of processors.

Using ThreadPool in ASP.NET will not interfere with ASP.NET worker threads. Using ThreadPool is fine.

It would also be acceptable to setup a single thread which is just for logging messages and using producer/consumer pattern to pass logs messages to that thread. In that case, since the thread is long-lived, you should create a single new thread to run the logging.

Using a new thread for every message is definitely overkill.

Another alternative, if you're only talking about logging, is to use a library like log4net. It handles logging in a separate thread and takes care of all the context issues that could come up in that scenario.

゛清羽墨安 2024-08-09 08:04:59

我想说这篇文章是错误的。如果您正在运营一家大型 .NET 商店,您可以安全地跨多个应用程序和多个网站使用该池(使用单独的应用程序池),只需根据 ThreadPool 文档:

每个进程有一个线程池。
线程池的默认大小为
每个可用工作线程 250 个
处理器和 1000 个 I/O 完成
线程。中的线程数
线程池可以通过使用来更改
SetMaxThreads 方法。每个线程
使用默认堆栈大小并运行
默认优先级。

I'd say the article is wrong. If you're running a large .NET shop you can safely use the pool across multiple apps and multiple websites (using seperate app pools), simply based on one statement in the ThreadPool documentation:

There is one thread pool per process.
The thread pool has a default size of
250 worker threads per available
processor, and 1000 I/O completion
threads. The number of threads in the
thread pool can be changed by using
the SetMaxThreads method. Each thread
uses the default stack size and runs
at the default priority.

还如梦归 2024-08-09 08:04:59

上周我在工作中被问到了类似的问题,我会给你同样的答案。为什么每个请求都使用多线程 Web 应用程序? Web 服务器是一个经过大量优化的出色系统,可以及时提供许多请求(即多线程)。想一想当您请求网络上的几乎任何页面时会发生什么。

  1. 对某些页面发出请求
  2. Html 被返回
  3. Html 告诉客户端进行进一步的请求(js、css、图像等)。
  4. 返回更多信息

您给出了远程日志记录的示例,但这应该是一个问题你的记录器。应建立异步流程来及时接收消息。 Sam 甚至指出您的记录器 (log4net) 应该已经支持这一点。

Sam 的观点也是正确的,即在 CLR 上使用线程池不会导致 IIS 中的线程池出现问题。不过,这里要关心的是,您不是从进程中生成线程,而是从 IIS 线程池线程中生成新线程。这是有区别的,而且区别很重要。

线程与进程

线程和进程都是方法
并行化应用程序。
然而,进程是独立的
包含自己的执行单元
状态信息,使用自己的
地址空间,并且只与交互
通过进程间相互
沟通机制(一般
由操作系统管理)。
应用程序通常被划分
在设计过程中融入流程
阶段,以及明确的主进程
当它创建时产生子进程
逻辑上分开的意义
重要的应用程序功能。
换句话说,流程是一个
架构构造。

相比之下,线程是一种编码
构造不影响
应用程序的架构。一个
单个进程可能包含多个
线程;进程内的所有线程
共享相同的状态和相同的内存
空间,并且可以与每个人进行交流
其他直接,因为他们共享
相同的变量。

来源

I was asked a similar question at work last week and I'll give you the same answer. Why are you multi threading web applications per request? A web server is a fantastic system optimized heavily to provide many requests in a timely fashion (i.e. multi threading). Think of what happens when you request almost any page on the web.

  1. A request is made for some page
  2. Html is served back
  3. The Html tells the client to make further requets (js, css, images, etc..)
  4. Further information is served back

You give the example of remote logging, but that should be a concern of your logger. An asynchronous process should be in place to receive messages in a timely fashion. Sam even points out that your logger (log4net) should already support this.

Sam is also correct in that using the Thread Pool on the CLR will not cause issues with the thread pool in IIS. The thing to be concerned with here though, is that you are not spawning threads from a process, you are spawning new threads off of IIS threadpool threads. There is a difference and the distinction is important.

Threads vs Process

Both threads and processes are methods
of parallelizing an application.
However, processes are independent
execution units that contain their own
state information, use their own
address spaces, and only interact with
each other via interprocess
communication mechanisms (generally
managed by the operating system).
Applications are typically divided
into processes during the design
phase, and a master process explicitly
spawns sub-processes when it makes
sense to logically separate
significant application functionality.
Processes, in other words, are an
architectural construct.

By contrast, a thread is a coding
construct that doesn't affect the
architecture of an application. A
single process might contains multiple
threads; all threads within a process
share the same state and same memory
space, and can communicate with each
other directly, because they share the
same variables.

Source

风筝在阴天搁浅。 2024-08-09 08:04:59

您可以使用 Parallel.For 或 Parallel.ForEach 并定义要分配的可能线程的限制,以平稳运行并防止池不足。

但是,要在后台运行,您将需要在 ASP.Net Web 应用程序中使用下面的纯 TPL 样式。

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;

ParallelOptions po = new ParallelOptions();
            po.CancellationToken = ts.Token;
            po.MaxDegreeOfParallelism = 6; //limit here

 Task.Factory.StartNew(()=>
                {                        
                  Parallel.ForEach(collectionList, po, (collectionItem) =>
                  {
                     //Code Here PostLog(logEvent);
                  }
                });

You can use Parallel.For or Parallel.ForEach and define the limit of possible threads you want to allocate to run smoothly and prevent pool starvation.

However, being run in background you will need to use pure TPL style below in ASP.Net web application.

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;

ParallelOptions po = new ParallelOptions();
            po.CancellationToken = ts.Token;
            po.MaxDegreeOfParallelism = 6; //limit here

 Task.Factory.StartNew(()=>
                {                        
                  Parallel.ForEach(collectionList, po, (collectionItem) =>
                  {
                     //Code Here PostLog(logEvent);
                  }
                });
一抹淡然 2024-08-09 08:04:59

我不同意引用的文章(C#feeds.com)。创建新线程很容易,但很危险。在单个核心上运行的最佳活动线程数实际上低得惊人 - 不到 10 个。如果为次要任务创建线程,则很容易导致机器浪费时间切换线程。线程是一种需要管理的资源。 WorkItem 抽象就是用来处理这个问题的。

这里需要权衡减少可用于请求的线程数量和创建太多线程以允许其中任何线程有效处理。这是一种非常动态的情况,但我认为应该主动管理(在本例中由线程池),而不是将其留给处理器以保持在线程创建之前。

最后,本文对使用 ThreadPool 的危险做了一些非常全面的陈述,但它确实需要一些具体的东西来支持它们。

I do not agree with the referenced article(C#feeds.com). It is easy to create a new thread but dangerous. The optimal number of active threads to run on a single core is actually surprisingly low - less than 10. It is way too easy to cause the machine to waste time switching threads if threads are created for minor tasks. Threads are a resource that REQUIRE management. The WorkItem abstraction is there to handle this.

There is a trade off here between reducing the number of threads available for requests and creating too many threads to allow any of them to process efficiently. This is a very dynamic situation but I think one that should be actively managed (in this case by the thread pool) rather than leaving it to the processer to stay ahead of the creation of threads.

Finally the article makes some pretty sweeping statements about the dangers of using the ThreadPool but it really needs something concrete to back them up.

并安 2024-08-09 08:04:59

IIS 是否使用相同的 ThreadPool 来处理传入请求似乎很难得到明确的答案,而且似乎也随着版本的不同而发生了变化。因此,不要过度使用 ThreadPool 线程似乎是个好主意,这样 IIS 就有很多可用的线程。另一方面,为每个小任务生成自己的线程似乎是一个坏主意。据推测,您在日志记录中进行了某种锁定,因此一次只能有一个线程进行,其余线程将轮流进行计划和未计划(更不用说生成新线程的开销)。本质上,您遇到的正是 ThreadPool 旨在避免的问题。

似乎合理的妥协是让您的应用程序分配一个可以向其传递消息的日志记录线程。您需要注意发送消息的速度尽可能快,以免减慢应用程序的速度。

Whether or not IIS uses the same ThreadPool to handle incoming requests seems hard to get a definitive answer to, and also seems to have changed over versions. So it would seem like a good idea not to use ThreadPool threads excessively, so that IIS has a lot of them available. On the other hand, spawning your own thread for every little task seems like a bad idea. Presumably, you have some sort of locking in your logging, so only one thread could progress at a time, and the rest would just take turns getting scheduled and unscheduled (not to mention the overhead of spawning a new thread). Essentially, you run into the exact problems the ThreadPool was designed to avoid.

It seems that a reasonable compromise would be for your app to allocate a single logging thread that you could pass messages to. You would want to be careful that sending messages is as fast as possible so that you don't slow down your app.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文