Click here to Skip to main content
13,633,747 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

3.5K views
5 bookmarked
Posted 14 Apr 2018
Licenced CPOL

The Foreground Await / Async Anti-Pattern

, 14 Apr 2018
Rate this:
Please Sign up or sign in to vote.
The mis-use of async/await point to an underlying lack of awareness of C# design principles, including event-driven programming.. The post The Foreground Await / Async Anti-Pattern appeared first on Marcus Technical Services..

The Foreground Await-Async Anti-Pattern

Surprising Discoveries About the Task Parallel Sequential Library

Microsoft’s C# is an amazing language. It is a powerful, conceptually brilliant high-level tool for creating applications. One unfortunate side-effect of C#’s reputation is that anything published in the language API seems like some sort of visionary insight. That certainly was the case with the confusingly named Task Parallel Library and its accompanying design pattern, await / async.

Some months back, I worked on an app that, when launched on Android devices, ran so slowly that it seemed “hung”. The IOS version did load, albeit slowly. The app was huge and poorly designed. However, if the coding had been the problem, both targeted operating systems would have failed – not just one. Something was slowing down Android in a way that IOS managed to survive, even if barely.

I searched the source code for awaits. Twelve-hundred and fifty (yup: 1,250) of them showed up and mostly tied to the foreground thread. I identified “lynch-pin” locations, like setting up view models, removed the awaits, and replaced them with traditional threaded call-backs. After that, the Android app was able to run. Under IOS, the app launched like a rocket.

On another recent project, a client complained that their Xamarin.Forms app drew jerky animations at load time.

Again, I searched out the awaits in the code. The code base was competently designed. Yet it contained 171 awaits, most of them on (or tied directly to) the foreground thread. I decided to settle the await/async  foreground issue once and for all. I created a stripped-down test app that only contained: (1) the animations in question; (2) Task.Run style threaded call-backs; and (3) zero awaits. Guess what? The animations flowed smoothly.

This issue is so serious that it qualifies as an anti-pattern: a thing that you should avoid doing.

What is Wrong with Async/Await on the Foreground Thread

If you had to select the best programmers from a random group of qualified candidates, you would be wise to focus on threading. Threading possesses two deadly features: (1) It seems like a person could figure it out intuitively; and (2) Nothing in it occurs intuitively.

Microsoft understood this when they created the Task Parallel Library (TPL). They said to themselves, “amateur programmers are making too many threading mistakes. Let’s simplify it for them.” Which they did. At an extraordinary, hidden cost.

Ten years ago, if you wanted to read a file stream, you would create something like this:

private string ReadStream()
{
}

… however, these calls tended to bog down the foreground thread. So Microsoft developed TPL and started adding the Async suffix to most calls, both local and web-based:

private Task<string> ReadStreamAsync()
{
	return Task.FromResult(string.Empty);
}

To consume these new calls, the programmer calls await (which requires the term async on the method signature):

private async Task<bool> DataExists()
{
	var retStream = await ReadStreamAsync();
	return (retStream.Length > 0);
}

Any consumer of this method can be sure that while reading the stream, the app’s UI is “free” — the user can continue to tap UI elements and receive responses – because the activity does not occupy the foreground. The stream operates on a background thread.

There are some dilemmas with this new philosophy. The only way to consume an awaitable Task is asynchronously, chaining into endless async methods. These async calls eventually “take over” the app through sheer numbers. They entangle the code like a clinging vine. The only way to end the chain is to jump off a “cliff” – a rooted startup location such as a constructor:

public class MyFirstClass
{
	private bool _localDataExistsBool;

	public MyFirstClass()
	{
		// WARNING issued by Resharper and compiler: we are failing to await!
		VerifyDataExists();
	}

	public async Task<bool> DataExists()
	{
		var retStream = await ReadStreamAsync();
		return (retStream.Length > 0);
	}

	public async Task VerifyDataExists()
	{
		_localDataExistsBool = await DataExists();
	}

