Running Multiple threads in queue using BlockingCollections - task-parallel-library

My program has 3 functions. Each function takes a list of Items and fill certain information.
For example
class Item {
String sku,upc,competitorName;
double price;
}
function F1 takes a List and fills upc
function F2 takes List (output of F1) and fills price.
function F3 takes List (output of F2) and fills competitorName
F1 can process 5 items at a time,
F2 can process 20 items at a time,
F3 also 20.
Right now I am running F1 -> F2 -> F3 in serial because F2 needs info(UPC code) from F1. F3 needs price from F2.
I would like to make this process efficient by running F1 run continuously instead of waiting for F2 and F3 to be completed. F1 executes and output into queue then F2 takes 20 items at a time and process them. and then follows F3.
How can i achieve this by using BlockingCollection and Queue?

This is a typical use case of Apache Storm in case you've continuous items coming in to F1. You can implement this in Storm in matter of minutes and you'll have fast and perfectly parallel system in place. Your F1, F2 and F3 will become bolts and your Items producer will become spout.
Since you asked how to do it using BlockingCollections here is an implementation. You'll need 3 threads in total.
ItemsProducer: It is producing 5 items at a time and feeding it to F1.
F2ExecutorThread: It is consuming 20 items at a time and feeding it to F2.
F3ExecutorThread: It is consuming 20 items at a time and feeding it to F3.
You also have 2 blocking queues one is used to transfer data from F1->F2 and one from F2->F3. You can also have a queue to feed data to F1 in similar fashion if required. It depends upon how you are getting the items. I've used Thread.sleep to simulate the time required to execute the function.
Each function will keep looking for items in their assigned queue, irrespective of what other functions are doing and wait until the queue has items. Once they've processed the item they'll put it in another queue for another function. They'll wait until the other queue has space if it is full.
Since all your functions are running in different threads, F1 won't be waiting for F2 or F3 to finish. If your F2 and F3 are significantly faster then F1 you can assign more threads to F1 and keep pushing to same f2Queue.
public class App {
final BlockingQueue<Item> f2Queue = new ArrayBlockingQueue<>(100);
final BlockingQueue<Item> f3Queue = new ArrayBlockingQueue<>(100);
public static void main(String[] args) throws InterruptedException {
App app = new App();
app.start();
}
public void start() throws InterruptedException {
Thread t1 = new ItemsProducer(f2Queue);
Thread t2 = new F2ExecutorThread(f2Queue, f3Queue);
Thread t3 = new F3ExecutorThread(f3Queue);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
/**
* Thread producing 5 items at a time and feeding it to f1()
*/
class ItemsProducer extends Thread {
private BlockingQueue<Item> f2Queue;
private static final int F1_BATCH_SIZE = 5;
public ItemsProducer(BlockingQueue<Item> f2Queue) {
this.f2Queue = f2Queue;
}
public void run() {
Random random = new Random();
while (true) {
try {
List<Item> items = new ArrayList<>();
for (int i = 0; i < F1_BATCH_SIZE; i++) {
Item item = new Item(String.valueOf(random.nextInt(100)));
Thread.sleep(20);
items.add(item);
System.out.println("Item produced: " + item);
}
// Feed items to f1
f1(items);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void f1(List<Item> items) throws InterruptedException {
Random random = new Random();
for (Item item : items) {
Thread.sleep(100);
item.upc = String.valueOf(random.nextInt(100));
f2Queue.put(item);
}
}
}
/**
* Thread consuming items produced by f1(). It takes 20 items at a time, but if they are not
* available it waits and starts processesing as soon as one gets available
*/
class F2ExecutorThread extends Thread {
static final int F2_BATCH_SIZE = 20;
private BlockingQueue<Item> f2Queue;
private BlockingQueue<Item> f3Queue;
public F2ExecutorThread(BlockingQueue<Item> f2Queue, BlockingQueue<Item> f3Queue) {
this.f2Queue = f2Queue;
this.f3Queue = f3Queue;
}
public void run() {
try {
List<Item> items = new ArrayList<>();
while (true) {
items.clear();
if (f2Queue.drainTo(items, F2_BATCH_SIZE) == 0) {
items.add(f2Queue.take());
}
f2(items);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void f2(List<Item> items) throws InterruptedException {
Random random = new Random();
for (Item item : items) {
Thread.sleep(100);
item.price = random.nextInt(100);
f3Queue.put(item);
}
}
}
/**
* Thread consuming items produced by f2(). It takes 20 items at a time, but if they are not
* available it waits and starts processesing as soon as one gets available.
*/
class F3ExecutorThread extends Thread {
static final int F3_BATCH_SIZE = 20;
private BlockingQueue<Item> f3Queue;
public F3ExecutorThread(BlockingQueue<Item> f3Queue) {
this.f3Queue = f3Queue;
}
public void run() {
try {
List<Item> items = new ArrayList<>();
while (true) {
items.clear();
if (f3Queue.drainTo(items, F3_BATCH_SIZE) == 0) {
items.add(f3Queue.take());
}
f3(items);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void f3(List<Item> items) throws InterruptedException {
Random random = new Random();
for (Item item : items) {
Thread.sleep(100);
item.competitorName = String.valueOf(random.nextInt(100));
System.out.println("Item done: " + item);
}
}
}
class Item {
String sku, upc, competitorName;
double price;
public Item(String sku) {
this.sku = sku;
}
public String toString() {
return "sku: " + sku + " upc: " + upc + " price: " + price + " compName: " + competitorName;
}
}
I guess you can follow the exact same approach in .Net as well. For better understanding I suggest you to go through basic architecture of http://storm.apache.org/releases/current/Tutorial.html

I tried to do same thing in .NET and i think it is working.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace BlockingCollectionExample
{
class Program
{
static void Main(string[] args)
{
BlockingCollection<Listing> needUPCJobs = new BlockingCollection<Listing>();
BlockingCollection<Listing> needPricingJobs = new BlockingCollection<Listing>();
// This will have final output
List<Listing> output = new List<Listing>();
// start executor 1 which waits for data until available
var executor1 = Task.Factory.StartNew(() =>
{
int maxSimutenousLimit = 5;
int gg = 0;
while (true)
{
while (needUPCJobs.Count >= maxSimutenousLimit)
{
List<Listing> tempListings = new List<Listing>();
for (int i = 0; i < maxSimutenousLimit; i++)
{
Listing listing = new Listing();
if (needUPCJobs.TryTake(out listing))
tempListings.Add(listing);
}
// Simulating some delay for first executor
Thread.Sleep(1000);
foreach (var eachId in tempListings)
{
eachId.UPC = gg.ToString();
gg++;
needPricingJobs.Add(eachId);
}
}
if (needUPCJobs.IsAddingCompleted)
{
if (needUPCJobs.Count == 0)
break;
else
maxSimutenousLimit = needUPCJobs.Count;
}
}
needPricingJobs.CompleteAdding();
});
// start executor 2 which waits for data until available
var executor2 = Task.Factory.StartNew(() =>
{
int maxSimutenousLimit = 10;
int gg = 10;
while (true)
{
while (needPricingJobs.Count >= maxSimutenousLimit)
{
List<Listing> tempListings = new List<Listing>();
for (int i = 0; i < maxSimutenousLimit; i++)
{
Listing listing = new Listing();
if (needPricingJobs.TryTake(out listing))
tempListings.Add(listing);
}
// Simulating more delay for second executor
Thread.Sleep(10000);
foreach (var eachId in tempListings)
{
eachId.Price = gg;
gg++;
output.Add(eachId);
}
}
if (needPricingJobs.IsAddingCompleted)
{
if(needPricingJobs.Count==0)
break;
else
maxSimutenousLimit = needPricingJobs.Count;
}
}
});
// producer thread
var producer = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 100; i++)
{
needUPCJobs.Add(new Listing() { ID = i });
}
needUPCJobs.CompleteAdding();
});
// wait for producer to finish producing
producer.Wait();
// wait for all executors to finish executing
Task.WaitAll(executor1, executor2);
Console.WriteLine();
Console.WriteLine();
}
}
public class Listing
{
public int ID;
public string UPC;
public double Price;
public Listing() { }
}
}

Related

Dotnet yield with IEnumerable<Task<int>> not working

Playing around with yield and Task.
The following simple example runs fine.
class Program
{
private static void Main(string[] args)
{
string[] messages = { "First task", "Second task", "Third task", "Fourth task" };
var taskList = CreateTaskList(messages).ToList();
taskList.ForEach(task => task.Start());
Task.WaitAll(taskList.ToArray());
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}
static IEnumerable<Task> CreateTaskList(string[] messages)
{
foreach (var message in messages)
{
yield return new Task(obj => PrintMessage((string)obj!), message);
}
}
static void PrintMessage(object message)
{
Console.WriteLine("Message: {0}", message);
}
}
But the following does not. Is some deadlock at play? Its stuck at Task.WaitAll. So all that I get from Console is Before wait all
class SimpleClass
{
public int Counter { get; set; }
}
class Program
{
private static void Main(string[] args)
{
// create the simple object
var simpleObject = new SimpleClass();
var taskList = CreateTaskEnumerable(simpleObject, 10);
// Start all of the tasks
foreach (var task in taskList)
task.Start();
Console.WriteLine("Before wait all");
// wait for all of the tasks to complete
Task.WaitAll(taskList.ToArray());
Console.WriteLine("After wait all");
foreach (var task in taskList)
simpleObject.Counter += task.Result;
// write out the counter value
Console.WriteLine("Expected value {0}, Counter value: {1}", 10000, simpleObject.Counter);
// wait for input before exiting
Console.WriteLine("Press enter to finish");
Console.ReadLine();
}
private static IEnumerable<Task<int>> CreateTaskEnumerable(SimpleClass simpleObject, int numberOfTasks)
{
for (int i = 0; i < numberOfTasks; i++)
{
yield return new Task<int>((stateObject) =>
{
// get the state object
var localCounter = (int)stateObject!;
// enter a loop for 1000 increments
for (int j = 0; j < 1000; j++)
// increment the counters
localCounter++;
return localCounter;
}, simpleObject.Counter);
}
}
}
If I remove yield altogether, the above will be as follows, and works. It gives the output as follows. I am expecting the same output from the above as well, but thats stuck. Why?
Before wait all
After wait all
Expected value 10000, Counter value: 10000
Press enter to finish
The program.
class SimpleClass
{
public int Counter { get; set; }
}
class Program
{
private static void Main(string[] args)
{
// create the bank account instance
var simpleObject = new SimpleClass();
// create an list of tasks
var taskList = new List<Task<int>>();
for (int i = 0; i < 10; i++)
{
// create a new task
var task = new Task<int>((stateObject) =>
{
// get the state object
var localCounter = (int)stateObject!;
// enter a loop for 1000 increments
for (int j = 0; j < 1000; j++)
// increment the counters
localCounter++;
return localCounter;
}, simpleObject.Counter);
taskList.Add(task);
}
// Start all of the tasks
foreach (var task in taskList)
task.Start();
Console.WriteLine("Before wait all");
// wait for all of the tasks to complete
Task.WaitAll(taskList.ToArray());
Console.WriteLine("After wait all");
foreach (var task in taskList)
simpleObject.Counter += task.Result;
// write out the counter value
Console.WriteLine("Expected value {0}, Counter value: {1}", 10000, simpleObject.Counter);
// wait for input before exiting
Console.WriteLine("Press enter to finish");
Console.ReadLine();
}
}
In the first example you are calling CreateTaskList(messages).ToList() which forces CreateTaskList to yield all tasks before continuing. In the second example you do not call ToList(), and the tasks are yielded in the foreach and then started. The problem is in line Task.WaitAll(taskList.ToArray());. It takes the IEnumerable and yields the tasks again, and you are waiting for them to finish, but they are not started. In other words, every time you call foreach or ToList() on your 'yielded' IEnumerable, it will run the method CreateTaskEnumerable and create new tasks.
One solution is to call var taskList = CreateTaskEnumerable(simpleObject, 10).ToList() or you could just manualy create list in CreateTaskEnumerable and return it.
P.S. I would suggest you read how yield return works, or test it in https://sharplab.io/. It basically creates IEnumerable that gets its data from your method. This means your method will be executed every time your IEnumerable is enumerated.

How to create a restart method using JFrame?

recently I started learning Java, I watched a YT video where a programmer used static methods and variables to create a simple guess game using JFrame.
Afterwards I tried to implement a close/restart button, after reading some Threads I relized static methods arenĀ“t made that for. So my question is now how do I solve my problem now. :)
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.ThreadLocalRandom;
public class main extends JFrame {
JLabel text = new JLabel("Please choose a number between 1 & 10 ");
JLabel textVersuch = new JLabel();
JButton button = new JButton("Try");
int myNumber = ThreadLocalRandom.current().nextInt(1,10+1);
JTextField textField = new JTextField();
int count = 0;
//is there a better way to hide all this information, but still keep them useable for my methods?
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.openUI(); //error occurs
}
//How do I manage to start my method openUI() to start my game?
public void openUI(){
JFrame frame = new JFrame("Program");
frame.setSize(400,400);
frame.setLocation(800,400);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setDefaultLookAndFeelDecorated(true);
text.setBounds(0,50,400,25);
textVersuch.setBounds(300,0,100,25);
textField.setBounds(0,150,50,25);
button.setBounds(50,150,100,25);
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
try {
String textFromTextfield = textField.getText();
int number = Integer.parseInt(textFromTextfield);
if(number<1 || number>10){
text.setText("Your number has to be between 1 & 10 ");
textField.setText("");
}else{
guess(number);
}
}catch (Exception error){
text.setText("Please enter a digit! ");
textField.setText("");
}
}});
frame.add(button);
frame.add(textField);
frame.add(text);
frame.add(textVersuch);
frame.setLayout(null);
frame.setVisible(true);
}
public void close(JFrame frame){
frame.dispose(); //here I want to close the game
}
public void guess(int number ) throws InterruptedException {
count++;
textVersuch.setText(count + " tries!");
if(number == myNumber){
text.setText("You was right! " + " You tried " + count + " time(s) :)" );
button.setText("Restart");
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
//How can I restart my JFrame?
}
});
} else if (count < 3) {
text.setText("Wrong guess! Retry");
if (number < myNumber){
text.setText("Your searched number is bigger than" + number );
}else {
text.setText("Your searched number is lower than" + number );
}
} else {
text.setText("Sorry, you lost the number was " + myNumber);
}
textField.setText("");
}
}

