Sign in to follow this  
Followers 0

Threads or some form of parallelism

35 posts in this topic

Posted

Perhaps im spoiled by higher level languages, but i love using the AutoIt... until i needed to do two things at once. :-(. Threads would be awesome, also a tutorial on how others could make their own plugins or something of the sort, or perhaps i missed it? Perl is huge because people could write alot of their own functions if perl didnt already have a support for it. Personally i like AutoIt because its so easy to handle other applications, possibly i am wanting to stretch original intention a bit too far. :-P... but hey... look what happened to C. rofl. :-P

Share this post


Link to post
Share on other sites



Posted

i would love autoit to be multithreaded.

but thats probly not gonna hapen any time soon.

(maby in autoit v4/5 ?)

till then you can use AdLibEnable.

Share this post


Link to post
Share on other sites

Posted

Hmm... :) I would like multi-threading too. Now how to do it, so each thread behaves indepentently? That will be discussed in the developers forum.

What kind of interface do you have in mind?

Share this post


Link to post
Share on other sites

Posted

I don't think threading is such a good idea. Nothing (internally) is prepared for a multi-threaded environment. To try to add in threading support now (in any form) would require significant rewriting of the core AutoIt code. Personally, I do not want to go through an extended period of time where AutoIt is completely broken while regressions from implementing threading support are fixed; I get annoyed enough as is when minor regressions cause my scripts to stop working. The current core, although only single threaded, has not be drastically changed in one big shot like it would take to implement multi-threading. So basically, its not broke so lets not break it adding a feature that was never meant to be implemented.

That is my technical standpoint. My opionated standpoint based on my experience with C++ is that multi-threading is rarely needed. I can just see some of the idiotic code that will be produced by the forum-morons trying to use multi-threading where it is not needed. I continue to go back to the Call() function when I want to point out that a lot of users really don't have enough sense to be using AutoIt. Its constantly misused as the primary method for invoking user functions. If people can't manage to figure out how to call a function, do they really need access to threading support?

I think that of these two points, the first should hold the most merit. Like adding object-oriented support, multi-threading is one of those things that needs decided on before writing the first line of code because supporting features like that require conscious development decisions that permeate throughout the entire application. You don't simply come along later and just add it in without either breaking everything already written or rewriting everything (Which will probably lead to breakage, too).

Share this post


Link to post
Share on other sites

Posted

That was more or less my conclusion as well, after thinking about it for a while. A lot of internal structures that are together right now would need to be seperated (thinking about AutoItScript, probably the largest class in the whole program) so that each thread can have its own variables and process tree. This is not going to be simple. Sorry guys. This will have to wait a while, maybe version 4.

Share this post


Link to post
Share on other sites

Posted

I used to do a thing with BAT files, where I would have a master BAT file that would START several other BAT files, each of which would log on to a server and get some data and log it to a file. The master BAT file would loop waiting for all the sub-BATs to log their data, and then concatenate it to a master log file. I guess you could call this a primative form of multi-threading (since I used START instead of START /WAIT).

I guess AutoIt could do something similiar, just distribute several EXEs and run them using RUN (instead of RUNWAIT) and have some sort of system for them to communicate with the master script.

Share this post


Link to post
Share on other sites

Posted (edited)

I used to do a thing with BAT files, where I would have a master BAT file that would START several other BAT files, each of which would log on to a server and get some data and log it to a file.  The master BAT file would loop waiting for all the sub-BATs to log their data, and then concatenate it to a master log file.  I guess you could call this a primative form of multi-threading (since I used START instead of START /WAIT).

I guess AutoIt could do something similiar, just distribute several EXEs and run them using RUN (instead of RUNWAIT) and have some sort of system for them to communicate with the master script.

<{POST_SNAPBACK}>

You have command line parimeters and restart the script the in a different mode like this little script i made today.

This is good becuase you don't have to have multi exes which makes the distribution size smaller.

http://www.autoitscript.com/forum/index.php?showtopic=13190

Edited by SolidSnake

Share this post


Link to post
Share on other sites

Posted

At least it's a possibilty in v 4.

Share this post


Link to post
Share on other sites

Posted (edited)

what if keywords such as "ThreadStart" and "ThreadEnd" were introduced where the Autoit.exe was opened in an additional instance during that "Thread"? the code between these keywords being treated similar to a separate script.(but actually a child process of the main script, so only the Original Exe shows up in Task Manager))