	private string ReadStream()
	{
		return string.Empty;
	}

	private Task<string> ReadStreamAsync()
	{
		return Task.FromResult(string.Empty);
	}
}

 … yet that is illegal because a constructor is not async! So the code editor complains that the method VerifyDataExists is not awaited and so will run asynchronously. After all of that trouble!

This gives rise to exotic techniques to call an async method legally from a non-async location:

public class MyFirstClass
{
	// ...code as before

	public MyFirstClass()
	{
		try
		{
			Task.Run(async () =>  
			{ 
				await VerifyDataExists(); 
			});
		}
		catch
		{
			Debug.WriteLine("Error in Task.Run at constructor");
		}
	}

	// ...code as before

	private void ConstructClass()
	{
		// Do some complex construction that relies on _localDataExistsBool
	}
}

… yet that also produces new dilemmas. Task.Run asks to run the task when the CPU is free. That takes an unknown period. What if we rely on the result? In the code below, ConstructClass occurs before VerifyDataExists because that is a threaded call. ConstructClass, therefore, occurs “too soon”, before the state is known:

public class MyFirstClass
{
	// ...code as before

	public MyFirstClass()
	{
		try
		{
			Task.Run(async () =>  
			{ 
				await VerifyDataExists(); 
			});

			// Occurs immediately; VerifyDataExiusts has not completed yet!
			ConstructClass();
		}
		catch
		{
			Debug.WriteLine("Error in Task.Run at constructor");
		}
	}

	// ...code as before

	private void ConstructClass()
	{
		// Do some complex construction that relies on _localDataExistsBool
	}
}

 The only answer is to run the construction after the Task.Run thread has finished. One can use a call-back, or just do it in linear order after the await:

public class MyFirstClass
{
	// ...code as before

	public MyFirstClass()
	{
		try
		{
			Task.Run(async () =>  
			{ 
				await VerifyDataExists();

				// Occurs in the proper order, after VerifyDataExists has completed
				ConstructClass();
			});
		}
		catch
		{
			Debug.WriteLine("Error in Task.Run at constructor");
		}
	}

	// ...code as before

	private void ConstructClass()
	{
		// Do some complex construction that relies on _localDataExistsBool
	}
}

 .. which creates the next dilemma: what if other classes are being constructed – and have an extreme reliance on – the state of this class? What is this class’s state at instantiation? It is not yet created because its construction has been deferred to the thread. So it is unreliable from anyone else’s perspective. The point at which it will become reliable is also not known. One would have to raise an event to announce that:

public class MySecondClass
{
	private bool _localDataExistsBool;

	public MySecondClass()
	{
		try
		{
			Task.Run(async () =>  
			{ 
				await VerifyDataExists();
				ConstructClass();
				IAmNowReliable?.Invoke(this, this);
			});
		}
		catch
		{
			Debug.WriteLine("Error in Task.Run at constructor");
		}
	}

	public event EventHandler<object> IAmNowReliable;

	public async Task<bool> DataExists()
	{
		var retStream = await ReadStreamAsync();
		return (retStream.Length > 0);
	}

	public async Task VerifyDataExists()
	{
		_localDataExistsBool = await DataExists();
	}

	private void ConstructClass()
	{
		// Do some complex construction that relies on _localDataExistsBool
	}

	private Task<string> ReadStreamAsync()
	{
		return Task.FromResult("testing123");
	}
}

Now the app must be smart enough to assume that all classes are unreliable until they raise their IAmReliable event. What could possibly go wrong with that? Convolution. Complexity. Bugs.

One can see that consuming async/await is not efficient holistically. To see how perverse this is, consider taking the app mentioned earlier that contained 1250 awaits. How was I supposed to remove those? Every method was async, so those would have to go. But what about the Task<bool> signatures? Would I also have to remove all of those? What if they were interface contracts? Should I have removed all of those as well?

