使用任务可以避免多次调用昂贵的操作并缓存其结果

时间:2014-09-11 21:34:01

标签: c# multithreading task-parallel-library async-await task

我有一个异步方法从数据库中提取一些数据。此操作相当昂贵,并且需要很长时间才能完成。因此,我想缓存方法的返回值。但是,在初始执行有机会返回并将其结果保存到缓存之前,异步方法可能会被多次调用,从而导致对这种昂贵操作的多次调用。

为了避免这种情况,我目前正在重复使用Task,如下所示:

public class DataAccess
{
    private Task<MyData> _getDataTask;

    public async Task<MyData> GetDataAsync()
    {
        if (_getDataTask == null)
        {
            _getDataTask = Task.Run(() => synchronousDataAccessMethod());
        }

        return await _getDataTask;
    }
}

我的想法是,对GetDataAsync的初始调用将启动synchronousDataAccessMethod中的Task方法,以及Task之前对此方法的任何后续调用只需等待已经运行的Task,自动避免多次调用synchronousDataAccessMethod。私有GetDataAsync完成后对Task的调用将导致等待Task,这将立即返回初始执行中的数据。

这似乎有效,但我有一些奇怪的性能问题,我怀疑这可能与这种方法有关。具体来说,等待_getDataTask完成后需要几秒钟(并锁定UI线程),即使未调用synchronousDataAccessMethod调用。

我是否误用async / await?我有没有看到隐藏的问题?有没有更好的方法来实现理想的行为?

修改

以下是我如何称呼此方法:

var result = (await myDataAccessObject.GetDataAsync()).ToList();

也许这与结果没有立即枚举的事实有关?

4 个答案:

答案 0 :(得分:6)

如果你想在调用堆栈中等待它,我想你想要这个:

public class DataAccess
{
    private Task<MyData> _getDataTask;
    private readonly object lockObj = new Object();

    public async Task<MyData> GetDataAsync()
    {
        lock(lockObj)
        {
            if (_getDataTask == null)
            {
                _getDataTask = Task.Run(() => synchronousDataAccessMethod());
            }
        }
        return await _getDataTask;
    }
}

您的原始代码有可能发生这种情况:

  • 线程1看到_getDataTask == null,并开始构建任务
  • 线程2看到_getDataTask == null,并开始构建任务
  • 线程1完成构建任务,该任务启动,并且线程1等待该任务
  • 线程2完成构建任务,该任务启动,并且线程2等待该任务

最终会有两个正在运行的任务实例。

答案 1 :(得分:1)

使用锁定功能可以防止多次调用数据库查询部分。 Lock将使其成为线程安全的,这样一旦它被缓存,所有其他调用将使用它而不是运行到数据库进行实现。

lock(StaticObject) // Create a static object so there is only one value defined for this routine
{
    if(_getDataTask == null)
    {
         // Get data code here
    }
    return _getDataTask
}

答案 2 :(得分:1)

请将您的功能改写为:

public Task<MyData> GetDataAsync()
{
    if (_getDataTask == null)
    {
        _getDataTask = Task.Run(() => synchronousDataAccessMethod());
    }

    return _getDataTask;
}

这不应该改变使用此功能可以完成的所有事情 - 您仍然可以await返回任务!

请告诉我这是否有所改变。

答案 3 :(得分:0)

稍微回答这个问题,但是有一个名为LazyCache的开源库,它将在两行代码中为您完成此操作,最近更新它以处理缓存任务,仅适用于这种情况。它也可以在nuget上使用。

示例:

#include <cassert>
#include <iostream>
#include <limits>
#include <sstream>

bool StreamEndsWithNewline(std::basic_istream<char>& stream) {
    const auto Unlimited = std::numeric_limits<std::streamsize>::max();
    bool result = false;
    if(stream) {
        if(std::basic_ios<char>::traits_type::eof() != stream.peek()) {
            if(stream.seekg(-1, std::ios::end)) {
                char c;
                result = (stream.get(c) && c == '\n');
                stream.ignore(Unlimited);
            }
            else {
                stream.clear();
                while(stream && stream.ignore(Unlimited, '\n')) {}
                result = (stream.gcount() == 0);
            }
        }
        stream.clear();
    }
    return result;
}

int main() {
    std::cout << "empty\n";
    std::istringstream empty;
    assert(StreamEndsWithNewline(empty) == false);

    std::cout << "empty_line\n";
    std::istringstream empty_line("\n");
    assert(StreamEndsWithNewline(empty_line) == true);

    std::cout << "line\n";
    std::istringstream line("Line\n");
    assert(StreamEndsWithNewline(line) == true);

    std::cout << "unterminated_line\n";
    std::istringstream unterminated_line("Line");
    assert(StreamEndsWithNewline(unterminated_line) == false);

    std::cout << "Please enter ctrl-D: (ctrl-Z on Windows)";
    std::cout.flush();
    assert(StreamEndsWithNewline(std::cin) == false);
    std::cout << '\n';

    std::cout << "Please enter Return and ctrl-D (ctrl-Z on Windows): ";
    std::cout.flush();
    assert(StreamEndsWithNewline(std::cin) == true);
    std::cout << '\n';

    return 0;
}

它默认内置锁定,因此可缓存方法每次缓存未命中时只执行一次,并且它使用lamda,因此您可以一次性“获取或添加”。它默认为20分钟滑动到期,但您可以在其上设置您喜欢的任何缓存策略。

有关缓存任务的更多信息位于api docs,您可能会发现sample app to demo caching tasks很有用。

(免责声明:我是LazyCache的作者)