Then its just a matter of communication between scripts.

Yes this is possible now, by compiling and FileInstalling each "thread" then running them, but this method would simplify that process, keep the compiled executable small, and put all the source code in one file(for readability).--and wouldn't break existing scripts(providing it's possible to properly implement this)

probably the sanest tradeoff to get the end result without restructuring AutoIt's source Code.

Edited by quaizywabbit

Share this post


Link to post
Share on other sites

Posted

You have command line parimeters and restart the script the in a different mode like this little script i made today.

This is good becuase you don't have to have multi exes which makes the distribution size smaller.

http://www.autoitscript.com/forum/index.php?showtopic=13190

<{POST_SNAPBACK}>

I like that idea. I haven't tried your program, but I think I know what you mean. If I have a need to do multiple tasks, I think I'll use that approach, unless something better comes up (as far as built-in features for AutoIt)

Share this post


Link to post
Share on other sites

Posted (edited)

maby a command like Thread($function)

here is some ugly ascii to visualise my idee.

|
   |
main flow
   |
   |
  \|/
Thread("ex") ----> function ex()
   |				   |
   |				   |
some commands	more commands
   |				   |
   |				   |
  \|/				 \|/
  END				 END
Edited by w0uter

Share this post


Link to post
Share on other sites

Posted

w0uter..That would be a great idea..the best I've seen till now..hmm..and I've thought this multi-threading concept since Excalibur first brought it up..This is actually do-able..Will the Dev's actually do it?Great "work", man :)..GREAT work..

Share this post


Link to post
Share on other sites

Posted

w0uter..That would be a great idea..the best I've seen till now..hmm..and I've thought this multi-threading concept since Excalibur first brought it up..This is actually do-able..Will the Dev's actually do it?Great "work", man :)..GREAT work..

<{POST_SNAPBACK}>

If its so doable, when can I expect to see your implementation of it?

Share this post


Link to post
Share on other sites

Posted

...With that setup w0uter, you might as well just compile a separate exe file and use the nifty ole' Run()

Share this post


Link to post
Share on other sites

Posted

Hmm..my implementation?Simply FileInstall("Aut2exe.exe") then make a temp exe from the func..run it without a tray icon..but that's lame and slow..BTW..when can I see anything better than useless comments..like YOUR hardcoded implementation..

Share this post


Link to post
Share on other sites

Posted

Hmm..my implementation?Simply FileInstall("Aut2exe.exe") then make a temp exe from the func..run it without a tray icon..but that's lame and slow..BTW..when can I see anything better than useless comments..like YOUR hardcoded implementation..

<{POST_SNAPBACK}>

Did you actually read my comments and the comments of David (Nutster)? Both of us are quite familiar with the internal implementation of AutoIt. Both of us separately came to the conclusion that AutoIt would virtually need re-written to properly implement multi-threading. It is not "do-able" without writing a hack-job attempt or modifying every critical piece of AutoIt's implementation that has proven stable enough and doesn't need touched. It irks me just a bit that David and I both stated this and then people come along and ignore what we say and continue on their merry way saying "do it do it" when they have no real clue what it takes to do it and those who do have a clue have said why they won't do it.

Share this post


Link to post
Share on other sites

Posted

I've converted a single threaded C++ application into a multi-threaded one before. Trust me, it's about the most brutal coding job you can possibly imagine, frought with the most obscure, impossible to reproduce bugs you can dream of. Keeping it fully thread-safe over the maintenance lifetime is another big problem.

A lot of us would love to see a fully threaded AutoIt, but it's not gonna happen. Even professional programmers can have a hard time writing/debugging multi-threaded code. I can't imagine turning such functionality over to the average user of AutoIt, especially without a debugger.

Hypothetically, yes it could be done. You *might* even be able to put in a very high level mutex in the interpreter and take a huge performance hit, but odds are even then, you would just be better off writing multiple scripts and executing them simultaneously.

Finally, I would suggest you check out AdLibEnable() I've found that most things I wanted to handle in a multi-threaded fashion, I can tinker something together using this function.

Share this post


Link to post
Share on other sites

Posted

