|
|
Home » Developing U++ » UppHub » Job package: A lightweight worker thread for non-blocking operations. (A)
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48794 is a reply to message #48793] |
Tue, 19 September 2017 11:12 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello Mirek,
Thanks for all the corrections. I find your opinions really valuable and useful. I was suspicios there was something wrong with that benchmark too.
Quote:This is not quite true (unlike Job, which in fact requires you to move the data if you need it outside the Job).
Not necessarily. I can always use a pointer or Ref(), or std::ref. (In it's current state it wouldn't be as clean as capturing, I admit, but can be improved)
Or I can simply pass reference, using lambda capture. That is not forbidden. But then I have to take into account the object's life time too.
Job solves some of those head aches. (Of course, tt may introduce its own).
For the sake of discussion (ugly):
In Job.h
template<class V = T>
Job(V v) : Job() { *data = v; }
then, in Example.cpp:
int i;
Job<int*> job(&i);
job.Start([]{ auto& n = *Job<int*>::Data() = 100;});
job.Finish();
Cout() << "Number is " << i << '\n';
Quote:future / promise also seems to have superior error handling facilities (exceptions can easily be passed from thread to caller).
Not really hard to add. I can: process JobErrors, and re-throw plain Upp::Exc, or std::exception category.
Quote:
The one thing that seems to be missing is future -> promise abort.
Job has a per-job cancel mechanism.
So what do you suggest?
Your Async wrapper is really simple and nice. Are you going to add it to U++?
Or should I improve Job? (add exception forwarding, easier way to pass references and pointers...)
My favourite : Or should I re-introduce Future/promise pair as was before? After all Job was inially a future/promise wrapper. But it lacked RAII and worker thead model.
Now they are in place. All I need to do is to incorporate it to the current model using the Async example you provided (except cowork).
(Without changing the public api of Job much. IMO It looks familiar and acts fine.)
Or should I simply abandon it? This tool is born out of need, which plain future/promise mechanism couldn't satisfy (i.e. a fine-grained control over threads for non-blocking behaviour.)
I don't see a reson to hang onto it if there will be a better solution.
Again, thank you very much for taking time to discuss the issue.
Job currently lacks shared states but it is not what Job is meant for anyway. IMO CoWork is there for it (and for bunch of other things).
Best regards,
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Tue, 19 September 2017 11:37] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48795 is a reply to message #48793] |
Tue, 19 September 2017 12:31 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
How about adding An Async/Result pair, the way you suggested.
ASync will be an improved version of the example you provided with (using CoWork), addition of result id generator (for cancellation), exception handling:
Result will be a wrapper for future where we can do such things as follows:
auto result1 = Async([=]{ return GetDivisors(); })
while(!result.IsReady())
; // Do something;
Cout() << result.IsValid() ? result.Get() : result.GetErrorDesc();
// and of course...
Cout() << Async([=]{ return "Hello World\n"; }).Get();
Result's Interface may be consisted of:
Result::Get();
Result::IsReady();
Result::IsValid();
Result::GetError();
Result::GetErrorDesc():
Result::GetId();
Result::Cancel();
If you'd like, I can implement it this way. (IMO Result is a more meaningful name, but naming can change...)
Best regards,
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Tue, 19 September 2017 12:35] Report message to a moderator
|
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48807 is a reply to message #48796] |
Sat, 23 September 2017 14:17 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello Mirek (and Upp community),
Job package is updated in accordance with the above discussion.
To sum up, it is heavily improved (tested on GCC 7.2.0, MinGW (supplied with U++), and latest MinGWx64, and MSC 2017):
- Void type instantiation is re-introduced for good (does not rely on anything other than plain template specializaton rules.)
- Value return semantics is re-introduced for good.
- Constant reference access operator is added. (This is especially useful when using Job with containers such as Vector, or array (e.g. Job<Vector<int>>) in a loop. See JobExample test app provided)
- Exception propagation mechanism is added. Workers will propagate raised exceptions to their caller. (when one of the GeResult(), operator~(), or IsError() methods is called. )
- JobExample test application is added to the package. This example demonstrates the above mentioned traits.
I believe with the recent update Job is now slightly superior to async/future/promise trio (not shared_future), in that it has full control over its thread (which is a proper worker thread) worker cancelleation/abortion mechanics,
and optional container-like reference access to its result which can reduce copying/moving where unnecessary. (Except Job doesn'T support reference specialization. Job<T&> is not possible for now. But that is not a big deal, really)
Bug reports, criticism and feedback is deeply appreciated.
[ See below message for new version ]
Best regards.
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Sat, 07 October 2017 14:55] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48829 is a reply to message #48796] |
Sat, 07 October 2017 15:03 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello,
Job package is now compatible with single-threaded U++ environment. (Yet -almost- all methods and global functions retain their functionality under ST env.)
Documentation and provided example is updated accordingly.
The code below is a part of JobExample.cpp and demonstrates both non-blocking behaviour and Job cancelling feature in a single-threaded environment:
void CancelJob()
{
// Singlethreaded version. (non-blocking operation)
// Below example is one of the simplest ways to achive non-blocking operations with Job in a
// single-threaded environment. A finer-grained operation would involve handling of return
// codes. (e.g. using Job<int>)
Job<void> job;
auto work = [=] {
if(IsJobCancelled()) {
Cout() << "Worker #" << GetWorkerId() << " received cancel signal. Cancelling job...\n";
throw JobError("Operation cancelled.");
}
};
Cout() << "Worker #" << GetWorkerId() << " started. (Waiting the cancellation signal...)\n";
const int timeout = 5000;
auto start_time = msecs();
while(!job.IsError()) {
job.Start(work);
if(msecs(start_time) >= timeout)
job.Cancel(); // Or if you have more than one non-blocking operation in progress,
// you can call CancelJobs() global function to cancel all running jobs.
}
}
As always, reviews, criticism, bug reports, etc. are deeply appreciated.
Best regards,
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Sat, 07 October 2017 15:09] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48830 is a reply to message #48829] |
Sat, 07 October 2017 15:51 |
|
mirek
Messages: 14039 Registered: November 2005
|
Ultimate Member |
|
|
Oblivion wrote on Sat, 07 October 2017 15:03Hello,
Job package is now compatible with single-threaded U++ environment. (Yet -almost- all methods and global functions retain their functionality under ST env.)
ST is now deprecated...
BTW, have you noticed that, inspired by this discussion thread, CoWork now has cancelation and exception propagation?
IMO, that makes it very similar to job, the only part that is really missing is the return value.
Actually, the main reason I have not implemented that yet is that I do not like the explicit specification of return value, as in Job. Which requires to implement it as function, which in turn requires some form of 'future'.
Mirek
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48831 is a reply to message #48830] |
Sat, 07 October 2017 16:04 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello Mirek,
Quote:ST is now deprecated...
Sure, The reason I added ST support is that I have some code that needs to be backward (ST) compatible. And for those who want't to keep their code as such. (It wasn't very difficult to add, anyway).
Quote:
BTW, have you noticed that, inspired by this discussion thread, CoWork now has cancelation and exception propagation?
IMO, that makes it very similar to job, the only part that is really missing is the return value.
Actually, the main reason I have not implemented that yet is that I do not like the explicit specification of return value, as in Job. Which requires to implement it as function, which in turn requires some form of 'future'.
Mirek
No, I didn't notice it till now, that's great, one less reason for me to use Job, thanks!
I'll try and see if I can use CoWork instead. (It shouldn't be very difficult)
Job's return semantics proved really useful, so for the time being I intend to maintain it.
And in the meantime let's see if I can come up with a suitable alternative to std::future (to use it with CoWork).
Best regards,
Oblivion.
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Sat, 07 October 2017 16:33] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48832 is a reply to message #48830] |
Sun, 08 October 2017 15:01 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello Mirek,
I wrote a prototype for U++ implementation of promise/future mechanism. It is 104 LOCs, and it took an hour for me to to design and write it (It is in its current state not perfect but it works.)
It is consisted of three classes (please do not dwell on naming, I didn't think them through, they'll change):
- WorkEntry -> std:promise (This class is not explicitly called. It is allocated on the heap (using new), and its ownership is passed to WorkResult.)
- WorkResult -> std: future (With pick semantics, void type specialization, error handling, exception propagation, and optional constant reference access to the result.)
- Work (Contains a CoWork instance, and exposes Do() and Finish() methods.)
I re-implemented your Async() code in Work class, using CoWork::Do() instead of CoWork::Schedule(). The rationale behind this decision is that sometimes asynchronous operations need to be externally blocked, all at once ("wait for all works to finish"). And I found no other reliable way. Also, works use a single semaphore to wait and run.
Example code:
// Error handling, picking/moving.
auto r1 = work.Do([=] { throw Work::Error("Worker failed."); });
work.Finish();
auto r2 = pick(r1); // r1 is picked.
if(r2.IsError()) {
Cout() << r2.GetErrorDesc() << '\n';
}
There is room for improvements, and probably some issues I didn't encounter yet or I overlooked. Feel free to comment on it. (One area that needs improving and to be made more error resistant is moving/picking behaviour. I'll improve them)
Below you can find the Work, and WorkExample, which is basically JobBencmark adapted to Work.
Best regards,
Oblivion
-
Attachment: Work.zip
(Size: 3.19KB, Downloaded 266 times)
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Sun, 08 October 2017 15:07] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48835 is a reply to message #48830] |
Tue, 10 October 2017 11:51 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
A further refinement would be to explicity relate CoWork to promise/future pattern:
My suggested namings:
-
- WorkResult (former WorkEntry): Unlike std::promise, this is called privately. I don't really see any use in calling it explicitly
- Work (former WorkResult): This can be the U++ std::future counterpart (see the first prototype packageI provided above) This can be a helper class to CoWork, representing a single, isolated (semantically) thread.
- CoWork::Work(): This is the thread-starter method which will return, well, Work :)
I propose adding CoWork::Work() as a (non-static) method to CoWork, because from my experience with Job,Thread, future/promise and CoWork, and as I noted on my previous message, keeping workers contained in CoWork instances, using a CoWork::Do() call, have some advantages over using a static method such as CoWork::Schedule(): Such as the ability to use CoWork::Finish() on demand, and waiting the workers to be finished automatically on class instance destruction.
As for the error management mechanism using a specific exception type such as Work::Error, along with IsError(), GetError() and GetErrorDesc() methods, IMO they would be a very useful addition, but they are not necessary, can be removed.
What do you think?
Best regards,
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Tue, 10 October 2017 12:01] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48836 is a reply to message #48835] |
Tue, 10 October 2017 16:10 |
|
mirek
Messages: 14039 Registered: November 2005
|
Ultimate Member |
|
|
My try.
#include <Core/Core.h>
using namespace Upp;
template <class Ret>
class AsyncWork {
template <class Ret>
struct Imp {
CoWork co;
Ret ret;
template< class Function, class... Args>
void Do(Function&& f, Args&&... args) { co.Do([=]() { ret = f(args...); }); }
const Ret& Get() { return ret; }
};
template <>
struct Imp<void> {
CoWork co;
template< class Function, class... Args>
void Do(Function&& f, Args&&... args) { co.Do([=]() { f(args...); }); }
void Get() {}
};
One<Imp<Ret>> imp;
public:
template< class Function, class... Args>
void Do(Function&& f, Args&&... args) { imp.Create().Do(f, args...); }
void Cancel() { if(imp) imp->co.Cancel(); }
Ret Get() { ASSERT(imp); imp->co.Finish(); return imp->Get(); }
Ret operator~() { return Get(); }
AsyncWork(AsyncWork&&) = default;
AsyncWork() {}
~AsyncWork() { if(imp) imp->co.Cancel(); }
};
template< class Function, class... Args>
AsyncWork<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
Async(Function&& f, Args&&... args)
{
AsyncWork<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>> h;
h.Do(f, args...);
return pick(h);
}
CONSOLE_APP_MAIN
{
auto h = Async([] { return "Hello world"; });
DDUMP(h.Get());
DDUMP(~Async([](int x) { return x * x; }, 9));
auto x = Async([] { throw "error"; });
try {
x.Get();
}
catch(...) {
DLOG("Exception caught");
}
}
I guess it is quite similar now.
[Updated on: Tue, 10 October 2017 16:17] Report message to a moderator
|
|
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48840 is a reply to message #48839] |
Tue, 10 October 2017 23:48 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello Mirek,
tested Async/AsyncWork with MSC 2017, MinGW on windows, and with GCC on linux.
- It compiles on MSC without any hiccup.
- It does not compile on GCC (7.2) or MinGW unless the nested classes are moved out, (That's why I wrote my prototype that way.) and "Ret" is changed to some other parameter name. Here are the error codes I get:
\AsyncTest.cpp (8): error: declaration of template parameter 'Ret' shadows template parameter
\AsyncTest.cpp (18): error: explicit specialization in non-namespace scope 'class AsyncWork<Ret>'
\AsyncTest.cpp (19): error: template parameters not deducible in partial specialization:
\AsyncTest.cpp (31): error: too many template-parameter-lists
\AsyncTest.cpp (47): error: 'class AsyncWork<const char*>' has no member named 'Do' (): h.Do(f, args...);
\AsyncTest.cpp: In instantiation of 'AsyncWork<typename std::result_of<typename std::decay<_Tp>::type(std::decay_t<Args>...)>::type> Async(Function&&, Args&& ...) [wit
h Function = ConsoleMainFn_()::<lambda(int)>; Args = {int}; typename std::result_of<typename std::decay<_Tp>::type(std::decay_t<Args>...)>::type = int]':
\AsyncTest.cpp (56): required from here
\AsyncTest.cpp (47): error: 'class AsyncWork<int>' has no member named 'Do'
\AsyncTest.cpp (47): error: 'class AsyncWork<void>' has no member named 'Do'
\AsyncTest.cpp (11): error: 'AsyncWork<Ret>::Imp<Ret>::ret' has incomplete type
\AsyncTest.cpp (11): error: invalid use of 'void'
\AsyncTest.cpp (15): error: forming reference to void
\AsyncTest.cpp (34): error: 'struct AsyncWork<void>::Imp<void>' has no member named 'Get'; did you mean 'ret'?
\AsyncTest.cpp (34): error: return-statement with a value, in function returning 'void' [-fpermissive]
- More importantly there seems to be something wrong with the exception propagation mechanism. For,
1) Sometimes it fails to catch the exception, and the application crashes with that exception.
2) When it catches the exception the application hangs at the end (after the "exception caught" message is printed.)
3) Sometimes the application simply hangs.
I got this erratic behaviour both on windows and on linux, on a single machine, so it maybe a local hardware problem, I need to investigate it further...
Quote:s a tricky catch with IsFinished:
template <class Range>
ValueTypeOf<Range> ASum(const Range& rng, const ValueTypeOf<Range>& zero)
{
int n = rng.GetCount();
if(n == 1)
return rng[0];
if(n == 0)
return 0;
auto l = Async([&] { return ASum(SubRange(rng, 0, n / 2)); });
auto r = Async([&] { return ASum(SubRange(rng, n / 2, n - n / 2)); });
while(!l.IsFinished() || !r.IsFinished())
Sleep(1);
return l.Get() + r.Get();
}
What do you think is wrong with this code? Smile
Mirek
Sure, but can this really be attributed to a design flaw?
I mean, f I'm not really missing anything else, it seems that here we simply have a careless programming.
Recursion is potentially tricky by nature, and requires the developer to be extra cautious with his/her assumptions.
A proper use of IsFinished() can be found in my JobBenchmark example , where it is simply used to check the worker, and move on to others if the job is not finished... (at least, that's what I have in my mind in the first place)
Best regards.
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Wed, 11 October 2017 08:44] Report message to a moderator
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48841 is a reply to message #48840] |
Wed, 11 October 2017 09:23 |
|
mirek
Messages: 14039 Registered: November 2005
|
Ultimate Member |
|
|
Oblivion wrote on Tue, 10 October 2017 23:48Hello Mirek,
tested Async/AsyncWork with MSC 2017, MinGW on windows, and with GCC on linux.
- It compiles on MSC without any hiccup.
- It does not compile on GCC (7.2) or MinGW unless the nested classes are moved out, (That's why I wrote my prototype that way.) and "Ret" is changed to some other parameter name. Here are the error codes I get:
After a bit of wrestling with C++11, it now compiles.
Quote:
- More importantly there seems to be something wrong with the exception propagation mechanism. For,
1) Sometimes it fails to catch the exception, and the application crashes with that exception.
2) When it catches the exception the application hangs at the end (after the "exception caught" message is printed.)
3) Sometimes the application simply hangs.
I got this erratic behaviour both on windows and on linux, on a single machine, so it maybe a local hardware problem, I need to investigate it further...
I would like to investigate, but need a testcase.
Quote:
Sure, but can this really be attributed to a design flaw?
Recursion is potentially tricky by nature, and requires the developer to be extra cautious with his/her assumptions.
Well, recursion aside, I think that in general, we want the mechanism reentrant. I mean, it should not be a part of contract/function documentation whether is it using IsFinished internally.
I can actually fix the issue, but then IsFinished will not be doing the same thing.
BTW, the very same issue is true for CoWork. So something to think about..
Mirek
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48842 is a reply to message #48841] |
Wed, 11 October 2017 09:42 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Quote:
Well, recursion aside, I think that in general, we want the mechanism reentrant. I mean, it should not be a part of contract/function documentation whether is it using IsFinished internally.
Ah, I see. Apparently I'm still not thinking out of my Job-mindset (It was theoretically reentrant, although I did not actually test it.).
CoWork is a different beast internally. I forgot that.
As for the erratic exception handling, I didn't use any code other than you provided above. Simply put, it sometimes works and sometimes doesn't. I didn't notice any regular pattern.
Best regards,
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Wed, 11 October 2017 09:44] Report message to a moderator
|
|
|
|
|
|
Re: RE: Job package: A scope-bound worker thread for non-blocking operations. [message #48847 is a reply to message #48846] |
Wed, 11 October 2017 20:14 |
Oblivion
Messages: 1112 Registered: August 2007
|
Senior Contributor |
|
|
Hello Mirek,
Quote:Anyway, (maybe I forgot to mention to AsyncWork is now in U++): Are you trying with current trunk?
This is great news! Thank you very much for your patience and efforts. This is a really handy tool which obsoletes Job.
As for the weird error I get, it seems to be stopped after I purged UPPOUT.
IF I encounter it again, I'll report it.
By the way, I noticed that there are missing topic titles in Core/src.tpp : (Huge, Function, Unicode Support). Also, Huge class title is missing in it's doc.
Best regards.
Oblivion
Github page: https://github.com/ismail-yilmaz
upp-components: https://github.com/ismail-yilmaz/upp-components
Bobcat the terminal emulator: https://github.com/ismail-yilmaz/Bobcat
[Updated on: Wed, 11 October 2017 20:53] Report message to a moderator
|
|
|
Goto Forum:
Current Time: Sat Sep 21 00:43:43 CEST 2024
Total time taken to generate the page: 0.07488 seconds
|
|
|