In short: async/await is an invasive, complex, all-or-nothing approach to threading. And when you are done, at least in this example, what have you accomplished?

  • You began by trying to perform a task using async/await on the foreground thread.
  • You then had to push that to a thread because the constructor did not allow an async signature.

In other words, you just created a threaded callback, which is the very thing that async/await was supposed to eliminate!

When they released TPL, Microsoft should have – minimally – provided intrinsic async signatures for all constructors. That would have at least allowed async/await to operate as described. It would have been consistent and responsible, which would have garnered some respect from me. It would not have fixed anything I describe in this article, sadly.

What I am discussing here is what happens when you use await/async as Microsoft has instructed: to “leave the foreground thread free” by making unconditional calls to await/async on the foreground thread. So for instance:

public class MyPage : ContentPage
{
	private MyDeviceViewModel _viewModel;

	public MyPage()
	{
		var button = new Button()
		{
			Text = "Start Up Device",
			HorizontalOptions = LayoutOptions.Center,
			VerticalOptions = LayoutOptions.Center
		};

		Content = button;

		BindingContextChanged += (s, e) =>
		{
			_viewModel = BindingContext as MyDeviceViewModel;

			if (_viewModel != null)
			{
				button.Command = _viewModel.RetryStartDeviceCommand;
			}
		};
	}

	// Added async signature.
	protected override async void OnAppearing()
	{
		// This call then cascades into dozens of other awaits, all on the foreground thread.
		if (_viewModel != null)
		{
			await _viewModel.InitializeViewModel();
		}
	}
}

 This design pattern is published everywhere on the Internet. It is the pattern used by the programmers who wrote both of the afore-mentioned ship-wreck examples, where countless awaits occurred on the foreground thread, and the apps ground to a halt.

On the Foreground, a Stopped Thread Is a Blocked Thread (At Least Functionally)

Let’s say that you are a runner who also loves to throw boomerangs. You take great pride in this. You practice every day. You run down a woodland trail. You stop. You throw ten boomerangs. They fly various distances; they arrive back randomly and unpredictably. You wait for all ten boomerangs to land at your feet, pack them up, and start running down the trail again.

On your back, in bold letters, you proudly proclaim your identity:

AWAIT / ASYNC

This is because every call to Await / Async does something surprising: exactly what it says it is going to do. It waits while a threaded task runs, and after that task completes, it continues. So what’s the problem with that? The answer is that you only have one foreground thread in your app to support the entire UI and all of its activities. If you delay that thread in any way, your app loads more slowly. 

Let’s say that you are out practicing your await/async design pattern on the trail. You run to a clearing and stop. You throw ten boomerangs. Then something amazing happens. A second runner passes you, at full gait, throwing boomerangs as they go. They never stop. Instead, they are so skilled at throwing the boomerangs that they can catch them out in front of themselves as they run.

So who will finish this course first? The other runner. The sign on their back reads:

THREADED CALLBACK

You might argue: “when I am stopped, waiting for my boomerangs, I can do anything I want. I can wave my arms. I can jump up and down.

Yes, but to what end? The runner who passed you has a real purpose: to finish the course as soon as possible. They are serving their user. They are not wasting the user’s time.

What does it matter that during await/async, the keyboard is free, and the user can tap on it, or draw shapes? Is that a real-world scenario? Microsoft says that you have not blocked the foreground thread. Yet the app loads at a snail’s pace. Who cares if the foreground thread is blocked or not if the app is stuck? The user will not understand the nuances of your victory. They will give up and uninstall your app.

Here is an example of the tried-and-true threaded callback:

We are a view model. We do not wait for our page to ask us to initialize; we do that ourselves. We act independently of other classes so we can do so in parallel. The foreground thread is unaffected by us until or unless we need to bind or assign any of our values to a UI element on that foreground thread:

public class MyViewModel
{