Weighted Fair queuing algorithm doing round-robin with just 2 packet flow

I tried implementing a simple simulation of the Weighted Fair queuing algorithm.
But I noticed an issue when using only 2 packet flow, the algorithm is not letting twice the amount of T1 packet as it should because they are half the size of T2 packet. Instead it simply do round-robin between the 2 flow.
I understand the problem is that it take some time for T1 to add another packet to the queue and the Weighted Fair queuing have the time to run when T1 queue is empty.
This does not seem to be the expected behavior, am I missing something in the algorithm that would fix this corner case?
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
public class Packet
{
public string name;
public ManualResetEventSlim mres;
public long virFinish;
public long packetSize;
public Packet(string pname, long p_virFinish, long p_packetSize)
{
name = pname;
mres = new ManualResetEventSlim(false);
virFinish = p_virFinish;
packetSize = p_packetSize;
}
}
public class Queue
{
public ConcurrentQueue<Packet> q = new ConcurrentQueue<Packet>();
public long virStart;
public long lastVirFinish;
}
public class FairQueue
{
long VirtualTime; // system virtual time
ConcurrentDictionary<string, Queue> queues = new ConcurrentDictionary<string, Queue>();
public FairQueue() {
VirtualTime = long.MinValue;
queues.GetOrAdd("t1", new Queue());
queues.GetOrAdd("t2", new Queue());
queues.GetOrAdd("t3", new Queue());
}
public ManualResetEventSlim Write(string queueName, long packetSize)
{
var queue = queues[queueName];
Volatile.Write(ref queue.virStart, Math.Max(VirtualTime, queue.lastVirFinish));
var packetFinish = queue.virStart + packetSize;
var packet = new Packet(queueName, packetFinish, packetSize);
queue.q.Enqueue(packet);
Volatile.Write(ref queue.lastVirFinish, packetFinish);
UpdateVirtualClock(queue);
return packet.mres;
}
private void UpdateVirtualClock(Queue queue)
{
long minStart = queue.virStart;
foreach (var item in queues)
{
if (!item.Value.q.IsEmpty)
{
minStart = Math.Min(item.Value.virStart, minStart);
}
}
Volatile.Write(ref VirtualTime, Math.Max(VirtualTime, minStart));
}
// return queue with smallest lastVirFinish
public Queue SelectQueue()
{
long minVirFinish = long.MaxValue; // infinity
Queue selected = null;
foreach (var queue in queues)
{
if (!queue.Value.q.IsEmpty)
{
var lastVirFinish = Volatile.Read(ref queue.Value.lastVirFinish);
if (lastVirFinish < minVirFinish) {
minVirFinish = queue.Value.lastVirFinish;
selected = queue.Value;
}
}
}
return selected;
}
public Packet Send()
{
var selectedQueue = SelectQueue();
if (selectedQueue != null)
{
Packet p;
selectedQueue.q.TryDequeue(out p);
/* Set the start and the finish times of the remaining packets in the queue */
if (!selectedQueue.q.IsEmpty)
{
var next = selectedQueue.q.Take(1).First();
selectedQueue.virStart = selectedQueue.lastVirFinish;
selectedQueue.lastVirFinish = selectedQueue.virStart + next.packetSize;
}
return p;
}
else
{
return null;
}
}
}
class Program
{
static void Main(string[] args)
{
FairQueue fq = new FairQueue();
// Task that enqueue packet
var t1 = Task.Factory.StartNew(
() => {
while (true)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
var s1 = fq.Write("t1", 100);
s1.Wait();
sw.Stop();
}
}
);
// Task that enqueue packet
var t2 = Task.Factory.StartNew(
() => {
while (true)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
var s1 = fq.Write("t2", 200);
s1.Wait();
sw.Stop();
}
}
);
// Loop that run the Weighted Fair Queue Algorithm
while (true)
{
var toSend = fq.Send();
if (toSend != null)
{
Console.WriteLine(toSend.name);
Thread.Sleep((int)toSend.packetSize);
toSend.mres.Set();
}
}
}
}
}

