我有一个异步方法从数据库中提取一些数据。此操作相当昂贵,并且需要很长时间才能完成。因此,我想缓存方法的返回值。但是,在初始执行有机会返回并将其结果保存到缓存之前,异步方法可能会被多次调用,从而导致对这种昂贵操作的多次调用。
为了避免这种情况,我目前正在重复使用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();
也许这与结果没有立即枚举的事实有关?
答案 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;
}
}
您的原始代码有可能发生这种情况:
_getDataTask == null
,并开始构建任务_getDataTask == null
,并开始构建任务最终会有两个正在运行的任务实例。
答案 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的作者)