	public MyViewModel()
	{
		try
		{
			//Use the Task Parallel Library to run independent threads with no await – these do not hamper the foreground thread.
			var tasks = new [] 
			{
				SomeTask(),
				SomeOtherTask(),
				YetAnotherTask()
			};

			Task.WhenAll(tasks);
		}
		catch
		{
			Debug.WriteLine("Error in Task.Rur");
		}
	}

	public ObservableCollection<string> UIBoundList { get; private set; }

	// Not called on a foreground thread.
	public async Task SomeTask()
	{
		var list = await SomeWebServiceOperation();

		// Use this trick to come back from a threaded calld.
		Device.BeginInvokeOnMainThread(() => { UIBoundList = new ObservableCollection<string>(list); });
	}

	public Task SomeOtherTask()
	{
		return Task.CompletedTask;
	}

	public Task YetAnotherTask()
	{
		return Task.CompletedTask;
	}

	private Task<string[]> SomeWebServiceOperation()
	{
		return Task.FromResult(new [] { "dog", "cat", "tree" });
	}

	public async Task InitializeViewModel()
	{
		await Task.Delay(1000);
	}
}

As the view model constructs, the UIBoundList is declared and bound. Neither holds the other up. The foreground thread is unblocked and unstopped. If you want to run animations, for instance, those appear smoothly.

Corner case: If the view model finishes first, it is possible that the notification system should fire an event to refresh the page’s data bindings. I did not include that code here, as it is obvious.

Await / Async Calls on a Thread Always Occur Sequentially – Not in Parallel

More often than not, a foreground async/await is an anti-pattern that slows an app down. C# operates under a Single Threaded Apartment (STA) paradigm, which means that only one thing can happen on the foreground thread at a time. So when a programmer writes this code:

public class MyViewModel
{
	// ...code as before<br>
	public async Task InitializeViewModel()
	{
		await Task.Delay(1000);
	}

	// ...code as before
}

 … and it is consumed as expected:

public class MyPage : ContentPage
{
	private MyDeviceViewModel _viewModel;

	public MyPage()
	{
		var button = new Button()
		{
			Text = "Start Up Device",
			HorizontalOptions = LayoutOptions.Center,
			VerticalOptions = LayoutOptions.Center
		};

		Content = button;

		BindingContextChanged += (s, e) =>
		{
			_viewModel = BindingContext as MyDeviceViewModel;

			if (_viewModel != null)
			{
				button.Command = _viewModel.RetryStartDeviceCommand;
			}
		};
	}

	// Async signature is legal here
	protected override async void OnAppearing()
	{
		// This call then cascades into dozens of other awaits, all on the foreground thread.
		if (_viewModel != null)
		{
			await _viewModel.InitializeViewModel();
		}
	}
}

 … then the Task.Delay occurs on the foreground thread. Nothing else can occur on that thread for 1000 milliseconds. The keyboard is indeed free, but the app is functionally “hung”.

It gets worse. In this next example, the view model relies on the creation of a device:

public class MyDeviceViewModel
{
	private SomeDevice _device;

	public MyDeviceViewModel()
	{
		RetryStartDeviceCommand = new Command(async () => { await StartDevice(); });
	}

	// Called by the PageView at its construction
	public async Task InitializeViewModel()
	{
		// Benignly ask to start the device – foreground await
		await StartDevice();

		// This call relies on the device Feature that fails to construct.
		await SomeVeryLongRunningTask(_device.Feature);
	}

	private Task<SomeDevice> StartDevice()
	{
		// A seemingly harmless request to set up a device goes wrong; the foreground thread is now hopelessly stopped, though "unblocked".  The app will never construct!
		_device = new SomeDevice();
		return Task.FromResult(_device);
	}

	private async Task SomeVeryLongRunningTask(DeviceFeature feature)
	{
		await Task.Delay(5000);
	}
}

 The SomeDevice class, deriving from a base class, overrides and awaits in a typical “branched” scenario.