BeforeTextChanged event doesn't return the text about to be entered in custom Entry Renderer

I've a custom Entry Renderer in Android and I've implemented BeforeTextChanged event so that I get the text which is gonna be entered, but it doesn't return that text.
I've implemented in two different ways:
First implementing ITextWatcher by the class and then overriding the mentioned event (helping this link):
void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after)
{
var test1 = new string(s?.ToArray());
var test2 = Control?.Text;
SpannableString spannableString = new SpannableString(s);
BackgroundColorSpan backgroundSpan = new BackgroundColorSpan(Color.Blue);
spannableString.SetSpan(backgroundSpan, start, start + count,
SpanTypes.ExclusiveExclusive);
var test3 = spannableString;
}
Second way:
Control.BeforeTextChanged += (sender, args) =>
{
var test1 = new string(args?.Text.ToArray());
var entry = sender as EditText;
var test2 = entry?.Text;
var test3 = Control?.Text;
};
But none of them will return the text that is about to be entered.
What I want is to access that text and only in some circumstances allowing it to be inserted.
I don't wanna use Behavior as it doesn't suit my need.
First way, ITextWatcher:
class MyEntryRenderer : EntryRenderer
{
public MyEntryRenderer() : base()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.SetBackgroundColor(global::Android.Graphics.Color.LightGreen);
Control.AddTextChangedListener(new MyTextWatcher());
}
}
}
class MyTextWatcher :Java.Lang.Object, ITextWatcher
{
public void AfterTextChanged(IEditable s)
{
Android.Util.Log.Error("lv", "afterTextChanged ---->" + s);
}
public void BeforeTextChanged(ICharSequence s, int start, int count, int after)
{
Android.Util.Log.Error("lv", "beforeTextChanged ----> s=" + s + "----start=" + start
+ "----after=" + after + "----count" + count);
}
public void OnTextChanged(ICharSequence s, int start, int before, int count)
{
Android.Util.Log.Error("lv", "onTextChanged ---->s=" + s + "----start=" + start
+ "----before=" + before + "----count" + count);
}
}
it works well.
Second way, Control.BeforeTextChanged, it also works well.
it doesn't return that text
The methods you provided in your question, they are all work well in my phone. Please show more codes, so I can help you find where is wrong.

