I'm writing a Portable Class Library that is going to be used by WPF, Windows Phone and possibly WinRT apps and I'm doing some work on background threads that occasionally need to call back to the UI. I instantiate the classes doing this in the UI thread, so I can easily save the SynchronizationContext and use it to call back to the UI.
However, in PCL, SynchronizationContext.Send() is obsolete, because it's not supported by WinRT and SynchronizationContext.Post() (which runs asynchronously) is not always appropriate.
I figured I'd just wait until the delegate passed to Post() is run, but all my attempts ended with a deadlock if Post() was invoked from the same thread the saved SynchronizationContext referred to.
Now I've managed to fix this by checking if it's the same thread and just simply calling my delegate if it is, but the checks are incredibly ugly involving reflecting out the value of private fields of the API, so I thought someone could help me find a more proper way.
Here is my current code if you'd like to see some gore:
/// <summary>
/// Invokes the passed callback on this SynchronizationContext and waits for its execution. Can be used even if
/// SynchronizationContext.Send is not available. Throws the exceptions thrown in the delegate.
/// </summary>
/// <param name="context">the context to run the method</param>
/// <param name="d">the method to run</param>
/// <param name="state">the parameter of the method to run</param>
public static void InvokeSynchronized( this SynchronizationContext context, SendOrPostCallback d, object state )
{
if ( !context.Match( SynchronizationContext.Current ) )
{
ManualResetEvent waitHandle = new ManualResetEvent( false );
Exception error = null;
// replicate SynchronizationContext.Send with .Post as Send is obsolete in the Portable Class Library
context.Post( ( o ) =>
{
try
{
d( o );
}
catch ( Exception exc )
{
error = exc;
}
finally
{
waitHandle.Set();
}
},
state );
waitHandle.WaitOne();
if ( error != null )
{
throw error;
}
}
else
{
d( state );
}
}
/// <summary>
/// Checks if the two SynchronizationContexts refer to the same thread
/// </summary>
/// <param name="sc1"></param>
/// <param name="sc2"></param>
/// <returns></returns>
public static bool Match(this SynchronizationContext sc1, SynchronizationContext sc2)
{
if ( sc2 == null )
{
return false;
}
else if ( sc1 == sc2 || sc1.Equals(sc2) )
{
return true;
}
// check if the two contexts run on the same thread
// proper equality comparison is generally not supported, so some hacking is required
return sc1.FindManagedThreadId() == sc2.FindManagedThreadId();
}
/// <summary>
/// Finds the ManagedThreadId of the thread associated with the passed SynchronizationContext
/// </summary>
/// <param name="sc"></param>
/// <returns></returns>
public static int FindManagedThreadId(this SynchronizationContext sc)
{
// here be dragons
try
{
BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
switch ( sc.GetType().FullName )
{
case "System.Windows.Threading.DispatcherSynchronizationContext":
// sc._dispatcher.Thread.ManagedThreadId
var _dispatcher_field = sc.GetType().GetField( "_dispatcher", bindFlags );
var _dispatcher_value = _dispatcher_field.GetValue( sc );
var Thread_property = _dispatcher_value.GetType().GetProperty( "Thread", bindFlags );
var Thread_value = Thread_property.GetValue( _dispatcher_value, null ) as Thread;
return Thread_value.ManagedThreadId;
}
}
catch ( Exception e )
{
throw new InvalidOperationException( "ManagedThreadId could not be obtained for SynchronizationContext of type " + sc.GetType().FullName, e );
}
throw new InvalidOperationException( "ManagedThreadId not found for SynchronizationContext of type " + sc.GetType().FullName );
}
Thanks!
I think that SynchronizationContext.Send is being deprecated by Microsoft for a good reason. They really want the new Windows Store and WP8 apps to fully embrace asynchronous programming model and make the blocking code a thing of the past.
So, something that was:
void SendDataToUI()
{
_synchronizationContext.Send(_callback, data);
}
Should now become:
async Task SendDataToUIAsync()
{
var tcs = new TaskCompletionSource<object>();
_synchronizationContext.Post(a =>
{
try
{
_callback(a);
tcs.SetResult(Type.Missing);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}, data);
await tcs.Task;
}
That said, I suppose you have your own good reasons to use SynchronizationContext.Send in your PCL library.
The first part of your logic looks good, and you could cut off the reflection part of it by simply memorizing the Thread.CurrentThread.ManagedThreadId of the UI thread, at the same place where you memorize the SynchronizationContext.Current of the UI thread. Then in your implementation of InvokeSynchronized you just compare it to the Thread.CurrentThread.ManagedThreadId of the current thread, and use waitHandle.WaitOne() if your are on non-UI thread.
Related
I'm using the swagger-codegen to generate c# client, however noticing that the sortParamsByRequiredFlag is not applied to model generation.
For example, here is a sample config file:
{
"packageVersion" : "1.0.0",
"sortParamsByRequiredFlag": true,
"optionalProjectFile" : false
}
Here is the generated truncated code for model's constructor:
/// <summary>
/// Initializes a new instance of the <see cref="V2alpha1CronJobSpec" /> class.
/// </summary>
/// <param name="ConcurrencyPolicy">Specifies how to treat concurrent executions of a Job. Defaults to Allow..</param>
/// <param name="FailedJobsHistoryLimit">The number of failed finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified..</param>
/// <param name="JobTemplate">Specifies the job that will be created when executing a CronJob. (required).</param>
/// <param name="Schedule">The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. (required).</param>
/// <param name="StartingDeadlineSeconds">Optional deadline in seconds for starting the job if it misses scheduled time for any reason. Missed jobs executions will be counted as failed ones..</param>
/// <param name="SuccessfulJobsHistoryLimit">The number of successful finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified..</param>
/// <param name="Suspend">This flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false..</param>
public V2alpha1CronJobSpec(string ConcurrencyPolicy = default(string), int? FailedJobsHistoryLimit = default(int?), V2alpha1JobTemplateSpec JobTemplate = default(V2alpha1JobTemplateSpec), string Schedule = default(string), long? StartingDeadlineSeconds = default(long?), int? SuccessfulJobsHistoryLimit = default(int?), bool? Suspend = default(bool?))
{
// to ensure "JobTemplate" is required (not null)
if (JobTemplate == null)
{
throw new InvalidDataException("JobTemplate is a required property for V2alpha1CronJobSpec and cannot be null");
}
else
{
this.JobTemplate = JobTemplate;
}
// to ensure "Schedule" is required (not null)
if (Schedule == null)
{
throw new InvalidDataException("Schedule is a required property for V2alpha1CronJobSpec and cannot be null");
}
else
{
this.Schedule = Schedule;
}
this.ConcurrencyPolicy = ConcurrencyPolicy;
this.FailedJobsHistoryLimit = FailedJobsHistoryLimit;
this.StartingDeadlineSeconds = StartingDeadlineSeconds;
this.SuccessfulJobsHistoryLimit = SuccessfulJobsHistoryLimit;
this.Suspend = Suspend;
}
As you can see from the swagger spec, JobTemplate, Schedule are required parameters. However, the params in the constructor are sorted alphabetically.
I've been sorting through the swagger-codegen code base and I think the sortParamsByRequiredFlag only applies to API generated methods.
Is this by design? I'm not sure if I'm missing some config that I should be setting?
Here is the GitHub issue I opened but haven't heard anything on it.
This is not an answer to why Swagger is not generating the correct code, but an answer to your problem on GitHub.
There is absolutely no need for you to use this:
var jobSpec = new V2alpha1CronJobSpec(null, null, new V2alpha1JobTemplateSpec(), "stringValue", null, ...);
You can just use this (called named parameters):
var jobSpec = new V2alpha1CronJobSpec(JobTemplate: new V2alpha1JobTemplateSpec(), Schedule: "stringValue");
For c# client this flag is ignored in the main codebase.
You can create your own extension and override fromModel method to sort the properties.
Have a look at below code on how to do it.
#Override
public CodegenModel fromModel(String name, Model model, Map<String, Model> allDefinitions) {
CodegenModel codegenModel = super.fromModel(name, model, allDefinitions);
if (sortParamsByRequiredFlag)
{
Collections.sort(codegenModel.readWriteVars, new Comparator<CodegenProperty>() {
#Override
public int compare(CodegenProperty one, CodegenProperty another) {
if (one.required == another.required) return 0;
else if (one.required) return -1;
else return 1;
}
});
System.out.println("***Sorting based on required params in model....***");
}
return codegenModel;
}
I am using the Nuget package System.Threading.Tasks for Silverlight 4 (a port from Mono). I keep getting the InvalidOperationException ("The underlying Task is already in one of the three final states: RanToCompletion, Faulted, or Canceled.") for the following:
var tasks = new Task<DeploymentCatalog>[2];
//Catalog the XAP downloaded by the Application Loader, the BiggestBox Index:
var uri0 = new Uri(Application.Current.
Host.InitParams["LoaderInfoXapPath"], UriKind.Relative);
tasks[0] = CompositionUtility.DownloadCatalogAsync(uri0);
//Catalog the XAP with the BiggestBox Index Part:
var uri1 = new Uri(Application.Current
.Host.InitParams["BiggestBoxIndexXapPath"], UriKind.Relative);
tasks[1] = CompositionUtility.DownloadCatalogAsync(uri1);
//tasks did not run by default...
//tasks[0].Start();
//tasks[1].Start();
Task.WaitAll(tasks);
this.AddToAggregateCatalog(tasks[0].Result);
this.AddToAggregateCatalog(tasks[1].Result);
base.Compose();
where DownloadCatalogAsync is:
/// <summary>
/// Downloads the catalog
/// in a <see cref="System.Threading.Task"/>.
/// </summary>
/// <param name="location">The location.</param>
/// <param name="downloadCompleteAction">The download complete action.</param>
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope",
Justification = "Reliable disposal depends on callers.")]
public static Task<DeploymentCatalog> DownloadCatalogAsync(Uri location)
{
return DownloadCatalogAsTask(location, null);
}
/// <summary>
/// Downloads the catalog
/// in a <see cref="System.Threading.Task"/>.
/// </summary>
/// <param name="location">The location.</param>
/// <param name="downloadCompleteAction">The download complete action.</param>
/// <remarks>
/// For details, see the “Converting an Event-Based Pattern” section in
/// “Simplify Asynchronous Programming with Tasks”
/// by Igor Ostrovsky
/// [http://msdn.microsoft.com/en-us/magazine/ff959203.aspx]
/// </remarks>
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope",
Justification = "Reliable disposal depends on callers.")]
public static Task<DeploymentCatalog> DownloadCatalogAsync(Uri location,
Action<object, AsyncCompletedEventArgs> downloadCompleteAction)
{
var completionSource = new TaskCompletionSource<DeploymentCatalog>();
var catalog = new DeploymentCatalog(location);
catalog.DownloadCompleted += (s, args) =>
{
if(args.Error != null) completionSource.SetException(args.Error);
else if(args.Cancelled) completionSource.SetCanceled();
else
{
completionSource.SetResult(s as DeploymentCatalog); //exception thrown here
if(downloadCompleteAction != null)
downloadCompleteAction.Invoke(s, args);
}
};
catalog.DownloadAsync();
return completionSource.Task;
}
I use this same pattern with WebClient and it works fine (but I don't have to Start() the tasks explicitly---however I did not test WebClient with the Mono-ported version of Task Parallel Library (for Silverlight). I guess I should do that...
You're calling Start on the TaskCompletionSource and the TCS Task had already started and, in the case when you get this exception, it already completed too. Generally you want to design methods like DownloadCatalogAsTask to return already started Task instances and callers can expect them to be started and avoid calling Start themselves.
Couple other suggestions:
I would consider renaming the method to DownloadCatalogAsync to align with .NET 4.5 naming guidelines.
I would not pass the Action<> into the async method. The whole point of returning a Task is so that you can chain on continuations using familiar patterns like an explicit ContinueWith or the await keyword in C# 5.0. Here you're inventing you own approach.
I had the same problem. i.e. Mono TPL on Silverlight 4; exception thrown at completionSource.SetResult().
This was resolved when I used completionSource.TrySetResult() instead.
I am trying to write a 'SafeInvoke' method that handles all cases/problems that can occur when trying to access a control from another thread. I've seen a lot of solutions and a lot of questions about this and while there are some that are good enough for most people, they all fail to take race conditions in account (meaning that it's still possible to get an unwanted exception).
So this is what I have so far, I tried commenting as best as I could why I put some ifs and try catches. I also tried to catch only the relevant exceptions, InvalidOperationException is one that can occur for a wide range of reasons (including Collection was modified) and I didn't want to suppress those (because they have nothing to do with the safe invoking). To check that I based myself on the TargetSite.Name property of the exception, I also looked up the actual throw in reflector to see if there were any other locations that could cause an exception.
/// <summary>
/// Safely invokes an action on the thread the control was created on (if accessed from a different thread)
/// </summary>
/// <typeparam name="T">The return type</typeparam>
/// <param name="c">The control that needs to be invoked</param>
/// <param name="a">The delegate to execute</param>
/// <param name="spinwaitUntilHandleIsCreated">Waits (max 5sec) until the the control's handle is created</param>
/// <returns>The result of the given delegate if succeeded, default(T) if failed</returns>
public static T SafeInvoke<T>(this Control c, Func<T> a, bool spinwaitUntilHandleIsCreated = false)
{
if (c.Disposing || c.IsDisposed) // preliminary dispose check, not thread safe!
return default(T);
if (spinwaitUntilHandleIsCreated) // spin wait until c.IsHandleCreated is true
{
if (!c.SpinWaitUntilHandleIsCreated(5000)) // wait 5sec at most, to prevent deadlock
return default(T);
}
if (c.InvokeRequired) // on different thread, need to invoke (can return false if handle is not created)
{
try
{
return (T)c.Invoke(new Func<T>(() =>
{
// check again if the control is not dispoded and handle is created
// this is executed on the thread the control was created on, so the control can't be disposed
// while executing a()
if (!c.Disposing && !c.IsDisposed && c.IsHandleCreated)
return a();
else // the control has been disposed between the time the other thread has invoked this delegate
return default(T);
}));
}
catch (ObjectDisposedException ex)
{
// sadly the this entire method is not thread safe, so it's still possible to get objectdisposed exceptions because the thread
// passed the disposing check, but got disposed afterwards.
return default(T);
}
catch (InvalidOperationException ex)
{
if (ex.TargetSite.Name == "MarshaledInvoke")
{
// exception that the invoke failed because the handle was not created, surpress exception & return default
// this is the MarhsaledInvoke method body part that could cause this exception:
// if (!this.IsHandleCreated)
// {
// throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));
// }
// (disassembled with reflector)
return default(T);
}
else // something else caused the invalid operation (like collection modified, etc.)
throw;
}
}
else
{
// no need to invoke (meaning this is *probably* the same thread, but it's also possible that the handle was not created)
// InvokeRequired has the following code part:
// Control wrapper = this.FindMarshalingControl();
// if (!wrapper.IsHandleCreated)
// {
// return false;
// }
// where findMarshalingControl goes up the parent tree to look for a parent where the parent's handle is created
// if no parent found with IsHandleCreated, the control itself will return, meaning wrapper == this and thus returns false
if (c.IsHandleCreated)
{
try
{
// this will still yield an exception when the IsHandleCreated becomes false after the if check (race condition)
return a();
}
catch (InvalidOperationException ex)
{
if (ex.TargetSite.Name == "get_Handle")
{
// it's possible to get a cross threadexception
// "Cross-thread operation not valid: Control '...' accessed from a thread other than the thread it was created on."
// because:
// - InvokeRequired returned false because IsHandleCreated was false
// - IsHandleCreated became true just after entering the else bloc
// - InvokeRequired is now true (because this can be a different thread than the control was made on)
// - Executing the action will now throw an InvalidOperation
// this is the code part of Handle that will throw the exception
//
//if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
//{
// throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
//}
//
// (disassembled with reflector)
return default(T);
}
else // something else caused the invalid operation (like collection modified, etc.)
throw;
}
}
else // the control's handle is not created, return default
return default(T);
}
}
There is 1 thing I don't know for sure, which is if IsHandleCreated=true, will it ever become false again ?
I added the spinwait for IsHandleCreated because I started Task<>s in the OnLoad event of the control and it was possible that the task was finished before the control was completely finished with loading. If however it takes longer than 5sec to load a control I let the task finish anyway, without updating the GUI (otherwise I'd have a lot of threads spinwaiting for something that probably won't occur anymore)
If you have any suggestions for optimizations or find any bugs or scenario's that might still pose a problem, please let me know :).
Honestly, do you do so may checkings as well when you access a control/from with the UI thread in a basic application? Probably not, you just code and expect the control exists and is not disposed. Why do you that amount of checkings now?
It's not a good idea let multiple threads access the UI, but in case you have no other way I would recommend you to use Control.BeginInvoke. Using Control.BeginInvoke, Control.IsInvokeRequired should be enough.
Actually I've never used Control.IsInvokeRequired, I know before hand which access will come from a different thread and which no.
I recently came across this code:
public static class ClientBaseExtender
{
/// <summary>
/// Tries to execute async service call. If <see cref="TimeoutException"/> occured retries again.
/// </summary>
/// <typeparam name="TChannel">ServiceClient class.</typeparam>
/// <typeparam name="TArgs">Type of service client method return argument.</typeparam>
/// <param name="client">ServiceClient instance.</param>
/// <param name="tryExecute">Delegate that execute starting of service call.</param>
/// <param name="onCompletedSubcribe">Delegate that subcribes an event handler to the OnCompleted event of the service client method.</param>
/// <param name="onCompleted">Delegate that executes when service call is succeeded.</param>
/// <param name="onError">Delegate that executes when service call fails.</param>
/// <param name="maxAttempts">Maximum attempts to execute service call before error if <see cref="TimeoutException"/> occured (by default 5).</param>
public static void ExecuteAsyncRepeatedly<TChannel, TArgs>(this ClientBase<TChannel> client, Action tryExecute,
Action<EventHandler<TArgs>> onCompletedSubcribe, EventHandler<TArgs> onCompleted,
EventHandler<TArgs> onError, int maxAttempts)
where TChannel : class
where TArgs : AsyncCompletedEventArgs
{
int attempts = 0;
var serviceName = client.GetType().Name;
onCompletedSubcribe((s, e) =>
{
if (e.Error == null) // Everything is OK
{
if (onCompleted != null)
onCompleted(s, e);
((ICommunicationObject)client).Close();
Debug.WriteLine("[{1}] Service '{0}' closed.", serviceName, DateTime.Now);
}
else if (e.Error is TimeoutException)
{
attempts++;
if (attempts >= maxAttempts) // Final timeout after n attempts
{
Debug.WriteLine("[{2}], Final Timeout occured in '{0}' service after {1} attempts.", serviceName, attempts, DateTime.Now);
if (onError != null)
onError(s, e);
client.Abort();
Debug.WriteLine("[{1}] Service '{0}' aborted.", serviceName, DateTime.Now);
return;
}
// Local timeout
Debug.WriteLine("[{2}] Timeout occured in '{0}' service (attempt #{1}).", serviceName, attempts, DateTime.Now);
Debug.WriteLine("[{2}] Attempt #{0} to execute call to '{1}' service.", attempts + 1, serviceName, DateTime.Now);
tryExecute(); // Try again.
}
else
{
if (onError != null)
onError(s, e);
client.Abort();
Debug.WriteLine("[{1}] Service '{0}' aborted.", serviceName, DateTime.Now);
}
});
Debug.WriteLine("[{2}] Attempt #{0} to execute call to '{1}' service.", attempts + 1, serviceName, DateTime.Now);
tryExecute(); // First attempt to execute
}
}
public void GetData()
{
var client = new MyServiceClient();
client.ExecuteAsyncRepeatedly(() => client.MyOperationAsync(...),
(EventHandler<MyOperationCompletedEventArgs> handler) =>client.MyOperationCompleted += handler,
(s, e) => // OnCompleted
{
Do(e.Result);
},
(s, e) => // OnError
{
HandleError(e.Error);
}
);
}
The problem is, I have a button that fires this code off. When the button is pushed more than once the handler gets added again and again. This is a problem because the code will fire as many times as the user has pushed the button. How can I remove the handler created with the lambda expression in this code so it will only run once?
Thanks!
EDIT:
I'm calling the code like this from my button click command:
_dataService.GetData(GetDataCompleted);
private void GetDataComplete(Data data)
{
//do something with data }
I think that you can solve it by implementing a push-pull strategy in your code-behind. I propose something similar to this:
bool _requestPending;
readonly object _lock = new object();
void OnClick (...)
{
lock(_lock)
{
if (_requestPending == false)
{
_dataService.GetData(GetDataCompleted);
_requestPending = true;
}
}
}
private void GetDataComplete(Data data)
{
lock(_lock)
{
try
{
//do something with data
}
finally
{
_requestPending = false;
}
}
}
Even better, disable the UI button when you have a pending request. You wouldn't have any concurrency issues with accessing and modifying the _requestPending from different threads, but still you could suffer a race condition if the service response is fast enough, so still better to synchronize the two blocks of code.
Anyway, personally I don't like this implementation for what you're trying to achieve. The code is quite confusing, and makes it hard to foresee problems that may arise. Make sure that:
you provide a way to abort a request
and reenable the button again
the code that updates the screen is
executed by the UI thread
Today I got here nice answer that will resolve my problem. Unfortunately, I forgot to ask how about locking.
The issue was simple - in the server, each connected client will get an unique ID (reusable) from 1 to 500, which is maximum of clients.
The answer was to create a qeue and use waiting elements for new connections and just return them back when they are released.
I’m not sure whether I understand correctly - also I should init the qeue with 500 elements (ints) and just take them one by one and return back once released?
If so, what about locking here? My question was mainly aimed on performance because I was using lock.
If you can use Parallel Extensions go for a ConcurrentQueue
If you can't you can find how to implement your own in this article by Herb Sutter
By the way, the Jon Skeet answer is telling you to just try to unqueue and if empty generate and use. When you're done with an id, simply return it by enqueuing it. There's no reference to 500 ids. IF you need to limit to 500 ids then you will need to initialize the queue with your 500 ids and then, instead of generating new ones, BLOCK when the queue is empty. For that a consumer/producer pattern will be better suited.
i do not understand what you wanna do.
You can populate a hash table for used numbers.
And you can store it in an application wide variable and use Application.Lock and Application.Unlock methods while accessing to that resource.
Something like this?
/// <summary>
/// Thread safe queue of client ids
/// </summary>
internal class SlotQueue
{
private readonly AutoResetEvent _event = new AutoResetEvent(false);
private readonly Queue<int> _items = new Queue<int>();
private int _waitCount;
/// <summary>
/// Initializes a new instance of the <see cref="SlotQueue"/> class.
/// </summary>
/// <param name="itemCount">The item count.</param>
public SlotQueue(int itemCount)
{
// Create queue items
for (int i = 0; i < itemCount; ++i)
_items.Enqueue(i);
}
/// <summary>
/// Gets number of clients waiting in the queue.
/// </summary>
public int QueueSize
{
get { return _waitCount; }
}
/// <summary>
///
/// </summary>
/// <param name="waitTime">Number of milliseconds to wait for an id</param>
/// <returns></returns>
public int Deqeue(int waitTime)
{
// Thread safe check if we got any free items
lock (_items)
{
if (_items.Count > 0)
return _items.Dequeue();
}
// Number of waiting clients.
Interlocked.Increment(ref _waitCount);
// wait for an item to get enqueued
// false = timeout
bool res = _event.WaitOne(waitTime);
if (!res)
{
Interlocked.Decrement(ref _waitCount);
return -1;
}
// try to fetch queued item
lock (_items)
{
if (_items.Count > 0)
{
Interlocked.Decrement(ref _waitCount);
return _items.Dequeue();
}
}
// another thread got the last item between waitOne and the lock.
Interlocked.Decrement(ref _waitCount);
return -1;
}
/// <summary>
/// Enqueue a client ID
/// </summary>
/// <param name="id">Id to enqueue</param>
public void Enqueue(int id)
{
lock (_items)
{
_items.Enqueue(id);
if (_waitCount > 0)
_event.Set();
}
}
}