Ok..all of us have agreed this is not easy..But just saying this over and over again will lead nowhere..The AutoIt core will (hopefully?) get complicated enough as to hinder such a big step in the future..Would you rather trace through 5000 lines of code to find&change whatever needs changing or would you enjoy 50000 better?I am exaggerating of course but someone has to start doing it..and the approach that w0uter suggested seems do-able because, in a 'hackish' way, it CAN be done(Just putting the Func in another script and executing it..This could be done automatically, by treating the said function as a separate script, and executing it alongside the main script)..I was suggesting you implement the Thread($func) so that you have a starting point for when you'll have to properly write the threading code..

Share this post


Link to post
Share on other sites

Posted

Im not privy to anything , but after actually reading this thread Im gonna say that autoIT will not have a "thread()" function in the forseeable future... It seems like a few of the devs have made this abundantly clear.

Share this post


Link to post
Share on other sites

Posted

VicTT, rewriting AutoIt now does the following:

  • Stops all current development except for this feature because this feature will impact everything.
  • Throws away 2+ years of tested and stable code in favor of exponentially more complex and untested code.
  • Redefines how the language works on every level.
And just for reference, the last time I checked many months ago, I think that AutoIt was over 30,000 lines of code. I don't have a clue what it is now but I'd say its probably 5,000+ longer than it was then.

The best way to implement this is to start with 0 lines of code and go for there. The current architecture does not lend itself to multi-threading. It would be easier to start with a blank file and go from there than it would start from the existing code base. Trying to fit multi-threading into the current architecture or modify the architecture to support multi-threading leads to several significant and insurmountable flaws. This includes limiting the API so that it can work with as much current code as possible. This will cause the threading supporting not to live up to its potential. It will also force modifications to the existing code, anyway, breaking their stability. A proper design from scratch and then re-writing everything, maybe using the existing code as reference-only, is the only way to implement this.

Also, 95% of the time, multi-threading is not necessary. In general, multi-threading is a sign of poor or lazy design as opposed to clever programming. Multi-threading is never necessary; however, it can be convenient at times with regards to code organization. All multi-threaded applications have a single-threaded equivalent. That single-threaded equivalent will be terribly complex and hard to maintain, but it can be done.

So in summation, AutoIt 3 will probably not see multi-threading in its lifetime (or current iteration) because:

  • Most programs do not need multi-threading, they just need better designs.
  • Multi-threading can not be implemented properly in the current version of AutoIt without throwing away years worth of work.
I strongly dissuade any wide-eyed developer (current or future) who thinks they can hack this in. There can be no performance penalty. This is a high-level language and already incurs enough penalties from that aspect alone. The performance hit for executing code should be roughly equivalent to that of which it is in C++. That is, if multi-threaded C++ applications are 1% slower to execute than single-threaded, a multi-threaded AutoIt should be no more than 1% slower than the single-threaded form.

Share this post


Link to post
Share on other sites

Posted

Just to wade in again and try to clarify a few ideas that Valik touched on.

Making a program multi-threaded or single-threaded is an "initial design-time" decision, not a "add it on at the end" prospect. The initial design of the program must be made with multi-threading in mind in order to optimize the implementation, or in the case of AutoIt's internals as they stand, allow it in anything resembling a reasonable resource load. Several decisions that Jon, and later the rest of the development team, made during the initial creation of AutoIt 3 (That is around 3.0.0) would have to have been made differently. At this stage of the game, it would be a nightmare to re-write and test such a major departure from the current abilities of the system.

There are only a few cases where multi-threading makes sense. The most common one involves several processors to share the program over and a process that can be defined using (hopefully) one function with different arguments that complete the processing on its own. One such task like this is to run database engines (where several users send requests to one program which then assigns the individual requests to seperate threads), or the computation of a non-interdependent mathematics model. These are really not the kind of tasks that AutoIt was designed for, even though it is becoming much more than an automation engine.

I would like to see some multi-threading (real or simulated) capability in AutoIt, but at this late stage of the game, it is just too much work to change it over. When it comes time for us to design V4, I will be pushing for this idea along several others.

Well that's my three cents worth (2 American).

Share this post


Link to post
Share on other sites

Posted

