Jump to content

Alternatives to locking


Recommended Posts

I have made some alternative GOOP that do not require external locking of the get modify set pass (i believe, but i leave it for other to decide for themselves).

There are three alternatives:

1. FG. A functional global made as reentrant and called by a reference node. A "new" call consist of opening a new instance of the reentrant VI. All the methods are set in the "action" typedef enum. I have used this method for several years, but i always thought of it as a LCOD variant, but i see now that it is just as much a GOOP variant. There is no need for locking because all the methods are resolved internally (no get modify set). Normally i have used this with no wrappers, but when using LV8.2 it is much more convenient to make the whole system a LVOOP with the reference as a class, and then there is no way without a wrapper. Performance vise it is slightly faster than dqGOOP in get, set and modify, but much slower in create and delete (performance in create and delete is however irrelevant in most cases, at least in my applications, and for normal number of instances, 100 or so? this really makes no difference. I think the problem is that create/delete scales very poorly when number of instances increases).

2. Pointer. Here i use a LV8.2 version of the pointer system (see CR). Othervise this is very similar to the FG version. Locking is also internal here. Performance vise this is fastest. It is superior in get/set, but not only slightly faster in modify compared with FG. Create and delete is of course several orders of magnitude faster than FG. Since this is the fastest method, i will gradually use this instead of the FG i use now. Even with an external lock (which becomes an advantage programatically), the performance in modify is almost exactly that of the FG.

For both these cases i have also made an external lock in the modify method. There a normal get-modify-set pass is used, and using a queue to lock the pass. The lock doesnt really do that much different, maybe only 50% slower in the tests, and it is still faster than dqGOOP for some reason. However, programatically a lock has an advantage since i don't have to modify the core when i add methods. (I normally don't mind modifying the core myself if this result in better performance, but it is much more difficult to debug).

3. FG LV2OO. This is an FG version where i use LV2OO (Labview 2 style global with action classes made in LVOOP, see aristos post about the subject) instead of the traditional LV2 global (or functional global). This has a performance penalty (approximately a factor of 3 compared with FG). Programatically this has some advantages. The lock is now internal, still there is no need to modify the core. To add more methods i simply add more "do" actions, inhereted from the parent action class. I can also modify the get and set methods without any changes to the core, which is not possible in the FG, in fact i can modify almost anything without that resulting in modifications to anything else. Programatically this therefore is the preferred method, but for me the performance penalty is too large (although it is only about a factor 2.5 slower than dqGOOP, and thus much faster than the old NI GOOP and OpenGOOP).

I found what i believe is a bug (though i am not sure). The action do method is withing the global. the global is called by a reference node. When i wire the action class to the reference node, the input is OK. The output will however always be the parent class, and i have to manually cast the wire to the specific class. If this was an ordinary VI (not a call by reference node) then i do not need to do this manually, it happends automatically.

Here are the files. A similar dqGOOP is included for comparison. It is all LV8.2

Download File:post-4885-1160384003.zip

Link to comment
I found what i believe is a bug (though i am not sure). The action do method is withing the global. the global is called by a reference node. When i wire the action class to the reference node, the input is OK. The output will however always be the parent class, and i have to manually cast the wire to the specific class. If this was an ordinary VI (not a call by reference node) then i do not need to do this manually, it happends automatically.

The failure of the Call By Ref node to do the automatic downcasting is not a bug. Jimi and I discussed this earlier regarding the dynamic dispatch VIs which do not automatically downcast their outputs. The ability of a subVI node to automatically downcast outputs is based on the block diagram of the VI and whether or not that block diagram preserves run time type. Since they dynamic dispatch subVIs don't know what diagram will be invoked (and new classes could be dynamically loaded into memory at any time that do not support the automatic downcasting) we can't do the downcast. The same is true for the Call By Ref node. The VI you happen to be calling could do the downcast, but the CBR doesn't know what VI that will be until you actually are running.

Link to comment
The failure of the Call By Ref node to do the automatic downcasting is not a bug.

To find more about the technique Stephen is referring to you can alway try to read Stephen's patent application: Type propagation for automatic casting of output types in a data flow program

Abstract

System and method for implicit downcasting at compile time in a data flow program. A first data flow function in an object-oriented dataflow program is identified, where the first function includes an input of a parent data type and an output of the parent data type. The first function is analyzed to determine if the output preserves the run-time data type of the input. A second dataflow function in the object-oriented data flow program is identified, where the second function includes a program element that calls the first function, passing an input parameter of a child data type of the parent data type as input. If the analysis determines that an output parameter returned by the output of the first function will always be of the child data type, the program element is automatically configured at compile time to always downcast the output parameter from the parent data type to the child data type at run-time.

I read quite a few NI patent application, or the abstract and the first claim. Stephen's application is still quite good, but those application about automatically generating graphical program based on user input... It seems NI is trying to patent scripting many years later they introduced it.

Link to comment

OK, i think i understand the call by ref downcast thing. The call by ref node have no way of knowing which one of the child classes (or parent) the called VI will eventually call.

But regarding the locking. Are my assumptions correct that within the global the get-modify-set pass is locked? I mean since there are no traditional get-mod-set pass at all, only an ordinary functional global VI call, then i would believe that this is thread safe. Will there be any change if the modify VI is reentrant?

Link to comment
But regarding the locking. Are my assumptions correct that within the global the get-modify-set pass is locked? I mean since there are no traditional get-mod-set pass at all, only an ordinary functional global VI call, then i would believe that this is thread safe. Will there be any change if the modify VI is reentrant?

Global VIs are not in any way thread safe. It is one of the reasons that LV advocates functional globals (aka LV2-style globals) whenever you're doing parallel work.

Link to comment
Global VIs are not in any way thread safe. It is one of the reasons that LV advocates functional globals (aka LV2-style globals) whenever you're doing parallel work.

When talking about global here, i mean LV2 global or functional global. I take your answer for a yes then.

But now i just read this (from another thread):

LV2-style globals provide safety for whatever operations are in the LV2-style global, but try to string multiple operations together and you have problems. Global VIs guarantee that it is safe to read entire large cluster values from the globals without a write occuring mid-way through the read. But other than this primitive locking, they're wide open.

This makes me confused again. So i will ask the question a bit different. A functional global ala LCOD has lots of actions. Every one of these actions exist inside the global, and the action itself is a normal block diagram and can consist of call to sub vis and so on. Is a call to that global safe? Do i run any risk of "inter-global-confusion" if two or more calls to the same instance happends simultaneously?

I think a functional global is safe, because if the opposite was true, then all calls to any sub VI (functional global or not) would not be safe, and the whole concept of multithreading in LV would be meaningless. But right now i am a bit confused.

Link to comment

bsvingen,

I think the LV2 global approach is nice, but it does not allow for simulatenous access, e.g. if one action involves waiting for an response from an instrument, all other actions would have to wait for this response to arrive.

If we could preserve the LVOOP benefits, but with the addition of synchronization, we could probably increase the overall performance and still have "almost" simultaneous access.

I have included a small example of how synchronization could be achieved, but I haven't tested this with LVOOP classes.

It basically adds a copy of the data in a synchronization queue, while keeping local copies in each wire branch.

Please let me know what you think.

Edit: I know, you will probably say that the LV2-global will outperform this in terms of performance, and you are right.

I made a LV2-style core that did the same thing much faster, synchronization was down to ~250ns in the case of no update

/J

Download File:post-5958-1160553720.zip

Link to comment
bsvingen,

I think the LV2 global approach is nice, but it does not allow for simulatenous access, e.g. if one action involves waiting for an response from an instrument, all other actions would have to wait for this response to arrive.

Yes, the pointer version will do this (also with the external locking since it is not reentrant), but the LCOD version will not do this since this is reentrant, i think.

I like this approach of data and ref(s) in the same wire.

Link to comment
Yes, the pointer version will do this (also with the external locking since it is not reentrant), but the LCOD version will not do this since this is reentrant, i think.

I don't think that the reentrant VI really offers this.

I tested to add a 100ms delay in the core__FGA.vi, then I called set and get in parallell.

The resulting time was 200ms, so even if the core is reentrant we have a sequential access.

/J

Link to comment
I don't think that the reentrant VI really offers this.

I tested to add a 100ms delay in the core__FGA.vi, then I called set and get in parallell.

The resulting time was 200ms, so even if the core is reentrant we have a sequential access.

/J

I checked this. You also have to set the modify_FGA.vi to reentrant and it will take 100 ms (i forgot that earlier). You do this, and the VIs will operate in parallel.

Link to comment
But now i just read this (from another thread):

This makes me confused again. So i will ask the question a bit different. A functional global ala LCOD has lots of actions. Every one of these actions exist inside the global, and the action itself is a normal block diagram and can consist of call to sub vis and so on. Is a call to that global safe? Do i run any risk of "inter-global-confusion" if two or more calls to the same instance happends simultaneously?

Suppose I have a LV2-style global that stores a double-precision floating point value and supports these three operations:

"Round to nearest integer"

