Announcement

Collapse
No announcement yet.

Threadsafe Global Vars

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • Threadsafe Global Vars

    I know that, generally, everyone likes to avoid global vars. But, they're very useful in some situations where multiple functions need access to the same data. This isn't really an issue as long as the global data doesn't change (e.g. static config params). However, in some situations, that data can be dynamic (e.g. event counters, state conditions). In these cases, the Win32 InterLockedX functions seem to work well, so I've been testing them in my current project. Here are some macros that I'm playing with. They're based on functions in Kernel32.dll ( https://docs.microsoft.com/en-us/win...ariable-access ) which are supported by the PB win32.api.inc include file.

    Code:
    MACRO Set32 (dst, dat32)
      InterLockedExchange(dst, dat32)
    END MACRO
    MACRO Get32 (src) = InterLockedExchange(src, 0)
    MACRO Inc32 (dst)
      InterlockedIncrement (dst)
    END MACRO
    MACRO Dec32 (dst)
      InterLockedDecrement (dst)
    END MACRO
    MACRO Set64 (dst, dat64)
      InterLockedCompareExchange64(BYVAL VARPTR(dst), dat64, dst)
    END MACRO
    MACRO Get64 (src) = InterLockedCompareExchange64(BYVAL VARPTR(src), src, 0)
    MACRO Inc64 (dst)
      MACROTEMP qOld
      LOCAL qOld AS QUAD
      DO
        qOld = dst
      LOOP WHILE InterlockedCompareExchange64(dst, qOld + 1, qOld) <> qOld
    END MACRO
    MACRO Dec64 (dst)
      MACROTEMP qOld
      LOCAL qOld AS QUAD
      DO
        qOld = dst
      LOOP WHILE InterlockedCompareExchange64(dst, qOld - 1, qOld) <> qOld
    END MACRO
    The PB versions of InterlockedIncrement64 and InterlockedDecrement64 require pointers to QUAD vars and don't work correctly (call to InterlockedCompareExchange64 argument seems incorrect) so my Inc64 and Dec64 macros are a modified version of the PB functions.

    Here is one of the functions from the PB library. If gDestination is replaced with BYVAL gDestination then those functions seem to work.
    Code:
    MACRO FUNCTION InterlockedIncrement64 (qDestination)
        MACROTEMP qOld
        LOCAL qOld AS QUAD
        DO
            qOld = PEEK(QUAD,qDestination)
        LOOP WHILE InterlockedCompareExchange64(qDestination, qOld + 1, qOld) <> qOld
    END MACRO=(qOld+1)
    Also, I'm not sure if "get" functions are actually required. Initial testing seems to indicate that simple reads of global data don't interfere with protected (interlocked) writes. I haven't tested that exhaustively yet.

    Comments and criticisms are always welcome.

  • #2
    I don't know why they have used macros an PEEK. In my Windows API headers, I have them coded as follows:

    Code:
    FUNCTION InterlockedIncrement64 (BYREF Addend AS QUAD) AS QUAD
       LOCAL Old AS QUAD
       DO
          Old = Addend
       LOOP WHILE InterlockedCompareExchange64(Addend, Old + 1, Old) <> Old
       FUNCTION = Old + 1
    END FUNCTION
    Code:
    FUNCTION InterlockedDecrement64 (BYREF Addend AS QUAD) AS QUAD
       LOCAL Old AS QUAD
       DO
          Old = Addend
       LOOP WHILE InterlockedCompareExchange64(Addend, Old - 1, Old) <> Old
       FUNCTION = Old - 1
    END FUNCTION
    Code:
    FUNCTION InterlockedExchange64 (BYREF Target AS QUAD, BYVAL Value AS QUAD) AS QUAD
       LOCAL Old AS QUAD
       DO
          Old = Target
       LOOP WHILE InterlockedCompareExchange64(Target, Value, Old) <> Old
       FUNCTION = Old
    END FUNCTION
    Forum: http://www.jose.it-berater.org/smfforum/index.php

    Comment


    • #3
      I agree on the PEEK, and thought that, maybe the MACRO would be a bit faster (no overhead for a call) but didn't test - until now. It appears that there's virtually no performance hit for a FUNCTION. I suspect that most of the processing time is consumed by the M$ Interlocked function and any delta is negligible. So, goodbye macro

      Comment


      • #4

        WaitForSingleObject has a 64 thread count limit per group
        THREADCOUNT can be inaccurate if a thread has not allocated

        This method using InterlockedIncrement(gThreadsRun) or Jerry's Inc32(gThreadsRun)
        to count that all the threads have finished always works

        Code:
        #INCLUDE "win32api.inc"
        
        MACRO Inc32 (dst)
          InterlockedIncrement (dst)
        END MACRO
        
        %ThreadsWanted=65           'intentionally used a number greater than 64
        GLOBAL gThreadsRun AS LONG  'count the number of threads that have finished
        
        FUNCTION PBMAIN () AS LONG
        
         LOCAL x AS LONG
         DIM hThread(%ThreadsWanted) AS LONG
        
         FOR x=1 TO UBOUND(hThread)
          THREAD CREATE MyThread(x) TO hThread(x)
          THREAD CLOSE  hThread(x)  TO hThread(x)
         NEXT
        
         DO
          SLEEP 100
         LOOP UNTIL gThreadsRun=%ThreadsWanted
        
        END FUNCTION
        
        THREAD FUNCTION MyThread(BYVAL NotUsed AS LONG) AS LONG
        
         Inc32(gThreadsRun) 'Jerry Wilson MACRO  Thank you!
        
        END FUNCTION
        How long is an idea? Write it down.

        Comment


        • #5
          and thought that, maybe the MACRO would be a bit faster (no overhead for a call)…
          There is no "call" to a macro.

          MACRO is an automated "copy and paste". The code of the macro is placed into the procedure (function or sub) at compile time. Ends up the same as if you typed the code there.

          The advantage is when you use the same code repeatedly. You put macro name instead of retyping the code.

          Cheers,
          Dale

          Comment


          • #6
            Originally posted by Dale Yarker View Post
            There is no "call" to a macro.
            Understood. The implication was no overhead for a call to a procedure (Function or Sub) that has some overhead (stack stuff, whatever) vs code placed in a procedure with a macro. In any case, there doesn't seem to be a measurable performance difference here between the two.

            Generally, I prefer procedures over macros. In some cases, macros seem to execute code faster but procedures are easier to troubleshoot. I don't have to worry about forward referencing with a procedure. Macros are nice for aliasing data types. For instance, I use a 32-bit time library (functions similar to time(), difftime(), ctime()) but have to be careful not to mix LONG and DWORD vars. So, I alias LONG with TIME32 and use it for all time function var definitions.

            Comment


            • #7
              Generally, I prefer procedures over macros.
              I don't believe you "get it yet". The macro is not something "instead" of a procedure. It (the equivalent of) auto types the source code in the macro into the source code of procedure at compile time before compiling. It is not a matter of "over". There are only procedures. Macro only saves typing (if used multiple times).


              The only way it might appear faster is if it saves a call to yet another procedure by putting the "guts" of the second procedure into the first.
              Dale

              Comment


              • #8
                Originally posted by Dale Yarker View Post
                I don't believe you "get it yet".
                Not too worry - I get it. I'm just not explaining myself clearly enough.

                Thanks.

                Comment


                • #9
                  Originally posted by Jerry Wilson View Post
                  I know that, generally, everyone likes to avoid global vars. But, they're very useful in some situations where multiple functions need access to the same data. This isn't really an issue as long as the global data doesn't change (e.g. static config params). However, in some situations, that data can be dynamic
                  My understanding was that for "set once, read many" global, the above applies, but if a global needs to be updated, I thought a Critical Section as the tool of choice to protect it during the update.

                  Real programmers use a magnetized needle and a steady hand

                  Comment


                  • #10
                    Originally posted by Bud Durland View Post

                    My understanding was that for "set once, read many" global, the above applies, but if a global needs to be updated, I thought a Critical Section as the tool of choice to protect it during the update.
                    I think that there are two issues.

                    A critical section will protect, well, a section of code. Take for instance a network application that uses multiple threads to send and receive messages. And these threads call common functions to do part of the processing (e.g. netEncyptMsg, netDecryptMsg). It may be necessary to use a critical section to "protect" the code in netEncryptMsg and netDecryptMsg because those functions allocate static memory buffers for performance improvement. A THREADSAFE descriptor could also be used which, I believe, uses a semaphore (this is a whole different discussion).

                    Now consider that we want to use performance counters to keep track of usage and processing errors. For instance, we may want to track decryption failures, which could indicate a DoS attack. For convenience, these counters are stored in a global UDT and updated by all of the threads and supporting functions. Another thread may be periodically evaluating the counter data looking for trends (e.g. indicators of a DoS attack). Since concurrent incrementing of a counter can cause accuracy issues, this operation needs to be protected. Using the Win32 API Interlocked functions seems to do this well. Now all of the threads and supporting functions can safely update the global counters. A structure of counters could look like this:

                    Code:
                    TYPE NET_PERFCOUNTERS
                      rxMsg       AS QUAD
                      txMsg       AS QUAD
                      rxGood      AS QUAD
                      txGood      AS QUAD
                      rxBadSize   AS QUAD
                      rxRSAFail   AS QUAD
                      rxAESFail   AS QUAD
                      rxBadSeq    AS QUAD
                      rxBuffFull  AS QUAD
                      rxSockErr   AS QUAD
                      txSockErr   AS QUAD
                      rxKeyExpire AS QUAD
                      txKeyExpire AS QUAD
                    END TYPE
                    
                    GLOBAL cnt AS NET_PERFCOUNTERS
                    For instance, if a msg cannot be correctly decrypted by netDecryptMsg (probably a CRC error), that function can return an error to the calling thread and the calling thread can update the appropriate counter with a simple call like Inc64(cnt.rxAESFail).

                    Comment


                    • #11
                      My understanding was that for "set once, read many" global, the above applies, but if a global needs to be updated, I thought a Critical Section as the tool of choice to protect it during the update.
                      Bud,
                      I think that was well stated.
                      THREADSAFE keyword is my usual method if variables can be updated in a function.

                      Often threads can update only there own specific element and everything can be combined at the end.
                      This demonstrates that method and it doesn't require any special handling.
                      If disk i/o functions share a file in a threaded program they must be thread protected.

                      Here is a template where each thread writes only to its own global string element.
                      To make writing to any element thread safe, just add the keyword THREADSAFE.
                      It also demonstrates starting the action in another thread to free the dialog from waiting.

                      Notice in this template there is no CriticalSection, WaitForSingleObject,THREADCOUNT or THREADSAFE keyword.

                      TemplateThread.bas
                      Code:
                      #PBFORMS CREATED V2.01   'TemplateThread.bas
                      #COMPILE EXE
                      #DIM ALL
                      
                      MACRO Inc32 (dst)
                        InterlockedIncrement (dst)
                      END MACRO
                      
                      #INCLUDE "win32api.inc"
                      %ThreadsWanted=26
                      GLOBAL gThreadsRun AS LONG   'done when gThreadsRun  = %ThreadsWanted
                      GLOBAL gsArray()   AS STRING 'holds all data, unique element for each thread number
                      
                      #PBFORMS BEGIN INCLUDES
                      %USEMACROS = 1
                      #INCLUDE ONCE "WIN32API.INC"
                      #PBFORMS END INCLUDES
                      
                      #PBFORMS BEGIN CONSTANTS
                      %TEXTBOX1    = 1002
                      %mnu_Run     =    0
                      #PBFORMS END CONSTANTS
                      #PBFORMS DECLARATIONS
                      
                      '====================================================================================
                      FUNCTION PBMAIN()
                        Form1 %HWND_DESKTOP
                      END FUNCTION
                      '====================================================================================
                      THREAD FUNCTION MyThread(BYVAL ThreadNum AS LONG) AS LONG
                       HelpMe(ThreadNum)
                       Inc32(gThreadsRun)
                      END FUNCTION
                      '====================================================================================
                      SUB HelpMe(ThreadNum AS LONG) 'THREADSAFE keywork needed if threads can access any element
                       gsArray(ThreadNum) = STRING$(ThreadNum,64+ThreadNum)
                      END SUB
                      '====================================================================================
                      THREAD FUNCTION StartThreadsHere(BYVAL hDlg AS LONG) AS LONG
                       LOCAL x AS LONG
                       gThreadsRun = 0
                       REDIM hThread(%ThreadsWanted) AS LONG
                       REDIM gsArray(%ThreadsWanted) AS STRING  'element 0 will be used as a title when all done
                        FOR x=1 TO UBOUND(hThread)
                        THREAD CREATE MyThread(x) TO hThread(x)
                        THREAD CLOSE  hThread(x)  TO hThread(x)
                       NEXT
                       DO
                        SLEEP 100
                       LOOP UNTIL gThreadsRun=%ThreadsWanted
                       gsArray(0) = USING$("Finished # threads at &  GetTickCount(#)",gThreadsRun,TIME$,GetTickCount)
                       CONTROL SET TEXT hDlg,%TextBox1,JOIN$(gsArray(),$CRLF)
                      END FUNCTION
                      '====================================================================================
                      CALLBACK FUNCTION Form1_CallBack()
                        SELECT CASE AS LONG CB.MSG
                          CASE %WM_COMMAND
                           SELECT CASE AS LONG CB.CTL
                              CASE %TEXTBOX1
                              CASE %MNU_Run
                              LOCAL hThread AS LONG
                              THREAD CREATE StartThreadsHere(CB.HNDL) TO hThread
                              THREAD CLOSE hThread TO hThread
                            END SELECT
                        END SELECT
                      END FUNCTION
                      '====================================================================================
                      
                      
                      'Create/show Form1
                      FUNCTION Form1(BYVAL hParent AS DWORD) AS LONG
                        LOCAL lRslt AS LONG
                      
                      #PBFORMS BEGIN DIALOG %IDD_DIALOG1->%IDR_MENU1->
                        LOCAL hDlg   AS DWORD
                        LOCAL hFont1 AS DWORD
                      
                        DIALOG NEW hParent, USING$("ThreadsWanted #",%ThreadsWanted), 363, 186, 372, 289, %WS_POPUP OR _
                          %WS_BORDER OR %WS_DLGFRAME OR %WS_CAPTION OR %WS_SYSMENU OR _
                          %WS_MINIMIZEBOX OR %WS_CLIPSIBLINGS OR %WS_VISIBLE OR %DS_MODALFRAME OR _
                          %DS_CENTER OR %DS_3DLOOK OR %DS_NOFAILCREATE OR %DS_SETFONT, _
                          %WS_EX_CONTROLPARENT OR %WS_EX_LEFT OR %WS_EX_LTRREADING OR _
                          %WS_EX_RIGHTSCROLLBAR, TO hDlg
                        CONTROL ADD TEXTBOX, hDlg, %TEXTBOX1, "", 6, 4, 360, 256, %WS_CHILD _
                          OR %WS_VISIBLE OR %WS_HSCROLL OR %WS_VSCROLL OR %ES_LEFT OR _
                          %ES_MULTILINE OR %ES_AUTOHSCROLL OR %ES_AUTOVSCROLL OR %ES_WANTRETURN, _
                          %WS_EX_CLIENTEDGE OR %WS_EX_LEFT OR %WS_EX_LTRREADING OR _
                          %WS_EX_RIGHTSCROLLBAR
                        FONT NEW "Courier New", 8, 0, %ANSI_CHARSET TO hFont1
                        CONTROL SET FONT hDlg, %TEXTBOX1, hFont1
                        AttachMENU1 hDlg
                      #PBFORMS END DIALOG
                        DIALOG SHOW MODAL hDlg, CALL Form1_CallBack TO lRslt
                      #PBFORMS BEGIN CLEANUP %IDD_DIALOG1
                        FONT END hFont1
                      #PBFORMS END CLEANUP
                        FUNCTION = lRslt
                      END FUNCTION
                      
                      FUNCTION AttachMENU1(BYVAL hDlg AS DWORD) AS DWORD
                      #PBFORMS BEGIN MENU %IDR_MENU1->%IDD_DIALOG1
                        LOCAL hMenu   AS DWORD
                        MENU NEW BAR TO hMenu
                        MENU ADD STRING, hMenu, "&Run", %mnu_Run, %MF_ENABLED
                        MENU ATTACH hMenu, hDlg
                      #PBFORMS END MENU
                        FUNCTION = hMenu
                      END FUNCTION
                      Click image for larger version  Name:	templatethread.png Views:	0 Size:	3.0 KB ID:	782871
                      How long is an idea? Write it down.

                      Comment


                      • #12
                        I have had mixed results with THREADSAFE . Critical Section is my preferred way to go.

                        Comment


                        • #13
                          I have had mixed results with THREADSAFE . Critical Section is my preferred way to go.
                          David,
                          I have never had a problem using THREADSAFE keyword.
                          Michael Mattias showed it should not be used in a recursive function.
                          If you can demonstrate an issue, please post it.
                          Last edited by Mike Doty; 20 Jul 2019, 10:26 PM.
                          How long is an idea? Write it down.

                          Comment


                          • #14
                            Another interesting use of a critical section is to synchronize related procedures. For instance, I use a type of embedded database where multiple threads need to add, find, and delete "records" from a common table. The code for each operation is very different and has a lot of steps. So, I split the operations into different functions and synchronize them with a common critical section so that only one function can operate on the table at a time. The individual functions complete very quickly so blocking isn't really an issue. I threw in some global counters since that's really the subject of this thread.

                            Code:
                            MACRO TIME32 = LONG
                            
                            TYPE NET_USERRECORD
                              userHash  AS LONG        ' Maybe a unique FNV1_32 user name hash (indexed)
                              passHash  AS STRING * 32 ' Maybed a salted SHA256 password hash
                              loginTime AS TIME32      ' Maybe a 32-bit date/second
                              '... other acct related data
                            END TYPE
                            
                            TYPE NET_COUNTERS
                              noRecord  AS QUAD
                              getRecord AS QUAD
                              addRecord AS QUAD
                              delRecord AS QUAD
                              '... more counter vars
                            END TYPE
                            
                            GLOBAL cs   AS CRITICAL_SECTION
                            GLOBAL user AS NET_USERRECORD
                            GLOBAL cnt  AS NET_COUNTERS
                            
                            FUNCTION AddRecord(BYVAL userHash AS LONG, BYREF rec AS NET_USERRECORD) AS LONG
                              EnterCriticalSection cs
                              '... do the add processing
                              Inc64(cnt.addRecord)
                              LeaveCriticalSection cs
                            END FUNCTION
                            
                            FUNCTION GetRecord(BYVAL userHash AS LONG, BYREF rec AS NET_USERRECORD) AS LONG
                              EnterCriticalSection cs
                              '... do the read processing
                              Inc64(cnt.getRecord)
                              LeaveCriticalSection cs
                            END FUNCTION
                            
                            FUNCTION DeleteRecord(BYVAL userHash AS LONG) AS LONG
                              EnterCriticalSection cs
                              '... do the delete processing
                              Inc64(cnt.delRecord)
                              LeaveCriticalSection cs
                            END FUNCTION
                            
                            FUNCTION PBMAIN () AS LONG
                              InitializeCriticalSection cs
                              '... do code stuff like load config, launch threads, etc.
                              DeleteCriticalSection cs
                            END FUNCTION

                            Comment


                            • #15
                              Did you compare the speed and results to this?

                              Code:
                              MACRO TIME32 = LONG
                              
                              TYPE NET_USERRECORD
                                userHash  AS LONG        ' Maybe a unique FNV1_32 user name hash (indexed)
                                passHash  AS STRING * 32 ' Maybed a salted SHA256 password hash
                                loginTime AS TIME32      ' Maybe a 32-bit date/second
                              END TYPE
                              
                              TYPE NET_COUNTERS
                                noRecord  AS QUAD
                                getRecord AS QUAD
                                addRecord AS QUAD
                                delRecord AS QUAD
                              END TYPE
                              
                              GLOBAL USER AS NET_USERRECORD
                              GLOBAL cnt  AS NET_COUNTERS
                              
                              FUNCTION AddRecord(BYVAL userHash AS LONG, BYREF rec AS NET_USERRECORD) THREADSAFE AS LONG
                              END FUNCTION
                              
                              FUNCTION GetRecord(BYVAL userHash AS LONG, BYREF rec AS NET_USERRECORD) THREADSAFE AS LONG
                              END FUNCTION
                              
                              FUNCTION DeleteRecord(BYVAL userHash AS LONG) THREADSAFE AS LONG
                              END FUNCTION
                              
                              FUNCTION PBMAIN () AS LONG
                              
                              END FUNCTION
                              How long is an idea? Write it down.

                              Comment


                              • #16
                                I've done some latency testing with THREADSAFE and critical sections (also in a thread with MCM) and found that the "winner" varies based on a number of parameters.

                                However, the example in this thread was how to block one function when a related function is in use. Lets say one thread is trying to read a record and a second thread is simultaneously trying to delete a record, or update a record (latter function not shown). Using the THREADSAFE descriptor will protect a specific function from being called by multiple threads at the same time but will not prevent multiple threads from calling related functions at the same time.

                                Comment


                                • #17
                                  I hear ya. I'm rehashing this subject.
                                  https://forum.powerbasic.com/forum/u...n-is-this-safe
                                  How long is an idea? Write it down.

                                  Comment


                                  • #18
                                    Mike, I can't remember how or when it tripped me up. I do remember Michael Mattias telling me not to use it - he thought there was a bug in it but that may have been a long time ago. It may have been the recursive thing you were talking about.

                                    At the moment I am rewriting one of my programs that used lots of threads to use a few as possible. We will see how it goes!

                                    Comment

                                    Working...
                                    X