I am new to Unity and C# and trying to query my Firebase Realtime Database, but the code does not block for the callback to finish.
I have tried implementing callbacks but this does not seem to work.
static public void ReadFromDb(int level, Action<int> callback)
{
int return_value = -1;
string sessionId = PlayerPrefs.GetString("SessionID");
FirebaseDatabase.DefaultInstance.GetReference("users/"+sessionId).GetValueAsync().ContinueWith(task => {
if (task.IsFaulted)
{
// Handle the error...
Debug.Log("Task faulted");
callback(return_value);
}
else if (task.IsCompleted)
{
DataSnapshot snapshot = task.Result;
string score_string = (snapshot.Child(level.ToString()).Value).ToString();
Debug.Log("score_string " + score_string);
return_value = int.Parse(score_string);
callback(return_value);
}
});
}
public void LevelComplete()
{
DatabaseCode.writeDatabase(SceneManager.GetActiveScene().buildIndex + 1, counter);
DatabaseCode.ReadFromDb(SceneManager.GetActiveScene().buildIndex + 1, (result) => {
prevlevelscore = result;
Debug.Log("result " + result.ToString());
});
prevscore = prevlevelscore;
Debug.Log("Returned value: " + prevlevelscore.ToString());
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
In LevelComplete(), Debug.Log("Returned value: " + prevlevelscore.ToString()); executes before prevlevelscore = result;
I want to make sure that the value of prevlevelscore is updated before executing Debug.Log.
Put the rest of the code inside the callback too:
public void LevelComplete()
{
DatabaseCode.writeDatabase(SceneManager.GetActiveScene().buildIndex + 1, counter);
DatabaseCode.ReadFromDb(SceneManager.GetActiveScene().buildIndex + 1, (result) => {
Debug.Log("result " + result.ToString());
prevscore = result;
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
});
}
Your problem is that your ReadFromDb method returns before being finished. You can solve this issue by putting all your code in the callback (but you won't be able to do that all the time) or you can use the async await pattern.
Make ReadFromDb async:
static public async Task ReadFromDb(int level, Action<int> callback)
{
int return_value = -1;
string sessionId = PlayerPrefs.GetString("SessionID");
await FirebaseDatabase.DefaultInstance.GetReference("users/"+sessionId).GetValueAsync().ContinueWith(task => {
if (task.IsFaulted)
{
// Handle the error...
Debug.Log("Task faulted");
callback(return_value);
}
else if (task.IsCompleted)
{
DataSnapshot snapshot = task.Result;
string score_string = (snapshot.Child(level.ToString()).Value).ToString();
Debug.Log("score_string " + score_string);
return_value = int.Parse(score_string);
callback(return_value);
}
});
}
Note the await keywork before your GetValueAsync().ContinueWith because this precise code is asynchronous and needs to be awaited if you want to hold code execution until the result has been fetched.
And in your caller:
public async Task LevelComplete()
{
DatabaseCode.writeDatabase(SceneManager.GetActiveScene().buildIndex + 1, counter);
await DatabaseCode.ReadFromDb(SceneManager.GetActiveScene().buildIndex + 1, (result) => {
prevlevelscore = result;
Debug.Log("result " + result.ToString());
});
prevscore = prevlevelscore;
Debug.Log("Returned value: " + prevlevelscore.ToString());
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
This method becomes async as well (because asynchronicity propagates). Again, the await keyword will hold on execution until the readFromDb method has finished. Which means that your data will be ready.
Related
When using the UniRx package in Unity, one can use async-await. But what's the right way to convert something like the following (this example uses a GPT-3 www request, but it could be anything)
string result1 = await textAI.GetCompletion("Albeit Einstein was");
string result2 = await textAI.GetCompletion("Susan Sarandon is");
to something that launches the GetCompletion functions simultaneously, then continues when all finished and returned their result? E.g. using pseudo-code, like this:
string result1 = null;
string result2 = null;
await Task.WhenAll({
result1 = await textAI.GetCompletion("Albeit Einstein was"),
result2 = await textAI.GetCompletion("Susan Sarandon is")
});
Debug.Log("Results: " + result1 + ", " + result2);
Thanks!
You could use the existing Task.WhenAll
async void Start()
{
Task<string> a = AsyncRoutineA(); // Here no await, you want the Task object
Task<string> b = AsyncRoutineB();
await Task.WhenAll(a, b); // add all tasks and await that one
Debug.Log(a.Result + " " + b.Result);
}
async Task<string> AsyncRoutineA()
{
await Task.Delay(1000);
return "A Done";
}
async Task<string> AsyncRoutineB()
{
await Task.Delay(2000);
return "B Done";
}
UniRx would do the same, with extra overhead so that you'd get an Observable object that you'd not use in this context.
You can do the same with coroutines:
IEnumerator Start()
{
string resultA = null;
string resultB = null;
StartCoroutine(AsyncRoutineA((result) => resultA = result));
StartCoroutine(AsyncRoutineA((result) => resultB = result));
yield return new WaitUntil(()=>
{
return !string.IsNullOrEmpty(resultA) && !string.IsNullOrEmpty(resultB);
});
Debug.Log(resultA + " " + resultB);
}
IEnumerator AsyncRoutineA(Action<string> result)
{
yield return new WaitForSeconds(1);
result.Invoke("A done");
}
IEnumerator AsyncRoutineB(Action<string> result)
{
yield return new WaitForSeconds(2f);
result.Invoke("B done");
}
I understand async javascript but aync .NET has a different approach and I still haven't got my head around it properly.
I have a list of URLs that I would like to check. I would like to check them asynchronously and get the first one that returns a certain status code. In this case I am looking for status code 401 (Unauthorized) as this indicates that it is a login challenge, which is what I am expecting. So I can't just use Task.WaitAny because I need to run some code to see which one matches my status code first.
Can anyone give me an example of how you run a callback on an aync task and then stop all the other tasks if you found what you want?
I am using .NET 4 in this project and would prefer to stick with this if possible. I have the System.Net.Http.HttpClient nuget package installed.
UPDATE:
I have put together the following code, which I have finally got to produce the correct results, except I think it is waiting for each task - missing the whole point of being async. Not sure about use of new Task() or t.Wait() within the inner task but it seem the only way to catch the exception. (Exceptions happen on DNS fail and connection timeouts - I don't know a better way to handle those than catching and ignoring the exceptions.)
Any advice on improving this code to make it actually async?
public async Task<ActionResult> Test() {
//var patterns = GetPatterns();
var patterns = "http://stackoverflow.com/,https://www.google.com,http://www.beweb.co.nz,https://outlook.office365.com/Microsoft-Server-ActiveSync,http://rubishnotexist.com".Split(",").ToList();
var httpClient = new System.Net.Http.HttpClient();
string result = "";
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken cancellationToken = source.Token;
var allTasks = new List<Task>();
foreach (var pattern in patterns) {
var url = pattern;
Task task = new Task(() => {
string answer = "";
var st = DateTime.Now;
var t = httpClient.GetAsync(pattern, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
t.ContinueWith(d => {
if (!source.IsCancellationRequested) {
if (t.IsFaulted) {
answer = "Fault - " + " " + url;
} else if (d.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) {
// found it - so cancel all others
answer = "YES - " + d.Result.StatusCode + " " + url;
//source.Cancel();
} else {
answer = "No - " + d.Result.StatusCode + " " + url;
}
}
result += answer + " ("+(DateTime.Now-st).TotalMilliseconds+"ms)<br>";
});
try {
t.Wait();
} catch (Exception) {
// ignore eg DNS fail and connection timeouts
}
});
allTasks.Add(task);
task.Start();
}
// Wait asynchronously for all of them to finish
Task.WaitAll(allTasks.ToArray());
return Content(result + "<br>DONE");
}
In the above I didn't have the cancellation part working. Here is a version including cancellation:
public async Task<ActionResult> Test2(string email) {
var patterns = GetPatterns(email);
patterns = "http://stackoverflow.com/,https://www.google.com,http://www.beweb.co.nz,https://outlook.office365.com/Microsoft-Server-ActiveSync,http://rubishnotexist.com".Split(",").ToList();
var httpClient = new System.Net.Http.HttpClient();
string result = "";
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken cancellationToken = source.Token;
var allTasks = new List<Task>();
foreach (var pattern in patterns) {
var url = pattern;
Task task = new Task(() => {
string answer = "";
var st = DateTime.Now;
var t = httpClient.GetAsync(pattern, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
t.ContinueWith(d => {
if (!source.IsCancellationRequested) {
if (t.IsFaulted) {
answer = "Fault - " + " " + url;
} else if (d.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) {
// found it - so cancel all others
answer = "YES - " + d.Result.StatusCode + " " + url;
result += answer + " (" + (DateTime.Now - st).TotalMilliseconds + "ms) <-- cancelled here <br>";
source.Cancel();
} else {
answer = "No - " + d.Result.StatusCode + " " + url;
}
} else {
answer = "cancelled - " + url;
}
result += answer + " (" + (DateTime.Now - st).TotalMilliseconds + "ms)<br>";
});
try {
t.Wait();
} catch (Exception) {
// ignore
}
});
allTasks.Add(task);
task.Start();
}
// Wait asynchronously for all of them to finish
Task.WaitAll(allTasks.ToArray());
return Content(result + "<br>DONE");
}
Use Task.WhenAll() instead, then examine the results of the tasks.
To prevent other tasks continuing once any one throws an exception, you can create a single CancellationToken (by first creating a CancellationTokenSource, then using its .Token) that you pass to all the tasks, and on failure, you cancel the token; see How to cancel and raise an exception on Task.WhenAll if any exception is raised? for more details and sample code. All the tasks then observe the token, and optionally explicitly check it occasionally and exit if it's canceled. They should also pass it on to those methods that support it, so they, in turn, can cancel quickly when the token is canceled.
Re exceptions, this answer covers them pretty well. If you want no exception thrown into the calling code, you should handle the exception within each task create instead, but then you will need to modify the above canceling mechanism accordingly. You could instead just catch the single exception that await Task.WhenAll() might throw and at that point observe all the exceptions thrown in the Task.Exception property of each task, or ignore them if that is the desired result.
Re canceling on success (from the comments) - I guess there are many ways to do it, but one could be:
using (var cts = new CancellationTokenSource())
{
var tasks = new List<Task<HttpStatusCode>>();
foreach (var url in patterns)
{
tasks.Add(GetStatusCodeAsync(url, cts.Token));
}
while (tasks.Any() && !cts.IsCancellationRequested)
{
Task<HttpStatusCode> task = await Task.WhenAny(tasks);
if (await task == HttpStatusCode.Unauthorized)
{
cts.Cancel();
// Handle the "found" situation
// ...
}
else
{
tasks.Remove(task);
}
}
}
and then put your HttpClient code in a separate method:
private static async Task<HttpStatusCode> GetStatusCodeAsync(object url, CancellationToken token)
{
try
{
// Your HttpClient code
// ...
await <things>;
// (pass token on to methods that support it)
// ...
return httpStatusCode;
}
catch (Exception e)
{
// Don't rethrow if you handle everything here
return HttpStatusCode.Unused; // (or whatever)
}
}
Let's say I have a class called Scheduler that contains a Dictionary of <UserId, Task>, the Task continuously loops and updates an internal Dictionary with Schedules for that User <UserId, Schedule> with information from a database i.e. I want to keep the information updated in real time.
I want to have a method on the Scheduler class GetScheduleForUser which checks to see if there is a Task for that User and if not it'll create the task wait til it finishes and then retrieves the Schedules for that User (lazy load it).
My question is, after the first iteration of the task I'll have a schedule available and I can just retrieve the schedule...no problem but for the first iteration I need to wait until the task is finished at least one time before retrieving the schedule.
I can just start the task and create a while loop until a certain flag is set when it's finished for the first time loop but it seems to me like there is a better way and it'll only be useful for the first iteration. Afterwards the schedule will always be available and I won't need the functionality.
Does anyone have a clean way to accomplish this?
The best solution I can think of would use TaskCompletionSource, which Eser mentioned in his comment. Here's a rough code sample with lots of console output to make it easier to follow what it's doing. I also added IDisposable to the Scheduler calss and a dictionary of CancellationTokenSource so that, when you're done with the Scheduler, it can stop all of the Tasks.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class Program
{
// Helper property to simplify console output
public static string TimeString { get { return DateTime.Now.ToString("mm:ss.fff"); } }
public static void Main(string[] args)
{
using (var scheduler = new Scheduler())
{
var userID = "1";
Console.WriteLine(TimeString + " Main: Getting schedule for first time...");
var sched1 = scheduler.GetScheduleForUser(userID);
Console.WriteLine(TimeString + " Main: Got schedule: " + sched1);
Console.WriteLine(TimeString + " Main: Waiting 2 seconds...");
System.Threading.Thread.Sleep(2000);
Console.WriteLine(TimeString + " Main: Getting schedule for second time...");
var sched2 = scheduler.GetScheduleForUser(userID);
Console.WriteLine(TimeString + " Main: Got schedule: " + sched2);
}
Console.WriteLine();
Console.WriteLine("Press any key to end . . .");
Console.ReadKey();
}
}
public class Scheduler : IDisposable
{
// Helper property to simplify console output
public static string TimeString { get { return DateTime.Now.ToString("mm:ss.fff"); } }
private Dictionary<string, Task> TasksDictionary { get; set; }
private Dictionary<string, TaskCompletionSource<bool>> TaskCompletionSourcesDictionary { get; set; }
private Dictionary<string, CancellationTokenSource> CancellationTokenSourcesDictionary { get; set; }
private Dictionary<string, string> SchedulesDictionary { get; set; }
public Scheduler()
{
TasksDictionary = new Dictionary<string, Task>();
TaskCompletionSourcesDictionary = new Dictionary<string, TaskCompletionSource<bool>>();
CancellationTokenSourcesDictionary = new Dictionary<string, CancellationTokenSource>();
SchedulesDictionary = new Dictionary<string, string>();
}
public void Dispose()
{
if (TasksDictionary != null)
{
if (CancellationTokenSourcesDictionary != null)
{
foreach (var tokenSource in CancellationTokenSourcesDictionary.Values)
tokenSource.Cancel();
Task.WaitAll(TasksDictionary.Values.ToArray(), 10000);
CancellationTokenSourcesDictionary = null;
}
TasksDictionary = null;
}
CancellationTokenSourcesDictionary = null;
SchedulesDictionary = null;
}
public string GetScheduleForUser(string userID)
{
// There's already a schedule, so get it
if (SchedulesDictionary.ContainsKey(userID))
{
Console.WriteLine(TimeString + " GetSchedule: Already had schedule for user " + userID);
return SchedulesDictionary[userID];
}
// If there's no task yet, start one
if (!TasksDictionary.ContainsKey(userID))
{
Console.WriteLine(TimeString + " GetSchedule: Starting task for user " + userID);
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
TaskCompletionSourcesDictionary.Add(userID, new TaskCompletionSource<bool>());
var task = (new TaskFactory()).StartNew(() => GenerateSchedule(userID, token, TaskCompletionSourcesDictionary[userID]), token);
TasksDictionary.Add(userID, task);
CancellationTokenSourcesDictionary.Add(userID, tokenSource);
Console.WriteLine(TimeString + " GetSchedule: Started task for user " + userID);
}
// If there's a task running, wait for it
Console.WriteLine(TimeString + " GetSchedule: Waiting for first run to complete for user " + userID);
var temp = TaskCompletionSourcesDictionary[userID].Task.Result;
Console.WriteLine(TimeString + " GetSchedule: First run complete for user " + userID);
return SchedulesDictionary.ContainsKey(userID) ? SchedulesDictionary[userID] : "null";
}
private void GenerateSchedule(string userID, CancellationToken token, TaskCompletionSource<bool> tcs)
{
Console.WriteLine(TimeString + " Task: Starting task for userID " + userID);
bool firstRun = true;
while (!token.IsCancellationRequested)
{
// Simulate work while building schedule
if (token.WaitHandle.WaitOne(1000))
break;
// Update schedule
SchedulesDictionary[userID] = "Schedule set at " + DateTime.Now.ToShortTimeString();
Console.WriteLine(TimeString + " Task: Updated schedule for userID " + userID);
// If this was the first run, set the result for the TaskCompletionSource
if (firstRun)
{
tcs.SetResult(true);
firstRun = false;
}
}
Console.WriteLine(TimeString + " Task: Ended task for userID " + userID);
}
}
Here's a fiddle to show it in action: .NET Fiddle
I have this code to load and count data from API server;
class TestNetWork
{
private Task taskFillPicker;
private List<CityItemDB> itemsCity;
private CustomPicker cpCity;
public async Task FillPicker()
{
try {
JObject res = await SuperFUNC.GET_CITY_ACTIVE_SENDER();
if(res == null){
//null
}else{
string message = res["message"].ToString();
if(message.Equals("Success")){
itemsCity.Clear();
cpCity.Items.Clear();
JArray data = (JArray)res["data"];
int count = data.Count;
for (int i = 0; i < count; i++) {
CityItemDB node = new CityItemDB();
node.cityId = Int32.Parse(data[i]["cityId"].ToString());
node.cityName = data[i]["cityName"].ToString();
itemsCity.Add(node);
cpCity.Items.Add(node.ToString());
}
}else{
//null
}
}
} catch (Exception ex) {
Debug.WriteLine (TAG + " : " + ex.StackTrace);
}
}
public TestNetWork()
{
this.itemsCity = new List<CityItemDB> ();
this.cpCity = new CustomPicker {
HeightRequest = 40,
TextColor = Color.FromHex("#5a5a5a"),
Title = "City Choose",
};
taskFillPicker = FillPicker ();
Debug.WriteLine (COUNT + " : " + itemsCity.Count);
}
}
But console print me COUNT : 0, I'm sure code get and parse json from internet is correct, picker show full data but List<CityItemDB> itemsCity count 0.
Thank for read, sorry my english not good!
You need to await the task, otherwise execution might continue before FillPicker has completed:
taskFillPicker = await FillPicker ();
As this code is in a constructor where await is not possible, I suggest moving it to a separate async method:
public async Task Init()
{
taskFillPicker = await FillPicker ();
Debug.WriteLine (COUNT + " : " + itemsCity.Count);
}
You have to write a little bit more code to construct the object now:
var n = new TestNetWork();
await n.Init();
So I am trying to add an async progress bar on a really slow and long query that inserts a bunch of rows to a database. My implementation is based off this example: http://blog.janjonas.net/2012-01-02/asp_net-mvc_3-async-jquery-progress-indicator-long-running-tasks
Here is the javascript code for the progress bar
function updateMonitor(taskId, status) {
$("#" + taskId).html("Task [" + taskId + "]: " + status);
}
//other code
if (doSend == true) {
$.post("/SendBatch/HandleBatchRequest", {
//other code
},
function (taskId) {
// Init monitors
//breakpoint here does not stop it, it never enters this somehow?
$("#monitors").append($("<p id='" + taskId + "'/>"));
updateMonitor(taskId, "Started");
// Periodically update monitors
var intervalId = setInterval(function () {
$.post("SendBatch/Progress", { id: taskId }, function (progress) {
if (progress >= 100) {
updateMonitor(taskId, "Completed");
clearInterval(intervalId);
} else {
updateMonitor(taskId, progress + "%");
}
});
}, 100);
}
,"html");
Then there is the DIV within the display part of the website
<div id="monitors"></div>
Here is how the controller looks
public SendBatchController
//some code
private static IDictionary<Guid, int> tasks = new Dictionary<Guid, int>();
public ActionResult HandleBatchRequest(
//some code
)
{
var taskId = Guid.NewGuid();
tasks.Add(taskId, 0);
var batchId = Guid.NewGuid().ToString("N");
var costd = cost.ToDecimal();
IEnumerable<BatchListModel> customers;
try
{
customers = new CustomerService(_customerRepository.Session).GetCustomers(
//some code
);
}
catch (Exception err)
{
return Json(err.Message);
}
if (doSend)
{
var sent = 0;
foreach (var c in customers)
{
try
{
var usr = _customerRepository.LoadByID(c.ID);
var message = new ComLog
{
//insertions to log
};
_comLogRepository.Save(message);
sent++;
//progress bar part inside here that is important comes here:
tasks[taskId] = sent;
}
catch (Exception e)
{
Log.WriteLine("ERR:" + e);
}
tasks.Remove(taskId);
}
return Json(taskId);
}
return Json(customers.Count() + " customers");
}
public ActionResult Progress(Guid id)
{
return Json(tasks.Keys.Contains(id) ? tasks[id] : 100);
}
This does not work. The process works in the background. It is only the div that never shows up and never gives any indication. I know this is a lot of code to read but I am really stuck and would love some input on how to fix this.
try change your updateMonitor function into this :
function updateMonitor(taskId, status) {
$("#monitors").html("Task [" + taskId + "]: " + status);
}