I pretty much agree with everything David has said and will add this to boot. For most (all?) of us, this is our first attempt to design and shape a language. Also, though this is Jon's (at least) 2nd attempt, he never imagined the scope or popularity AutoIt would achieve (None of us did). Therefore, design decisions were made as they were for a number of factors including simple mis-judgments on our part being new to the whole language design thing. Given an opportunity to do all over again, a second implementation would be significantly superior, as is the case with all projects. Anybody reading this right now doing any development, even with AutoIt should know that the first implementation of something is always the worst. But the key thing is, we didn't screw up so bad that the result is a fundamentally flawed language. We don't feel that AutoIt 3 has any significant problems that would warrant a rewrite nor do we feel that its reached its pinnacle and its time to move on and replace it. Development continues on as healthy as ever right now. Even Jon has gotten back into things with outstanding new ideas that none of the rest of us have thought up (Although he screwed up there, once he opened his big mouth I have 9328958342 ideas feeding off his one simple one, :) ). AutoIt has grown in scope beyond a simple automation language to a full-fledged scripting language and has exceeded both the original goals and the original scope. Quite honestly, we're all probably lucky its as good as it is given just how much we under-anticipated all of that.

Share this post


Link to post
Share on other sites

Posted

Alright..If it's a possible prospect for the future(V4), I can't wait to see it..BTW, Valik..Speed is not that much of a problem nowadays..there is no sense in coding apps that need to be run at compiled language speed..That's not the scope of AutoIt either..But I agree with what you've said..in the end, it's more feasible to wait than "hack a thread()" :)..Thanks for caring..

Share this post


Link to post
Share on other sites

Posted

Alright..If it's a possible prospect for the future(V4), I can't wait to see it.

<{POST_SNAPBACK}>

Can't wait to see it? You will have to. We are not ready to start writing it yet. Sorry.

Share this post


Link to post
Share on other sites

Posted (edited)

@Wooter : so with the implementation being single threaded... as overly stated by all so when a script is active and then waits in while loop the thread must be suspended running the loop then the thing is to reopen the thread alloc some memory write stuff then callback to the origanal process..

Provided below is SDK documentation of singlethreaded apartments

and a Overveiw of processes, threads and apartments

Single-Threaded Apartments

Using single-threaded apartments (the apartment model process) offers a message-based paradigm for dealing with multiple objects running concurrently. It enables you to write more efficient code by allowing a thread, while it waits for some time-consuming operation to complete, to allow another thread to be executed.

Each thread in a process that is initialized as an apartment model process, and that retrieves and dispatches window messages, is a single-threaded apartment thread. Each thread lives within its own apartment. Within an apartment, interface pointers can be passed without marshaling, and therefore, all objects in one single-threaded apartment thread communicate directly.

A logical grouping of related objects that all execute on the same thread, and therefore must have synchronous execution, could live on the same single-threaded apartment thread. However, an apartment model object cannot reside on more than one thread. Calls to objects in other processes must be made within the context of the owning process, so distributed COM switches threads for you automatically when you call on a proxy.

The interprocess and interthread models are similar. When it is necessary to pass an interface pointer to an object in another apartment (on another thread) within the same process, you use the same marshaling model that objects in different processes use to pass pointers across process boundaries. By getting a pointer to the standard marshaling object, you can marshal interface pointers across thread boundaries (between apartments) in the same way you do between processes. (Interface pointers must be marshaled when passed between apartments.)

Rules for single-threaded apartments are simple, but it is important to follow them carefully:

Every object should live on only one thread (within a single-threaded apartment).

Initialize the COM library for each thread.

Marshal all pointers to objects when passing them between apartments.

Each single-threaded apartment must have a message loop to handle calls from other processes and apartments within the same process. Single-threaded apartments without objects (client only) also need a message loop to dispatch broadcast sendmessages that some applications use.

DLL-based or in-process objects do not call the COM initialization functions; instead, they register their threading model with the ThreadingModel named-value under the InprocServer32 key in the registry. Apartment-aware objects must also write DLL entry points carefully. There are special considerations that apply to threading in-process servers. For more information, see In-Process Server Threading Issues.

While multiple objects can live on a single thread, no apartment model object can live on more than one thread.

Each thread of a client process or out-of-process server must call or call CoInitializeEx and specify COINIT_APARTMENTTHREADED for the dwCoInit parameter. The main apartment is the thread that calls CoInitializeEx first. For information on in-process servers, refer to In-Process Server Threading Issues.

All calls to an object must be made on its thread (within its apartment). It is forbidden to call an object directly from another thread; using objects in this free-threaded manner could cause problems for applications. The implication of this rule is that all pointers to objects must be marshaled when passed between apartments. COM provides the following two functions for this purpose:

CoMarshalInterThreadInterfaceInStream marshals an interface into a stream object that is returned to the caller.

CoGetInterfaceAndReleaseStream unmarshals an interface pointer from a stream object and releases it.

These functions wrap calls to CoMarshalInterface and CoUnmarshalInterface functions, which require the use of the MSHCTX_INPROC flag.

In general, the marshaling is accomplished automatically by COM. For example, when passing an interface pointer as a parameter in a method call on a proxy to an object in another apartment, or when calling CoCreateInstance, COM does the marshaling automatically. However, in some special cases, where the application writer is passing interface pointers between apartments without using the normal COM mechanisms, the writer must handle the marshaling manually.

If one apartment (Apartment 1) in a process has an interface pointer and another apartment (Apartment 2) requires its use, Apartment 1 must call CoMarshalInterThreadInterfaceInStream to marshal the interface. The stream that is created by this function is thread-safe and must be stored in a variable that is accessible by Apartment 2. Apartment 2 must pass this stream to CoGetInterfaceAndReleaseStream to unmarshal the interface and will get back a pointer to a proxy through which it can access the interface. The main apartment must remain alive until the client has completed all COM work (because some in-process objects are loaded in the main apartment, as described in In-Process Server Threading Issues). After one object has been passed between threads in this manner, it is very easy to pass interface pointers as parameters. That way, distributed COM does the marshaling and thread switching for the application.

To handle calls from other processes and apartments within the same process, each single-threaded apartment must have a message loop. This means that the thread's work function must have a GetMessage/DispatchMessage loop. If other synchronization primitives are being used to communicate between threads, the Win32 function MsgWaitForMultipleObjects can be used to wait both for messages and for thread synchronization events. The Platform SDK documentation for this function has an example of this sort of combination loop.

COM creates a hidden window using the Windows class "OleMainThreadWndClass" in each single-threaded apartment. A call to an object is received as a window message to this hidden window. When the object's apartment retrieves and dispatches the message, the hidden window will receive it. The window procedure will then call the corresponding interface method of the object.

When multiple clients call an object, the calls are queued in the message queue and the object will receive a call each time its apartment retrieves and dispatches messages. Because the calls are synchronized by COM and the calls are always delivered by the thread that belongs to the object's apartment, the object's interface implementations need not provide synchronization. Single-threaded apartments can implement IMessageFilter to permit them to cancel calls or receive window messages when necessary.

The object can be reentered if one of its interface method implementations retrieves and dispatches messages or makes an ORPC call to another thread, thereby causing another call to be delivered to the object (by the same apartment). OLE does not prevent reentrancy on the same thread, but it can help provide thread safety. This is identical to the way in which a window procedure can be reentered if it retrieves and dispatches messages while processing a message. However, calling an out-of-process single-threaded apartment server that calls another single-threaded apartment server will allow the first server to be reentered.

Single-Threaded Apartments

Using single-threaded apartments (the apartment model process) offers a message-based paradigm for dealing with multiple objects running concurrently. It enables you to write more efficient code by allowing a thread, while it waits for some time-consuming operation to complete, to allow another thread to be executed.

Each thread in a process that is initialized as an apartment model process, and that retrieves and dispatches window messages, is a single-threaded apartment thread. Each thread lives within its own apartment. Within an apartment, interface pointers can be passed without marshaling, and therefore, all objects in one single-threaded apartment thread communicate directly.

A logical grouping of related objects that all execute on the same thread, and therefore must have synchronous execution, could live on the same single-threaded apartment thread. However, an apartment model object cannot reside on more than one thread. Calls to objects in other processes must be made within the context of the owning process, so distributed COM switches threads for you automatically when you call on a proxy.

The interprocess and interthread models are similar. When it is necessary to pass an interface pointer to an object in another apartment (on another thread) within the same process, you use the same marshaling model that objects in different processes use to pass pointers across process boundaries. By getting a pointer to the standard marshaling object, you can marshal interface pointers across thread boundaries (between apartments) in the same way you do between processes. (Interface pointers must be marshaled when passed between apartments.)

Rules for single-threaded apartments are simple, but it is important to follow them carefully:

Every object should live on only one thread (within a single-threaded apartment).

Initialize the COM library for each thread.

Marshal all pointers to objects when passing them between apartments.

Each single-threaded apartment must have a message loop to handle calls from other processes and apartments within the same process. Single-threaded apartments without objects (client only) also need a message loop to dispatch broadcast sendmessages that some applications use.

DLL-based or in-process objects do not call the COM initialization functions; instead, they register their threading model with the ThreadingModel named-value under the InprocServer32 key in the registry. Apartment-aware objects must also write DLL entry points carefully. There are special considerations that apply to threading in-process servers. For more information, see In-Process Server Threading Issues.

While multiple objects can live on a single thread, no apartment model object can live on more than one thread.

Each thread of a client process or out-of-process server must call or call CoInitializeEx and specify COINIT_APARTMENTTHREADED for the dwCoInit parameter. The main apartment is the thread that calls CoInitializeEx first. For information on in-process servers, refer to In-Process Server Threading Issues.

All calls to an object must be made on its thread (within its apartment). It is forbidden to call an object directly from another thread; using objects in this free-threaded manner could cause problems for applications. The implication of this rule is that all pointers to objects must be marshaled when passed between apartments. COM provides the following two functions for this purpose:

CoMarshalInterThreadInterfaceInStream marshals an interface into a stream object that is returned to the caller.

CoGetInterfaceAndReleaseStream unmarshals an interface pointer from a stream object and releases it.

These functions wrap calls to CoMarshalInterface and CoUnmarshalInterface functions, which require the use of the MSHCTX_INPROC flag.

In general, the marshaling is accomplished automatically by COM. For example, when passing an interface pointer as a parameter in a method call on a proxy to an object in another apartment, or when calling CoCreateInstance, COM does the marshaling automatically. However, in some special cases, where the application writer is passing interface pointers between apartments without using the normal COM mechanisms, the writer must handle the marshaling manually.

If one apartment (Apartment 1) in a process has an interface pointer and another apartment (Apartment 2) requires its use, Apartment 1 must call CoMarshalInterThreadInterfaceInStream to marshal the interface. The stream that is created by this function is thread-safe and must be stored in a variable that is accessible by Apartment 2. Apartment 2 must pass this stream to CoGetInterfaceAndReleaseStream to unmarshal the interface and will get back a pointer to a proxy through which it can access the interface. The main apartment must remain alive until the client has completed all COM work (because some in-process objects are loaded in the main apartment, as described in In-Process Server Threading Issues). After one object has been passed between threads in this manner, it is very easy to pass interface pointers as parameters. That way, distributed COM does the marshaling and thread switching for the application.

To handle calls from other processes and apartments within the same process, each single-threaded apartment must have a message loop. This means that the thread's work function must have a GetMessage/DispatchMessage loop. If other synchronization primitives are being used to communicate between threads, the Win32 function MsgWaitForMultipleObjects can be used to wait both for messages and for thread synchronization events. The Platform SDK documentation for this function has an example of this sort of combination loop.

COM creates a hidden window using the Windows class "OleMainThreadWndClass" in each single-threaded apartment. A call to an object is received as a window message to this hidden window. When the object's apartment retrieves and dispatches the message, the hidden window will receive it. The window procedure will then call the corresponding interface method of the object.

When multiple clients call an object, the calls are queued in the message queue and the object will receive a call each time its apartment retrieves and dispatches messages. Because the calls are synchronized by COM and the calls are always delivered by the thread that belongs to the object's apartment, the object's interface implementations need not provide synchronization. Single-threaded apartments can implement IMessageFilter to permit them to cancel calls or receive window messages when necessary.

The object can be reentered if one of its interface method implementations retrieves and dispatches messages or makes an ORPC call to another thread, thereby causing another call to be delivered to the object (by the same apartment). OLE does not prevent reentrancy on the same thread, but it can help provide thread safety. This is identical to the way in which a window procedure can be reentered if it retrieves and dispatches messages while processing a message. However, calling an out-of-process single-threaded apartment server that calls another single-threaded apartment server will allow the first server to be reentered.

Send comments about this topic to Microsoft.

Copyright © 2005 Microsoft Corporation. All rights reserved.

Edited by WSCPorts

Share this post


Link to post
Share on other sites
This topic is now closed to further replies.
Sign in to follow this  
Followers 0