/// <remarks>
/// Instantiated on the foreground thread.
/// </remarks>
public class SomeDevice : BaseDevice
{
	private bool _isFeatureCreated;

	// Occurs automatically as a part of the device start-up; cannot be controlled; occurs on the foreground thread
	protected override async void RequestFeatureCreation()
	{
		// This while loop runs *forever* since the device has unanticipated problems starting up.  
		// Yet it is awaited on the foreground thread. So it stops the app entirely.  
		// The user, meanwhile, can tap the keyboard at will, since the app is not technically "blocked".
		while (!_isFeatureCreated)
		{
			_isFeatureCreated = await CreateFeature();
		}
	}

	public DeviceFeature Feature { get; set; }

	private async Task<bool> CreateFeature()
	{
		await Task.Delay(1000);

		// For demonstration purposes, this function always returns false.  
		// This aggravates our example and causes errors.
		return false;
	}
}

Programmers assume that the Task Parallel Library is so named to indicate that its activities always run in parallel and that it can “do no harm”. So they routinely use the async/await pattern in every situation that arises. In the example above, all calls exist in the foreground, each one contending for the foreground thread. Only one can win. The others must follow. If any of these calls hangs, the others must wait *forever*. Yet the app does not appear hung to the user because they can tap the screen or their keyboard and see a response. This is where things get interesting.

public class MyDeviceViewModel
{
	private SomeDevice _device;

	public MyDeviceViewModel()
	{
		RetryStartDeviceCommand = new Command(async () => { await StartDevice(); });
	}

	// Called by the PageView at its construction
	public async Task InitializeViewModel()
	{
		// Benignly ask to start the device – foreground await
		await StartDevice();

		// This call relies on the device Feature that fails to construct.
		await SomeVeryLongRunningTask(_device.Feature);
	}

	private Task<SomeDevice> StartDevice()
	{
		// A seemingly harmless request to set up a device goes wrong; the foreground thread is now hopelessly stopped, though "unblocked".  The app will never construct!
		_device = new SomeDevice();
		return Task.FromResult(_device);
	}

	private async Task SomeVeryLongRunningTask(DeviceFeature feature)
	{
		await Task.Delay(5000);
	}

	// A clever programmer has decided to add a button to the UI to retry if the device appears to fail to start up. The button binds to this command.
	public ICommand RetryStartDeviceCommand { get; }
}

 The app is running and stopped because the device won’t start. The user has keyboard control – which is a good thing, right? So the user taps the button to retry the device. This makes a recursive call to the already-hung StartDevice() method.

The so-called “benefit” of running async/await to allow the “foreground to be free” has just allowed a user to fall into the embrace of death: recursion.

Even if the programmer never accidentally recurs, and the user behaves perfectly, the attempt for the CPU to manage these complex sequential awaits is extremely labor-intensive. One cannot expect an app to behave well under these circumstances.

The app mentioned earlier with 1,250 awaits featured only 20 visible pages. By straight math, every page tried to suck up 62 awaits, many of them vying for the same foreground thread. That is why the app would not run.

Valid Uses of the Async/Await Foreground Design Pattern

There are a few cases where you can use this pattern effectively. The tests are:

  1. The user must expect to wait. For example, if they are logging in and have just entered their password, they expect a *brief* wait while you authorize them.
  2. There must be a clear reason to wait. With a password, you cannot proceed without finding out if the user should be allowed to open the app. Challenge yourself: is this await absolutely necessary?
  3. There must be no other way to proceed. The answer is probably “no” for the password case, but most other cases can easily be solved using threaded call-backs and a little creativity. After all, C# is an event-driven system.

Don’t forget: when I say that the user “expects to wait”, I mean: they expect to wait for the minimal time necessary for the narrowest possible contract that you have with them. If it is logging in, that is only about 2 seconds. You cannot chain other activities onto this, or the user will feel that your app is “freeloading” on their precious time.

How to Manage Successive Foreground Awaits

