Java 执行器:当任务完成时,如何在不阻塞的情况下收到通知?

发布于 2024-07-19 03:40:06 字数 481 浏览 16 评论 0原文

假设我有一个充满任务的队列,我需要将其提交给执行器服务。 我希望一次处理一个。 我能想到的最简单的方法是:

  1. 从队列中取出一个任务 将
  2. 其提交给执行程序
  3. 在返回的 Future 上调用 .get 并阻塞直到结果可用
  4. 从队列中取出另一个任务...

但是,我正在尝试避免完全阻塞。 如果我有 10,000 个这样的队列,它们需要一次处理一个任务,那么我将耗尽堆栈空间,因为它们中的大多数将保留阻塞的线程。

我想要的是提交任务并提供任务完成时调用的回调。 我将使用该回调通知作为发送下一个任务的标志。 (functionjava和jetlang显然使用了这种非阻塞算法,但我无法理解他们的代码)

我如何使用JDK的java.util.concurrent来做到这一点,而不需要编写自己的执行器服务?

(为我提供这些任务的队列本身可能会阻塞,但这是一个稍后要解决的问题)

Say I have a queue full of tasks which I need to submit to an executor service. I want them processed one at a time. The simplest way I can think of is to:

  1. Take a task from the queue
  2. Submit it to the executor
  3. Call .get on the returned Future and block until a result is available
  4. Take another task from the queue...

However, I am trying to avoid blocking completely. If I have 10,000 such queues, which need their tasks processed one at a time, I'll run out of stack space because most of them will be holding on to blocked threads.

What I would like is to submit a task and provide a call-back which is called when the task is complete. I'll use that call-back notification as a flag to send the next task. (functionaljava and jetlang apparently use such non-blocking algorithms, but I can't understand their code)

How can I do that using JDK's java.util.concurrent, short of writing my own executor service?

(the queue which feeds me these tasks may itself block, but that is an issue to be tackled later)

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

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

发布评论

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

评论(12

岁月无声 2024-07-26 03:40:06

定义一个回调接口来接收您想要在完成通知中传递的任何参数。 然后在任务结束时调用它。

您甚至可以为 Runnable 任务编写一个通用包装器,并将它们提交给 ExecutorService。 或者,请参阅下面的 Java 8 内置机制。

class CallbackTask implements Runnable {

  private final Runnable task;

  private final Callback callback;

  CallbackTask(Runnable task, Callback callback) {
    this.task = task;
    this.callback = callback;
  }

  public void run() {
    task.run();
    callback.complete();
  }

}

使用 CompletableFuture,Java 8 包含了一种更复杂的方法来组合管道,其中流程可以异步且有条件地完成。 这是一个人为但完整的通知示例。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class GetTaskNotificationWithoutBlocking {

  public static void main(String... argv) throws Exception {
    ExampleService svc = new ExampleService();
    GetTaskNotificationWithoutBlocking listener = new GetTaskNotificationWithoutBlocking();
    CompletableFuture<String> f = CompletableFuture.supplyAsync(svc::work);
    f.thenAccept(listener::notify);
    System.out.println("Exiting main()");
  }

  void notify(String msg) {
    System.out.println("Received message: " + msg);
  }

}

class ExampleService {

  String work() {
    sleep(7000, TimeUnit.MILLISECONDS); /* Pretend to be busy... */
    char[] str = new char[5];
    ThreadLocalRandom current = ThreadLocalRandom.current();
    for (int idx = 0; idx < str.length; ++idx)
      str[idx] = (char) ('A' + current.nextInt(26));
    String msg = new String(str);
    System.out.println("Generated message: " + msg);
    return msg;
  }

  public static void sleep(long average, TimeUnit unit) {
    String name = Thread.currentThread().getName();
    long timeout = Math.min(exponential(average), Math.multiplyExact(10, average));
    System.out.printf("%s sleeping %d %s...%n", name, timeout, unit);
    try {
      unit.sleep(timeout);
      System.out.println(name + " awoke.");
    } catch (InterruptedException abort) {
      Thread.currentThread().interrupt();
      System.out.println(name + " interrupted.");
    }
  }

  public static long exponential(long avg) {
    return (long) (avg * -Math.log(1 - ThreadLocalRandom.current().nextDouble()));
  }

}

Define a callback interface to receive whatever parameters you want to pass along in the completion notification. Then invoke it at the end of the task.

You could even write a general wrapper for Runnable tasks, and submit these to ExecutorService. Or, see below for a mechanism built into Java 8.

class CallbackTask implements Runnable {

  private final Runnable task;

  private final Callback callback;

  CallbackTask(Runnable task, Callback callback) {
    this.task = task;
    this.callback = callback;
  }

  public void run() {
    task.run();
    callback.complete();
  }

}

With CompletableFuture, Java 8 included a more elaborate means to compose pipelines where processes can be completed asynchronously and conditionally. Here's a contrived but complete example of notification.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class GetTaskNotificationWithoutBlocking {

  public static void main(String... argv) throws Exception {
    ExampleService svc = new ExampleService();
    GetTaskNotificationWithoutBlocking listener = new GetTaskNotificationWithoutBlocking();
    CompletableFuture<String> f = CompletableFuture.supplyAsync(svc::work);
    f.thenAccept(listener::notify);
    System.out.println("Exiting main()");
  }

  void notify(String msg) {
    System.out.println("Received message: " + msg);
  }

}

class ExampleService {

  String work() {
    sleep(7000, TimeUnit.MILLISECONDS); /* Pretend to be busy... */
    char[] str = new char[5];
    ThreadLocalRandom current = ThreadLocalRandom.current();
    for (int idx = 0; idx < str.length; ++idx)
      str[idx] = (char) ('A' + current.nextInt(26));
    String msg = new String(str);
    System.out.println("Generated message: " + msg);
    return msg;
  }

  public static void sleep(long average, TimeUnit unit) {
    String name = Thread.currentThread().getName();
    long timeout = Math.min(exponential(average), Math.multiplyExact(10, average));
    System.out.printf("%s sleeping %d %s...%n", name, timeout, unit);
    try {
      unit.sleep(timeout);
      System.out.println(name + " awoke.");
    } catch (InterruptedException abort) {
      Thread.currentThread().interrupt();
      System.out.println(name + " interrupted.");
    }
  }

  public static long exponential(long avg) {
    return (long) (avg * -Math.log(1 - ThreadLocalRandom.current().nextDouble()));
  }

}
最佳男配角 2024-07-26 03:40:06

在 Java 8 中,您可以使用 CompletableFuture 。 这是我在代码中的一个示例,我使用它从我的用户服务中获取用户,将它们映射到我的视图对象,然后更新我的视图或显示错误对话框(这是一个 GUI 应用程序):

    CompletableFuture.supplyAsync(
            userService::listUsers
    ).thenApply(
            this::mapUsersToUserViews
    ).thenAccept(
            this::updateView
    ).exceptionally(
            throwable -> { showErrorDialogFor(throwable); return null; }
    );

它异步执行。 我使用两个私有方法:mapUsersToUserViewsupdateView

In Java 8 you can use CompletableFuture. Here's an example I had in my code where I'm using it to fetch users from my user service, map them to my view objects and then update my view or show an error dialog (this is a GUI application):

    CompletableFuture.supplyAsync(
            userService::listUsers
    ).thenApply(
            this::mapUsersToUserViews
    ).thenAccept(
            this::updateView
    ).exceptionally(
            throwable -> { showErrorDialogFor(throwable); return null; }
    );

It executes asynchronously. I'm using two private methods: mapUsersToUserViews and updateView.

流年里的时光 2024-07-26 03:40:06

使用 Guava 的可监听 future API 并添加回调。 比照。 来自网站:

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
  public Explosion call() {
    return pushBigRedButton();
  }
});
Futures.addCallback(explosion, new FutureCallback<Explosion>() {
  // we want this handler to run immediately after we push the big red button!
  public void onSuccess(Explosion explosion) {
    walkAwayFrom(explosion);
  }
  public void onFailure(Throwable thrown) {
    battleArchNemesis(); // escaped the explosion!
  }
});

Use Guava's listenable future API and add a callback. Cf. from the website :

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
  public Explosion call() {
    return pushBigRedButton();
  }
});
Futures.addCallback(explosion, new FutureCallback<Explosion>() {
  // we want this handler to run immediately after we push the big red button!
  public void onSuccess(Explosion explosion) {
    walkAwayFrom(explosion);
  }
  public void onFailure(Throwable thrown) {
    battleArchNemesis(); // escaped the explosion!
  }
});
嗳卜坏 2024-07-26 03:40:06

您可以扩展 FutureTask 类,并重写 done() 方法,然后将 FutureTask 对象添加到 ExecutorService ,因此当 FutureTask 立即完成时,done() 方法将被调用。

You could extend FutureTask class, and override the done() method, then add the FutureTask object to the ExecutorService, so the done() method will invoke when the FutureTask completed immediately.

橘香 2024-07-26 03:40:06

ThreadPoolExecutor 还具有您可以重写和使用的 beforeExecuteafterExecute 挂钩方法。 以下是 ThreadPoolExecutor 的 Javadocs

钩子方法

此类提供受保护的可重写beforeExecute(java.lang.Thread, java.lang.Runnable)afterExecute(java .lang.Runnable, java.lang.Throwable) 在每个任务执行之前和之后调用的方法。 这些可用于操纵执行环境; 例如,重新初始化 ThreadLocals、收集统计信息或添加日志条目。 此外,方法 Termination() 可以被重写,以执行 Executor 完全终止后需要完成的任何特殊处理。 如果钩子或回调方法抛出异常,内部工作线程可能会失败并突然终止。

ThreadPoolExecutor also has beforeExecute and afterExecute hook methods that you can override and make use of. Here is the description from ThreadPoolExecutor's Javadocs.

Hook methods

This class provides protected overridable beforeExecute(java.lang.Thread, java.lang.Runnable) and afterExecute(java.lang.Runnable, java.lang.Throwable) methods that are called before and after execution of each task. These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals, gathering statistics, or adding log entries. Additionally, method terminated() can be overridden to perform any special processing that needs to be done once the Executor has fully terminated. If hook or callback methods throw exceptions, internal worker threads may in turn fail and abruptly terminate.

桃扇骨 2024-07-26 03:40:06

使用 CountDownLatch

它来自java.util.concurrent,它正是等待多个线程完成执行然后再继续的方法。

为了实现您正在寻找的回调效果,这确实需要一些额外的额外工作。 也就是说,您自己在一个单独的线程中处理此问题,该线程使用 CountDownLatch 并等待它,然后继续通知您需要通知的任何内容。 没有对回调或类似效果的本机支持。


编辑:现在我进一步理解了你的问题,我认为你太过分了,没有必要。 如果您采用常规 SingleThreadExecutor,给它所有的任务,它会本地进行排队。

Use a CountDownLatch.

It's from java.util.concurrent and it's exactly the way to wait for several threads to complete execution before continuing.

In order to achieve the callback effect you're looking after, that does require a little additional extra work. Namely, handling this by yourself in a separate thread which uses the CountDownLatch and does wait on it, then goes on about notifying whatever it is you need to notify. There is no native support for callback, or anything similar to that effect.


EDIT: now that I further understand your question, I think you are reaching too far, unnecessarily. If you take a regular SingleThreadExecutor, give it all the tasks, and it will do the queueing natively.

鸠魁 2024-07-26 03:40:06

如果您想确保没有任务同时运行,请使用 SingleThreadedExecutor。 任务将按照提交的顺序进行处理。 您甚至不需要保留任务,只需将它们提交给执行人员即可。

If you want to make sure that no tasks will run at the same time then use a SingleThreadedExecutor. The tasks will be processed in the order the are submitted. You don't even need to hold the tasks, just submit them to the exec.

谈下烟灰 2024-07-26 03:40:06

实现 Callback 机制的简单代码

import java.util.concurrent.*;
import java.util.*;

public class CallBackDemo{
    public CallBackDemo(){
        System.out.println("creating service");
        ExecutorService service = Executors.newFixedThreadPool(5);

        try{
            for ( int i=0; i<5; i++){
                Callback callback = new Callback(i+1);
                MyCallable myCallable = new MyCallable((long)i+1,callback);
                Future<Long> future = service.submit(myCallable);
                //System.out.println("future status:"+future.get()+":"+future.isDone());
            }
        }catch(Exception err){
            err.printStackTrace();
        }
        service.shutdown();
    }
    public static void main(String args[]){
        CallBackDemo demo = new CallBackDemo();
    }
}
class MyCallable implements Callable<Long>{
    Long id = 0L;
    Callback callback;
    public MyCallable(Long val,Callback obj){
        this.id = val;
        this.callback = obj;
    }
    public Long call(){
        //Add your business logic
        System.out.println("Callable:"+id+":"+Thread.currentThread().getName());
        callback.callbackMethod();
        return id;
    }
}
class Callback {
    private int i;
    public Callback(int i){
        this.i = i;
    }
    public void callbackMethod(){
        System.out.println("Call back:"+i);
        // Add your business logic
    }
}

使用 ExecutorService输出

creating service
Callable:1:pool-1-thread-1
Call back:1
Callable:3:pool-1-thread-3
Callable:2:pool-1-thread-2
Call back:2
Callable:5:pool-1-thread-5
Call back:5
Call back:3
Callable:4:pool-1-thread-4
Call back:4

:要点:

  1. 如果您希望按 FIFO 顺序按顺序处理任务,请替换 newFixedThreadPool(5) with newFixedThreadPool(1)
  2. 如果你想在分析上一个任务的callback结果后处理下一个任务,只需取消下面一行的注释

    //System.out.println("未来状态:"+future.get()+":"+future.isDone()); 
      
  3. 你可以替换 newFixedThreadPool() 与其中之一

    Executors.newCachedThreadPool() 
      Executors.newWorkStealingPool() 
      线程池执行器 
      

    取决于您的用例。

  4. 如果你想异步处理回调方法

    a. 将共享的 ExecutorService 或 ThreadPoolExecutor 传递给 Callable 任务

    b. 将您的 Callable 方法转换为 Callable/Runnable 任务

    c. 请将回调任务推送到 ExecutorService 或 ThreadPoolExecutor

Simple code to implement Callback mechanism using ExecutorService

import java.util.concurrent.*;
import java.util.*;

public class CallBackDemo{
    public CallBackDemo(){
        System.out.println("creating service");
        ExecutorService service = Executors.newFixedThreadPool(5);

        try{
            for ( int i=0; i<5; i++){
                Callback callback = new Callback(i+1);
                MyCallable myCallable = new MyCallable((long)i+1,callback);
                Future<Long> future = service.submit(myCallable);
                //System.out.println("future status:"+future.get()+":"+future.isDone());
            }
        }catch(Exception err){
            err.printStackTrace();
        }
        service.shutdown();
    }
    public static void main(String args[]){
        CallBackDemo demo = new CallBackDemo();
    }
}
class MyCallable implements Callable<Long>{
    Long id = 0L;
    Callback callback;
    public MyCallable(Long val,Callback obj){
        this.id = val;
        this.callback = obj;
    }
    public Long call(){
        //Add your business logic
        System.out.println("Callable:"+id+":"+Thread.currentThread().getName());
        callback.callbackMethod();
        return id;
    }
}
class Callback {
    private int i;
    public Callback(int i){
        this.i = i;
    }
    public void callbackMethod(){
        System.out.println("Call back:"+i);
        // Add your business logic
    }
}

output:

creating service
Callable:1:pool-1-thread-1
Call back:1
Callable:3:pool-1-thread-3
Callable:2:pool-1-thread-2
Call back:2
Callable:5:pool-1-thread-5
Call back:5
Call back:3
Callable:4:pool-1-thread-4
Call back:4

Key notes:

  1. If you want process tasks in sequence in FIFO order, replace newFixedThreadPool(5) with newFixedThreadPool(1)
  2. If you want to process next task after analysing the result from callback of previous task,just un-comment below line

    //System.out.println("future status:"+future.get()+":"+future.isDone());
    
  3. You can replace newFixedThreadPool() with one of

    Executors.newCachedThreadPool()
    Executors.newWorkStealingPool()
    ThreadPoolExecutor
    

    depending on your use case.

  4. If you want to handle callback method asynchronously

    a. Pass a shared ExecutorService or ThreadPoolExecutor to Callable task

    b. Convert your Callable method to Callable/Runnable task

    c. Push callback task to ExecutorService or ThreadPoolExecutor

萝莉病 2024-07-26 03:40:06

这是使用 Guava 的 ListenableFuture 对 Pache 答案的扩展。

特别是,Futures.transform() 返回 ListenableFuture,因此可用于链接异步调用。 Futures.addCallback() 返回 void,因此不能用于链接,但适合处理异步完成时的成功/失败。

// ListenableFuture1: Open Database
ListenableFuture<Database> database = service.submit(() -> openDatabase());

// ListenableFuture2: Query Database for Cursor rows
ListenableFuture<Cursor> cursor =
    Futures.transform(database, database -> database.query(table, ...));

// ListenableFuture3: Convert Cursor rows to List<Foo>
ListenableFuture<List<Foo>> fooList =
    Futures.transform(cursor, cursor -> cursorToFooList(cursor));

// Final Callback: Handle the success/errors when final future completes
Futures.addCallback(fooList, new FutureCallback<List<Foo>>() {
  public void onSuccess(List<Foo> foos) {
    doSomethingWith(foos);
  }
  public void onFailure(Throwable thrown) {
    log.error(thrown);
  }
});

注意:除了链接异步任务之外,Futures.transform() 还允许您在单独的执行器上安排每个任务(本示例中未显示)。

This is an extension to Pache's answer using Guava's ListenableFuture.

In particular, Futures.transform() returns ListenableFuture so can be used to chain async calls. Futures.addCallback() returns void, so cannot be used for chaining, but is good for handling success/failure on an async completion.

// ListenableFuture1: Open Database
ListenableFuture<Database> database = service.submit(() -> openDatabase());

// ListenableFuture2: Query Database for Cursor rows
ListenableFuture<Cursor> cursor =
    Futures.transform(database, database -> database.query(table, ...));

// ListenableFuture3: Convert Cursor rows to List<Foo>
ListenableFuture<List<Foo>> fooList =
    Futures.transform(cursor, cursor -> cursorToFooList(cursor));

// Final Callback: Handle the success/errors when final future completes
Futures.addCallback(fooList, new FutureCallback<List<Foo>>() {
  public void onSuccess(List<Foo> foos) {
    doSomethingWith(foos);
  }
  public void onFailure(Throwable thrown) {
    log.error(thrown);
  }
});

NOTE: In addition to chaining async tasks, Futures.transform() also allows you to schedule each task on a separate executor (Not shown in this example).

少女净妖师 2024-07-26 03:40:06

只是为了补充 Matt 的答案(这很有帮助),这里有一个更充实的示例来展示回调的使用。

private static Primes primes = new Primes();

public static void main(String[] args) throws InterruptedException {
    getPrimeAsync((p) ->
        System.out.println("onPrimeListener; p=" + p));

    System.out.println("Adios mi amigito");
}
public interface OnPrimeListener {
    void onPrime(int prime);
}
public static void getPrimeAsync(OnPrimeListener listener) {
    CompletableFuture.supplyAsync(primes::getNextPrime)
        .thenApply((prime) -> {
            System.out.println("getPrimeAsync(); prime=" + prime);
            if (listener != null) {
                listener.onPrime(prime);
            }
            return prime;
        });
}

输出是:

    getPrimeAsync(); prime=241
    onPrimeListener; p=241
    Adios mi amigito

Just to add to Matt's answer, which helped, here is a more fleshed-out example to show the use of a callback.

private static Primes primes = new Primes();

public static void main(String[] args) throws InterruptedException {
    getPrimeAsync((p) ->
        System.out.println("onPrimeListener; p=" + p));

    System.out.println("Adios mi amigito");
}
public interface OnPrimeListener {
    void onPrime(int prime);
}
public static void getPrimeAsync(OnPrimeListener listener) {
    CompletableFuture.supplyAsync(primes::getNextPrime)
        .thenApply((prime) -> {
            System.out.println("getPrimeAsync(); prime=" + prime);
            if (listener != null) {
                listener.onPrime(prime);
            }
            return prime;
        });
}

The output is:

    getPrimeAsync(); prime=241
    onPrimeListener; p=241
    Adios mi amigito
素年丶 2024-07-26 03:40:06

您可以使用 Callable 的实现,其中

public class MyAsyncCallable<V> implements Callable<V> {

    CallbackInterface ci;

    public MyAsyncCallable(CallbackInterface ci) {
        this.ci = ci;
    }

    public V call() throws Exception {

        System.out.println("Call of MyCallable invoked");
        System.out.println("Result = " + this.ci.doSomething(10, 20));
        return (V) "Good job";
    }
}

CallbackInterface 是非常基本的东西

public interface CallbackInterface {
    public int doSomething(int a, int b);
}

,现在主类将如下所示

ExecutorService ex = Executors.newFixedThreadPool(2);

MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);

You may use a implementation of Callable such that

public class MyAsyncCallable<V> implements Callable<V> {

    CallbackInterface ci;

    public MyAsyncCallable(CallbackInterface ci) {
        this.ci = ci;
    }

    public V call() throws Exception {

        System.out.println("Call of MyCallable invoked");
        System.out.println("Result = " + this.ci.doSomething(10, 20));
        return (V) "Good job";
    }
}

where CallbackInterface is something very basic like

public interface CallbackInterface {
    public int doSomething(int a, int b);
}

and now the main class will look like this

ExecutorService ex = Executors.newFixedThreadPool(2);

MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);
错爱 2024-07-26 03:40:06

您可以在 java 执行器中实现未来任务,该执行器在任务完成时返回回调。

  • 对任务使用 callable 而不是 runnable
  • 可以在异步执行线程时执行不相关的任务
  • 可以执行多个任务并可以获得每个任务的结果

Follwoing 是从任务返回随机整数值并在主线程上打印的

public class ExecutorCallable {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //fix number of threads
        //blocking queue-thread safe
        ExecutorService service = Executors.newFixedThreadPool(100);

        //submit the tasks for execution
        Future<Integer> result = service.submit(new Task());

        //*** perform some unrelated operations

        System.out.println("Result of submitted task is : " + result.get());//blocks until the future is ready after completion

        System.out.println("Thread name " + Thread.currentThread().getName());
    }

    static class Task implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            return new Random().nextInt();
        }
    }
 }

类输出是:

Result of submitted task is : 16645418
Thread name main

You can implement future tasks in java executors that returns the callback on the completion of the tasks.

  • Use callable for tasks instead of runnable
  • Can perform non related tasks when execution of threads are in progress asynchronously
  • Multiple tasks can be executed and can get result of each one

Follwoing is the class that returns random integer values from a task and printed on main thread

public class ExecutorCallable {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //fix number of threads
        //blocking queue-thread safe
        ExecutorService service = Executors.newFixedThreadPool(100);

        //submit the tasks for execution
        Future<Integer> result = service.submit(new Task());

        //*** perform some unrelated operations

        System.out.println("Result of submitted task is : " + result.get());//blocks until the future is ready after completion

        System.out.println("Thread name " + Thread.currentThread().getName());
    }

    static class Task implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            return new Random().nextInt();
        }
    }
 }

The Output is:

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