Algorithm to fire sequential keystrokes

I want to write an algorithm to sequentially press keys F1-F3. My form has these controls:
lblF1
textboxF1
lblF2
textboxF2
lblF3
textboxF3
btnStart
In textboxF1-textboxF3 the time in seconds is entered. This when the program is to press the hotkey. It is important that the program can't press two keys at once, for example F1 and F2. It may not press more than one key in a second. When I click on btnStart it calls Run().
This is how I tried to resolve this:
static int counterF1 = 9999;
static int counterF2 = 9999;
static int counterF3 = 9999;
public void Run()
{
counterF1 = 9999;
counterF2 = 9999;
counterF3 = 9999;
while (true)
{
Loop();
}
}
public void Loop()
{
bool used = false;
if (counterF1 >= (int)textboxF1.text)
{
counterF1 = PressKey(VK_F1);
used = true;
}
if (counterF2 >= (int)textboxF2.text)
{
counterF2 = PressKey(VK_F2);
used = true;
}
if (counterF3 >= (int)textboxF3.text)
{
counterF3 = PressKey(VK_F3);
used = true;
}
if (used == false)
{
IncrementCounters();
Delay(1000);
}
}
public double PressKey(uint key)
{
myPostMessageA(hWindow, WM_KEYDOWN, (uint)key, (uint)key);
IncrementCounters();
return 1; //return 1 because one second
}
public void IncrementCounters()
{
counterF1++;
counterF2++;
counterF3++;
}
But often it doesn't press any key (it is possible it is too late, but can't be an omission). Can you explain how to make an algorithm for this?
We will use a class KeyStroke that stores the necessary data for a special key:
public class KeyStroke
{
public int period { get; set; } // Period in which to hit key
public int next { get; set; } // ticks to the next hit of this key
public int VK { get; set; } //KeyCode
}
public List<KeyStroke> keys = new List<KeyStroke>();
An Initialize() method is needed to read the data from the text boxes and to init the simulation. We utilize a timer with the interval of one second to run the simulation. In my example, I don't read from textboxes, but use constant values. Add the input and error handling. If you use WPF, you can bind the KeyStroke objects to the textboxes.
void Init()
{
//Initialize keys with according periods from input
keys.Clear();
keys.Add(new KeyStroke() { VK = VK_F1, period = 1, next = 1 });
keys.Add(new KeyStroke() { VK = VK_F2, period = 10, next = 10 });
keys.Add(new KeyStroke() { VK = VK_F3, period = 5, next = 5 });
//sort keys by period (descending), in order to handle long period keys, too
keys.Sort((first, second) => second.period.CompareTo(first.period));
//Start the program
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(1);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
The tick event is similar to yours:
void t_Tick(object sender, EventArgs e)
{
bool used = false;
foreach (var key in keys)
{
if (key.next <= 0 && !used)
{
PressKey(key.VK);
key.next = key.period;
used = true;
}
key.next--;
}
}

Resources