To cure the cited constructor example, we could create an event in the SomeDevice class to let us know if it succeeds. We could then monitor the event to make sure we respond to success or failure. However, design-wise, this whole approach is convoluted. The real take-away is: watch out for awaiting a constructor using any means whatsoever including the ever-popular BeginInvokeOnMainThread:

var dataExists = false;

Device.BeginInvokeOnMainThread(async () => dataExists = await DataExists());

 Awaiting a constructor says that you do not know when an object will become valid. That is a huge problem for an app.

The same dilemma occurs on any of the aforementioned “cliffs” where an await-async pattern originates:

  • An event handler that you did not write and therefore lacks a Task signature
  • An override that you did not write and therefore lacks a Task signature

Each one of these produces an unknown app progress state. If you listen to an async method using Task.Run, your app will not hang or become blocked. You will have to carefully manage the “flow” to allow for all of the uncertainty introduced by threaded call-backs.

An Interesting Case – Event-Based Requests to Foreground Await/Async

During my testing, I did find one way to use await/async in a way that behaves similarly to Task.Run. I have never actually seen it in an enterprise coding environment, but it works. You just have to use an event to request any series of await/asyncs that are both foreground-based and also that occur in a batch. This scenario worked almost as fast as Task.Run.

Note that in the normal use of an event, the await chain is “broken” because you are not awaiting the event’s original raised location. Instead, you await event’s subscribed location. Remember the discussion about not knowing the state of an object at startup? An event defers this decision, so cannot accurately report the object state.

Here is an example of this approach (see the attached solution for more details):

public bool IsAnimating
{
	get => _isAnimating;

	set
	{
		if (_isAnimating == value)
		{
			return;
		}

		_isAnimating = value;

		IsAnimatingChanged?.Invoke(_isAnimating);

		if (_isAnimating)
		{
			_threadSafePopulatedSuccessCount.WriteStoredValue(0);

			TimeToLoad = default(TimeSpan);

			_stopwatch.Reset();

			_stopwatch.Start();

			// Raise the event to ask for data loads
			RequestDataLoad?.Invoke();
		}
	}
}

 The user taps a button; the button sets the IsAnimating property; the property raises an event requesting new data.

At the view model that hosts the data, the constructor looks like this:

public BalloonListCollectionViewModel(bool isForeground)
{
	SubViewModels = new List<IBalloonListViewModel>();

	for (var row = 0; row < FakeStringService.LIST_COUNT; row++)
	{
		var newList = new BalloonListViewModel(isForeground, row);
		newList.PopulationCompleted+= HandleAnyListPopulationCompleted;

		RequestDataLoad += async () =>
		{
			await newList.HandleListLoadRequest();
		};

		SubViewModels.Add(newList);
	}
}

 If the listening method, HandleListLoadRequest, runs a single await for the data, and if it does not otherwise branch, then the load time is similar that of Task.Run. In essence, this is a pseudo Task.WhenAll by its structure.

In my actual test code, I disregard these precautions and branch the awaits, which causes the app to drag quite badly at run-time.

Takeaways

These examples might seem simplistic, but they do reflect coding errors from enterprise Xamarin apps. The code here also exposes the dilemma of a “free” user interface during await/async. That freedom just invites the user to pile up further await/async calls by tapping various buttons or widgets.

If you have to await anything complex or “branchy”, consider laying a transparent shield over the UI to prevent accidental button taps. This leaves the phone feeling “free” while protecting the app against recursion or inadvertent branching.

If your code contains a lot of awaitable Task method signatures, move these to a background thread and call Task.WhenAll, which is truly parallel.

Generally, await/async on a background thread is much better than on the foreground thread. This approach does not delay the app’s construction, and also behaves well with animations.

If you cannot avoid a foreground await, do *not* tie this to other long awaits, as the entire chain will occur sequentially – not in parallel.

Hard Proofs