"Get Value"

"Add 1"

"Divide by 2"

and

"Multiply by 2"

Then I write this code:

1) Round to nearest integer

2) Get the value

3) if the value is odd, then Add 1

4) if the value is not equal to 2, then Divide by 2

My expectation is that I will always end up with an even number for my result (I realized had to put the "not equal to 2" in for this example since 2/2 is 1). For whatever reason, I need to make sure the value in there is two. Now I do these three steps in two parallel sections of code (a and b). Remember, each individual operation is protected, but the aggregate is not protected. So I end up with this:

Initial value in LV2-style global: 5.3

1a) Round to nearest integer (was 5.3, now 5.0)

2a) Get the value (it returns 5.0)

1b) Round to nearest integer (was 5.0, now 5.0)

2b) Get the value (it returns 5.0)

3a) if the value is odd (we got 5.0) then Add 1 (was 5.0, now 6.0)

3b) if the value is odd (we got 5.0) then Add 1 (was 6.0, now 7.0)

4a) if the value is not equal to two (we got 5.0, so we assume it was 6.0) then Divide by 2 (was 7.0 now 3.5)

4b) if the value is not equal to two (we got 5.0, so we assume it was 6.0) then Divide by 2 (was 3.5 now 1.75)

A completely different result occurs if the user puts a semaphore around the code so that these instructions are forced to happen as a complete block -- the final answer comes out to 2.0.

This is what I'm talking about == each individual operation is locked, but the overall operation is not locked.

Link to comment

OK, thanks. I can put a semaphore around the block to serialize the operation, and the problem can be solved. What i wondered about was if i put the whole operation as a block, inside a LV2 global, then the LV2 global will protect the operation just as if the operation was protected by semaphores. That is what my three examples does, and so far i have seen nothing that should suggest othervise.

Link to comment
OK, thanks. I can put a semaphore around the block to serialize the operation, and the problem can be solved. What i wondered about was if i put the whole operation as a block, inside a LV2 global, then the LV2 global will protect the operation just as if the operation was protected by semaphores. That is what my three examples does, and so far i have seen nothing that should suggest othervise.

Excellent. I think we're both on the same page now.

Link to comment
I checked this. You also have to set the modify_FGA.vi to reentrant and it will take 100 ms (i forgot that earlier). You do this, and the VIs will operate in parallel.

I still don't think that the methods can be called in parallel.

I took your original code and removed all class specific code, and then added a 100ms delay to the core VI itself.

Then I created a test VI that does the following

1. init (basically only loads a reference to the core VI)

2. get and set in parallell (just calls the core that is empty, except for the 100ms wait)

3. delete

Download File:post-5958-1160737603.zip

when I run this test the total time for get/set is still 200ms, so I do not understand how they could be called simultaneously?

/J

Link to comment

:D You are right about that.

I used your idea with sending a locking queue with the reference to modify the pointer version. It turns out that this makes it possible to very easy make two versions by just changing the "modify" VI. I have called one for _serial and the other for _lock. The serial version is just an ordinary VI, and therefore execute in serial fashion. It doesnt use the locking queue at all, and therefore becomes very efficient. The other one use the queue to lock the get modify set pass, and is set to reentrant and execute in parallel.

Edit: added a newer version that has an additional serial modify pass which is internal (thus more speed :D ).

Download File:post-4885-1160807656.zip

To sum it up (when using the pointer system):

Internal locking (modify internally to the global, there is no get modify set pass at all):

This is both fastest and safest since deadlock and so on is impossible. It will allways execute in serial for the same reference and in parallel for different references. The downside is if one reference wait for some hardware measurements to be available, this will block all the other references as well (when using the pointer system, but not neccesarliy when using FG). For all other cases however, this will be the fastes.

Locking by serializing the get-modify-set pass:

A get-modify-set pass is used, and that pass is set in a non-reentrant VI. The pass will allways operate in serial mode, thus the pass is locked with respect to other passes with the same VI. Since only the pass is serialized, this version will not block other references, or the get method. This version is also very fast.

Locking by queue

A queue is used for locking, and is passed with the reference for each unique reference:

The get-modify-set pas is now reentrant, and operates in parallel. The queue will lock the pass. Basically this method will do nothing more than the serialized GMS pass, because all it does is to serialize the pass with a lock instead of using the VI properties. There is however one advantage, and that is timout. Using queue for the lock enables the use of timeouts which in some circumstances is neccesary. Another advantage is that it will actually operate in parallel in dual processor systems (and in general with regard to internal timers). It is much slower than the other two methods.

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.