I planned on this post to be about UI, but I’m going to defer that until the next post. I said from the start of this series that I would document about everything about building the application from scratch, including my struggles.
And with that I want to mention something that got me scratching my head one too many times. It was with how I used LinqToTwitter. Here is the source code which you can immediately copy/paste into a blank project to reproduce:
1: public partial class MainPage : UserControl
2: {
3: private readonly TwitterContext _context = new TwitterContext();
4: private readonly ViewModel _vm1, _vm2, _vm3;
5:
6: public MainPage()
7: {
8: InitializeComponent();
9: _vm1 = new ViewModel(_context);
10: _vm2 = new ViewModel(_context);
11: _vm3 = new ViewModel(_context);
12: _vm1.Callback += () => Debug.WriteLine("Callback of VM1: " + _vm1.LocalState);
13: _vm2.Callback += () => Debug.WriteLine("Callback of VM2: " + _vm2.LocalState);
14: _vm3.Callback += () => Debug.WriteLine("Callback of VM3: " + _vm3.LocalState);
15:
16: _vm1.Start();
17: _vm2.Start();
18: _vm3.Start();
19: }
20: }
21:
22: public class ViewModel
23: {
24: private readonly TwitterContext _context;
25: public event Action Callback;
26:
27: public int LocalState;
28:
29: public ViewModel(TwitterContext context)
30: {
31: _context = context;
32: }
33:
34: public void Start()
35: {
36: var query = (from s in _context.Status
37: where s.Type == StatusType.Public && s.Count == 10
38: select s);
39: Debug.WriteLine("Hash code of ViewModel: " + query.GetHashCode());
40: query.AsyncCallback(statuses =>
41: {
42: LocalState++;
43: Debug.WriteLine("Hash code inside callback: " + GetHashCode());
44: Callback();
45: }).FirstOrDefault();
46: }
47: }
Now, if you run this, you will see that only one of the view models will get its state updated. Huh?!
How is that possible? I started getting paranoid so I even added the local state variable “just in case.”
Well, I had to look into the source code of LinqToTwitter to figure out exactly what happened. Here is the code for AsyncCallback:
public static IQueryable<T> AsyncCallback<T>(this IQueryable<T> queryType, Action<IEnumerable<T>> callback)
{
(queryType.Provider as TwitterQueryProvider)
.Context
.TwitterExecutor
.AsyncCallback = callback;
return queryType;
}
See what happened? The callback gets overwritten every time you call this method. Even though the call to FirstOrDefault() causes all 3 expressions to evaluate, only the last view model will get values because that’s the with the callback attached.
Lesson of the day: The AsyncCallback extension method for LinqToTwitter is not thread-safe.
So…the question is, how do we make it thread safe? I just replaced wrapped the AsyncCallback with another extension method:
private static readonly AutoResetEvent _twitterEvt = new AutoResetEvent(true);
public static void AsyncTwitterCallback<T>(this IQueryable<T> twitter, Action<IEnumerable<T>> callback)
{
Observable.Start(() =>
{
_twitterEvt.WaitOne();
twitter.AsyncCallback(results =>
{
try
{
callback(results);
}
finally
{
_twitterEvt.Set();
}
}).FirstOrDefault();
});
}
Hopefully upstream fixes this, or at least documents it.
No comments:
Post a Comment