I created a Xamarin.Forms mobile app to demonstrate my points. The app shows a series of lists that load data while displaying an animation. On one side of the screen, we await using Microsoft’s published code samples. The total load time is enormous. The animations are extremely sticky. On the other side, we use the guidance issued here. The load time is minuscule. The animations are smooth in comparison.

 All of the code snippets here are also included in the sample solution, though not used in the demo. 

The code is published as open source and without encumbrance.

Xamarin Annoyances on GitHub

Closing Remarks

The mis-use of async/await point to an underlying lack of awareness of C# design principles, including event-driven programming. The examples described earlier, showing failed foreground awaits, are all quite “linear”. They look like someone with a Visual Basic or JavaScript background wrote them.

The safe examples, using threaded call-backs and occasional background awaits, are “event-like”: we know nothing at startup, so must wait for objects to carry out their internal processes before we can rely on them. So we must listen to – and respond carefully – to events. This is one of C#’s fundamental design principles. C# is not linear. It is behavioral.

The post The Foreground Await / Async Anti-Pattern appeared first on Marcus Technical Services.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

marcusts
Software Developer (Senior) Marcus Technical Services, Inc.
United States United States
I have been designing and coding software applications for 25 years.

Lately I have been obsessing about Xamarin.

If you enjoy my articles, send some work my way: marcus@marcusts.com.

You may also be interested in...

Comments and Discussions

 
QuestionWhat about await Task.Run(...) ? Pin
gassus16-Apr-18 21:21
membergassus16-Apr-18 21:21 
AnswerRe: What about await Task.Run(...) ? Pin
marcusts20-Apr-18 11:27
membermarcusts20-Apr-18 11:27 
QuestionPlatform Dependent Pin
Donald Wingate16-Apr-18 16:43
memberDonald Wingate16-Apr-18 16:43 
AnswerRe: Platform Dependent Pin
Nick Polideropoulos17-Apr-18 10:03
memberNick Polideropoulos17-Apr-18 10:03 
AnswerRe: Platform Dependent Pin
marcusts20-Apr-18 11:25
membermarcusts20-Apr-18 11:25 
Question.Getawaiter().Getresult() Pin
Sacha Barber15-Apr-18 21:08
mvpSacha Barber15-Apr-18 21:08 
AnswerRe: .Getawaiter().Getresult() Pin
Ehsan Sajjad16-Apr-18 4:23
mvpEhsan Sajjad16-Apr-18 4:23 
GeneralRe: .Getawaiter().Getresult() Pin
Sacha Barber16-Apr-18 11:10
mvpSacha Barber16-Apr-18 11:10 
GeneralRe: .Getawaiter().Getresult() Pin
Nick Polideropoulos16-Apr-18 11:40
memberNick Polideropoulos16-Apr-18 11:40 
GeneralRe: .Getawaiter().Getresult() Pin
Ehsan Sajjad16-Apr-18 20:07
mvpEhsan Sajjad16-Apr-18 20:07 
GeneralRe: .Getawaiter().Getresult() Pin
Nick Polideropoulos17-Apr-18 9:46
memberNick Polideropoulos17-Apr-18 9:46 
SuggestionSome async/await suggestions. Pin
Nick Polideropoulos14-Apr-18 5:03
memberNick Polideropoulos14-Apr-18 5:03 
GeneralRe: Some async/await suggestions. Pin
George Swan16-Apr-18 23:16
memberGeorge Swan16-Apr-18 23:16 
GeneralRe: Some async/await suggestions. Pin
Nick Polideropoulos17-Apr-18 9:53
memberNick Polideropoulos17-Apr-18 9:53 
GeneralRe: Some async/await suggestions. Pin
marcusts20-Apr-18 11:32
membermarcusts20-Apr-18 11:32 
GeneralRe: Some async/await suggestions. Pin
marcusts20-Apr-18 11:30
membermarcusts20-Apr-18 11:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03-2016 | 2.8.180712.1 | Last Updated 14 Apr 2018
Article Copyright 2018 by marcusts
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid