WPF, TPL, Producer/Consumer Pattern - Wrong Thread Error
I'm new to TPL and WPf and have the following problem. I try to download a site in an infinite loop (here only a for loop) and add it to a Queue. The next Task takes it out and shows it in the Textblock. However I don't seem to get the right Thread for the UI although I think that I use the TaskScheduler properly.
Thank you for any help!
BlockingCollection<string> blockingCollection = new BlockingCollection<string>();
CancellationToken token = tokenSource.Token;
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task task1 = new Task(
(obj) =>
{
for (int i = 0; i < 10; i++)
{
if (token.IsCancellationRequested)
{
TxtBlock2.Text = "Task cancel detected";
throw new OperationCanceledException(token);
}
else
{
string code = i.ToString() + "\t" + AsyncHttpReq.get_source_WebRequest(uri);
blockingCollection.Add(code);
}
}
}, TaskScheduler.Default);
task1.ContinueWith(antecedents =>
{
TxtBlock2.Text = "Signalling production end";
blockingCollection.CompleteAdding();
}, uiScheduler);
Task taskCP = new Task(
(obj) =>
{
while (!blockingCollection.IsCompleted)
{
string dlCode;
if (blockingCollection.TryTake(out dlCode))
{
//the calling thread cannot access this object because a different thread owns it.
TxtBlock3.Text = dlCode;
}
}
}, uiScheduler);
WindowsBase.dll!System.Windows.Threading.Dispatcher.VerifyAccess() + 0x4a bytes
WindowsBase.dll!System.Windows.DependencyObject.SetValue(System.Windows.DependencyProperty dp, object value) + 0x19 bytes
PresentationFramework.dll!System.Windows.Controls.TextBlock.Text.set(string value) + 0x24 bytes
WpfRibbonApplication4.exe!WpfRibbonApplication4.MainWindow.Button1_Click.AnonymousMethod__4(object obj) Line 83 + 0x16 bytes C# mscorlib.dll!System.Threading.Tasks.Task.InnerInvoke() + 0x44 bytes mscorlib.dll!System.Threading.Tasks.Task.Execute() + 0x43 bytes mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) + 0x27 bytes
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool ignoreSyncCtx) + 0xb0 bytes mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) + 0x154 bytes mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) + 0x8b bytes mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() + 0x7 bytes mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() + 0x147 bytes mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() + 0x2d bytes [Native to Managed Transition]
System.InvalidOperationException was unhandled by user code
Message=The calling thread cannot access this object because a different thread owns it.
Source=WindowsBase
StackTrace:
at System.Windows.Threading.Dispatcher.VerifyAccess()
at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value)
at System.Windows.Controls.TextBlock.set_Text(String value)
at WpfRibbonApplication4.MainWindow.<>c__DisplayClass5.<Button1_Click>b__3(Object o) in C:\ ... \WpfRibbonApplication4\WpfRibbonApplication4\MainWindow.xaml.cs:line 90
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
InnerException:
Thank you alot for all of your help. I still have two question: I rewrote my code a little bit with the Task.Factory.StartNew. However my Task2 seems to cause problems. There's no error message. Seems rather like a tight loop. Of course, I didn’t figure out why? Would you be so kind and point me into the right direction again. Keep in mind that I’ve been doing C# for ~6 months and TPL for a week otherwise I wouldn’t ask you again. But with this amount of experience ... Thank you again!
Brians Code:
var task1 = new Task(
(obj) =>
Why is obj
needed?
private void Button1_Click(object sender, RoutedEventArgs e)
{
TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); BlockingCollection blockingCollection = new BlockingCollection(); CancellationTokenSource cts = new CancellationTokenSource();
Cance开发者_开发问答llationToken token = cts.Token;
Task task1 = Task.Factory.StartNew(
() =>
{
for (int i = 0; i < 10 ; i++)
{
token.ThrowIfCancellationRequested();
string code = i++.ToString() + "\t" + AsyncHttpReq.get_source_WebRequest(uriDE);
blockingCollection.Add(code);
}
}, token, TaskCreationOptions.None, TaskScheduler.Default);
task1.ContinueWith(
(antecedents) =>
{
if (token.IsCancellationRequested)
{
TxtBlock2.Text = "Task cancel detected";
}
else
{
TxtBlock2.Text = "Signalling production end";
}
blockingCollection.CompleteAdding();
}, uiTaskScheduler);
Task task2 = Task.Factory.StartNew(
() =>
{
while (!blockingCollection.IsCompleted)
{
string dlcode;
if (blockingCollection.TryTake(out dlcode))
{
TxtBlock3.Text = dlcode;
}
}
}, token, TaskCreationOptions.None, uiTaskScheduler);
}
Ok, actually I just looked at your code again and the problem is simple: You're constructing a new Task
instances manually using the constructor overload that takes a state object. There is no constructor overload that takes a TaskScheduler
.
Normally people use Task.Factory.StartNew
, so I didn't even notice that you were constructing the Tasks
manually. When you construct Task
instances manually, the proper way to specify the scheduler to run them on is to use the Start
overload that takes a TaskScheduler
instance. So in your case you would need to do:
taskCP.Start(uiTaskScheduler);
UPDATE
Ok, so now the problem with your updated code is you've effectively scheduled a Task
on UI (dispatcher) thread that is sitting there reading in a tight loop.
Now that the code is reworked, it becomes clear that you do not want to schedule task2 on the UI thread. If you want to push out notifications from there to the UI you can either call Dispatcher::BeginInvoke
as suggested by another answer or you can start a new Task using the uiTaskScheduler inside of the loop. Calling Dispatcher::BeginInvoke will have less overhead and be clearer code wise IMHO, so I recommend just doing that.
You can use Dispatcher to access UI thread like:
Dispatcher.BeginInvoke(new Action(()=>{TxtBlock2.Text = "Signalling production end";}));
Because a task runs in another thread other than UI thread, Dispatcher gives you a changes to access the thread which your UI at. MSDN gives a good explanation about it, please view the remark part at: http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx
Hope it helps.
There are a couple of problems with your code.
- There is no overload of the Task ctor that accepts a
TaskScheduler
. What you have actually done is passed theTaskScheduler
to thestate
parameter that is then picked in yourobj
variable on the lambda expression. - Because of the point above
taskCP
is actually running on the default scheduler and notuiScheduler
. - Because of the point above
taskCP
is attempting to access a UI element from a non-UI thread by modifyingTxtBlock3
. - Likewise
task1
is attemping the same by modifyingTxtBlock2
.
Here is how I would refactor the code.
var queue = new BlockingCollection<string>();
var cts = new CancellationTokenSource();
TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext();
var task1 = new Task(
() =>
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
string code = i.ToString() + "\t" + AsyncHttpReq.get_source_WebRequest(uri);
queue.Add(code);
}
});
task1.ContinueWith(
antecedents =>
{
if (token.IsCancellationRequested)
{
TxtBlock2.Text = "Task cancel detected";
}
else
{
TxtBlock2.Text = "Signalling production end";
}
queue.CompleteAdding();
}, ui);
var taskCP = new Task(
() =>
{
while (!queue.IsCompleted)
{
string dlCode;
if (queue.TryTake(out dlCode))
{
Dispatcher.Invoke(() =>
{
TxtBlock3.Text = dlCode;
}
}
}
});
task1.Start();
taskCP.Start();
Notice that ContinueWith
can accept a TaskScheduler
and that is exactly what I have done above. I also have taskCP
running on the default scheduler and then I use Dispatcher.Invoke
before accessing TxtBlock3
.
If you really want to start a Task
on a specific scheduler then pass a TaskScheduler
to the Start
method like the following.
task1.Start(TaskScheduler.Default);
Use Dispatcher.Invoke()
to invoke the code that use on UI elements from different thread. For example
string dlCode;
if (blockingCollection.TryTake(out dlCode))
{
Dispatcher.Invoke(() =>
{
TxtBlock3.Text = dlCode;
}
}
精彩评论