【问题标题】:How to implement CancellationToken support in DropNet?如何在 DropNet 中实现 CancellationToken 支持?
【发布时间】:2012-11-12 15:52:21
【问题描述】:

我想在 MonoTouch 应用程序中异步访问 DropBox API。
我认为使用 DropNet 会很方便,它本身依赖于 RestSharp

这两个库都运行良好,但返回 Tasks 的 DropNet 重载无法让您将请求与取消令牌相关联。

这是他们的实现方式:

public Task<IRestResponse> GetThumbnailTask(string path, ThumbnailSize size)
{
    if (!path.StartsWith("/")) path = "/" + path;
    var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
    return ExecuteTask(ApiType.Content, request, cancel);
}

ExecuteTask 的实现是基于 TaskCompletionSourcewas 最初的 written by Laurent Kempé:

public static class RestClientExtensions
{
    public static Task<TResult> ExecuteTask<TResult>(
        this IRestClient client, IRestRequest request
        ) where TResult : new()
    {
        var tcs = new TaskCompletionSource<TResult>();

        WaitCallback asyncWork = _ => {
            try {
                client.ExecuteAsync<TResult>(request,
                    (response, asynchandle) => {
                        if (response.StatusCode != HttpStatusCode.OK) {
                            tcs.SetException(new DropboxException(response));
                        } else {
                            tcs.SetResult(response.Data);
                        }
                    }
                );
            } catch (Exception exc) {
                    tcs.SetException(exc);
            }
        };

        return ExecuteTask(asyncWork, tcs);
    }


    public static Task<IRestResponse> ExecuteTask(
        this IRestClient client, IRestRequest request
        )
    {
        var tcs = new TaskCompletionSource<IRestResponse>();

        WaitCallback asyncWork = _ => {
            try {
                client.ExecuteAsync(request,
                    (response, asynchandle) => {
                        if (response.StatusCode != HttpStatusCode.OK) {
                            tcs.SetException(new DropboxException(response));
                        } else {
                            tcs.SetResult(response);
                        }
                    }
                );
            } catch (Exception exc) {
                    tcs.SetException(exc);
            }
        };

        return ExecuteTask(asyncWork, tcs);
   }

    private static Task<TResult> ExecuteTask<TResult>(
        WaitCallback asyncWork, TaskCompletionSource<TResult> tcs
        )
    {
        ThreadPool.QueueUserWorkItem(asyncWork);
        return tcs.Task;
    }
}

如何更改或扩展此代码以支持使用 CancellationToken 取消?
我想这样称呼它:

var task = dropbox.GetThumbnailTask(
    "/test.jpg", ThumbnailSize.ExtraLarge2, _token
);

【问题讨论】:

    标签: c# task-parallel-library restsharp cancellation dropnet


    【解决方案1】:

    因为CancellationToken是一个值类型,我们可以将它作为可选参数添加到具有默认值的API中,避免空检查,这很甜蜜。

    public Task<IRestResponse> GetThumbnailTask(
        string path, ThumbnailSize size, CancellationToken cancel = default(CancellationToken)
    ) {
        if (!path.StartsWith("/")) path = "/" + path;
        var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
        return ExecuteTask(ApiType.Content, request, cancel);
    }
    

    现在,RestSharp ExecuteAsync 方法返回封装了底层HttpWebRequestRestRequestAsyncHandle,以及Abort 方法。这就是我们取消事物的方式。

    public static Task<TResult> ExecuteTask<TResult>(
        this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken)
        ) where TResult : new()
    {
        var tcs = new TaskCompletionSource<TResult>();
        try {
            var async = client.ExecuteAsync<TResult>(request, (response, _) => {
                if (cancel.IsCancellationRequested || response == null)
                    return;
    
                if (response.StatusCode != HttpStatusCode.OK) {
                    tcs.TrySetException(new DropboxException(response));
                } else {
                    tcs.TrySetResult(response.Data);
                }
            });
    
            cancel.Register(() => {
                async.Abort();
                tcs.TrySetCanceled();
            });
        } catch (Exception ex) {
            tcs.TrySetException(ex);
        }
    
        return tcs.Task;
    }
    
    public static Task<IRestResponse> ExecuteTask(this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken))
    {
        var tcs = new TaskCompletionSource<IRestResponse>();
        try {
            var async = client.ExecuteAsync<IRestResponse>(request, (response, _) => {
                if (cancel.IsCancellationRequested || response == null)
                    return;
    
                if (response.StatusCode != HttpStatusCode.OK) {
                    tcs.TrySetException(new DropboxException(response));
                } else {
                    tcs.TrySetResult(response);
                }
            });
    
            cancel.Register(() => {
                async.Abort();
                tcs.TrySetCanceled();
            });
        } catch (Exception ex) {
            tcs.TrySetException(ex);
        }
    
        return tcs.Task;
    }
    

    最后,Lauren's implementation 将请求放入线程池,但我看不出这样做的理由——ExecuteAsync 本身是异步的。所以我不这样做。

    这就是取消 DropNet 操作。

    我还做了一些可能对您有用的调整。

    因为我无法安排 DropBox Tasks 而不求助于将 ExecuteTask 调用包装到另一个 Tasks 中,所以我决定为请求找到最佳并发级别,结果证明是 4 用于我,并明确设置:

    static readonly Uri DropboxContentHost = new Uri("https://api-content.dropbox.com");
    
    static DropboxService()
    {
        var point = ServicePointManager.FindServicePoint(DropboxContentHost);
        point.ConnectionLimit = 4;
    }
    

    我也满足于让未处理的任务异常在地狱中腐烂,所以我就是这样做的:

    TaskScheduler.UnobservedTaskException += (sender, e) => {
        e.SetObserved();
    };
    

    最后一个观察是您不应该在DropNet 返回的任务上调用Start(),因为该任务会立即开始。如果您不喜欢这样,则需要将ExecuteTask 包装在另一个不受TaskCompletionSource 支持的“真实”任务中。

    【讨论】:

      猜你喜欢
      • 2013-07-03
      • 2023-03-19
      • 2023-03-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多