Announcement

Collapse
No announcement yet.

Precision SLEEP

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

  • Precision SLEEP

    I don't use SLEEP very often but have run into a case where I need it, or something similar. Typically, SLEEP accuracy/resolution is about 10-15ms but what I need is something around 1-2ms. It seems that timeBeginPeriod and timeEndPeriod may be the answer to my dilemma. The target app has a single thread that is periodically signaled, which causes the thread to service up to 10K clients in five groups. The entire operation (loops) should take less than 250ms. I put together a piece of code to demonstrate this, and it seems to work fine. But, this is new territory for me so I'm wondering if I'm missing something. Maybe someone has done something similar and learned lessons that can be shared.

    Thanks.
    Code:
    #COMPILE EXE      ' Default is an EXE file
    
    #INCLUDE "win32api.inc"
    
    MACRO zStartTimer (qStart)
      QueryPerformanceCounter qStart
    END MACRO
    
    MACRO FUNCTION zStopTimer (qStart)
      MACROTEMP qStop, qFreq, dTime
      DIM qStop AS QUAD
      DIM qFreq AS QUAD
      DIM dTime AS DOUBLE
      QueryPerformanceCounter qStop
      QueryPerformanceFrequency qFreq
      dTime = (qStop - qStart) / qFreq
    END MACRO = dTime
    
    
    FUNCTION PBMAIN () AS LONG
      REGISTER grp AS LONG
      LOCAL grpCnt, scanGap, loopTime AS LONG
      LOCAL qTime, qTotal AS QUAD
      LOCAL dTime AS DOUBLE
    
    scanGap = 48
    grpCnt  = 5     ' In app, this will be 1 or 5
    
    timeBeginPeriod 1
    zStartTimer(qTotal)
    FOR grp = 1 TO grpCnt
      IF grp = grpCnt THEN  ' Last or only group to be processed.
        GOSUB procClients
      ELSE                  ' First groups to be processed or skip if single group (track time).
        zStartTimer(qTime)
        GOSUB ProcClients
        loopTime = zStopTimer(qTime) * 1000
        IF loopTime < scanGap THEN
          SLEEP scangap - loopTime
        END IF
      END IF
    NEXT grp
    dTime = zStopTimer(qTotal)
    timeEndPeriod 1
    PRINT "Total loop processing time (ms) =" dTime * 1000
    GOTO ExitMain
    
    ProcClients:
    '... do a bunch of processing and packet transmission
    SLEEP 50  '<--- Simulate expected worse-case processing time
    RETURN
    
    Exitmain:
      WAITKEY$
    
    END FUNCTION

  • #2
    SLEEP accuracy cannot be relied on.


    2 alternatives come to mind.
    You could use timeSetEvent to create a timer which triggers in a millisecond and you could use that to check every millisecond to see if there is something to do.


    Or, the better way, use CreateEvent to create a synchronisation object.
    Your program then uses WaitForSingleObject to wait for the data to become available.

    The part of your program that's waiting will WaitForSingleObject
    The part of your program that is creating the files to be processed will use SetEvent() when the files are ready.

    The SetEvent immediately releases the WaitForSingleObject that is waiting for that event so there is no need to waste 1 ms waiting for the data.

    Comment


    • #3
      I am with Mr. Dixon, if your "precision sleep" is really your current solution to a "wait for something to happen" scenario.

      Search the word 'event' in the thread title in source code forum for at least one example of using user-defined Windows' events to achieve synchronization.

      MCM

      Michael Mattias
      Tal Systems Inc. (retired)
      Racine WI USA
      [email protected]
      http://www.talsystems.com

      Comment


      • #4
        Originally posted by Michael Mattias View Post
        I am with Mr. Dixon, if your "precision sleep" is really your current solution to a "wait for something to happen" scenario.

        Search the word 'event' in the thread title in source code forum for at least one example of using user-defined Windows' events to achieve synchronization.

        MCM
        Well, I am waiting for something to happen - I'm waiting for time to elapse (around 50ms). And accuracy of 1-2ms is fine. What worries me is if this "loop" gets continuously busy it could affect other parts of the app, other apps, or the entire OS. From the MSDN web site:

        This function affects a global Windows setting. Windows uses the lowest value (that is, highest resolution) requested by any process. Setting a higher resolution can improve the accuracy of time-out intervals in wait functions. However, it can also reduce overall system performance, because the thread scheduler switches tasks more often. High resolutions can also prevent the CPU power management system from entering power-saving modes.
        In fact, this app uses a number of threads that are either signaled with a Windows event or a thread message. I'm looking for a way to throttle activity within this thread once it's been turned loose. I have a couple of other things to try.

        Comment


        • #5
          Originally posted by Jerry Wilson View Post
          [CODE]
          ...
          loopTime = zStopTimer(qTime) * 1000
          IF loopTime < scanGap THEN
          SLEEP scangap - loopTime
          END IF
          ...
          What is the purpose of the above Sleep? I don't see how it is doing anything other than slowing down your application.

          Comment


          • #6
            If the thread is busy then just let it do its thing.
            Windows will schedule other threads as it sees fit.
            Forcing a busy thread to sleep is just a waste of resources.

            Comment


            • #7
              Originally posted by Jerry Wilson View Post

              Well, I am waiting for something to happen - I'm waiting for time to elapse (around 50ms). And accuracy of 1-2ms is fine. What worries me is if this "loop" gets continuously busy it could affect other parts of the app, other apps, or the entire OS. From the MSDN web site:



              In fact, this app uses a number of threads that are either signaled with a Windows event or a thread message. I'm looking for a way to throttle activity within this thread once it's been turned loose. I have a couple of other things to try.
              I've cautioned against misuse of timeBeginPeriod 1 or it's equivalent on several forum threads. You frequently see it misused.
              This is not an appropriate use .

              There is no need to "throttle activity within a thread" to prevent it "affecting other parts of the app". Your thread will still yield to other threads once it's thread quantum has elapsed.


              .

              Comment


              • #8
                If you are waiting for time to elapse, this humble city boy from the South Side of Milwaukee suggests the deviously-named ( wait for it...) timer object.

                The waitable timer might be what you want; search source code forum for "waitable" in the thread title because i know that is how I named that demo.
                Michael Mattias
                Tal Systems Inc. (retired)
                Racine WI USA
                [email protected]
                http://www.talsystems.com

                Comment


                • #9
                  Jerry,
                  Most generally it works pretty good staying within your parameters but occasionally it produces out of parameter results:
                  Click image for larger version  Name:	New Bitmap Image.jpg Views:	0 Size:	21.8 KB ID:	799373

                  Your loopTime can be 51, 55, 51, 51
                  Like Paul said...
                  SLEEP accuracy cannot be relied on.

                  Comment


                  • #10
                    Still waiting to find out what the SLEEP scangap - loopTime is intended to achieve

                    That said:

                    scangap = 48
                    The loop has a SLEEP 50 so looptime will always be at least 50.
                    Therefore IF loopTime < scanGap will always be false and the SLEEP scangap - loopTime will never trigger

                    Any timing variations will be the result of:
                    a. Granularity of the SLEEP 50 and
                    b. Thread/process switching by Windows.

                    timeBeginPeriod 1 will tend to decrease over all time based on (a) and increase overall time based on (b).

                    Comment


                    • #11
                      SLEEP 50 is merely representing the worst case scan time scenario which he says when totaled will equal 250ms +/- 2ms.

                      SLEEP scangap - loopTime is to fill up the remainder of the time to make his total scan times equal 250ms +/- 2ms.

                      Therefore IF loopTime < scanGap will always be false and the SLEEP scangap - loopTime will never trigger
                      Correct. I suspect he did that for a reason.

                      Note:
                      I would probably put this at the end of the group scans rather than for each scan to pad the whole process.
                      Code:
                      IF GroupTime < 250 THEN 
                          SLEEP 250 - GroupTime 
                      END IF

                      Comment


                      • #12
                        Modified for PBWIN10.4
                        Like this:
                        Code:
                        #COMPILE EXE
                        #DIM ALL
                        
                        #INCLUDE "win32api.inc"
                        
                        GLOBAL gsX         AS STRING
                        GLOBAL gsY         AS STRING
                        GLOBAL gsZ         AS STRING
                        
                        FUNCTION PBMAIN () AS LONG
                            REGISTER grp AS LONG
                        
                            LOCAL qFreq  AS QUAD
                            LOCAL qStart AS QUAD
                            LOCAL qStop  AS QUAD
                            LOCAL qGroupTimeStart  AS QUAD
                            LOCAL qGroupTimeStop   AS QUAD
                            LOCAL qGroupTime       AS QUAD
                            LOCAL grpCnt           AS LONG
                            LOCAL XD     AS LONG
                            LOCAL lsA    AS STRING
                        
                            LOCAL Time AS TIMECAPS
                        
                            TimeGetDevCaps ( Time, SIZEOF(Time) )
                            timeBeginPeriod (Time.wPeriodMin)
                            SLEEP 16
                            gsX = STRING$(80, "X")   'create the test string
                            gsY = STRING$(80, "Y")   'create the test string
                            gsZ = STRING$(80, "Z")   'create the test string
                        
                            QueryPerformanceFrequency qFreq
                        
                            grpCnt  = 5     ' In app, this will be 1 or 5
                        
                            QueryPerformanceCounter qStart
                            QueryPerformanceCounter qStart  '<< pump the process again
                            QueryPerformanceCounter qStart  '<< pump the process again
                        
                            QueryPerformanceCounter qGroupTimeStart
                            FOR grp = 1 TO grpCnt
                                GOSUB ProcClients
                            NEXT grp
                            QueryPerformanceCounter qGroupTimeStop
                        
                            qGroupTime = INT(1000 *((qGroupTimeStop - qGroupTimeStart)/qFreq))
                            IF grpCnt > 1 THEN
                                IF qGroupTime < 250 THEN
                                    SLEEP 250 - qGroupTime
                                END IF
                            ELSE
                                IF qGroupTime < 50 THEN
                                    SLEEP 50 - qGroupTime
                                END IF
                            END IF
                        
                            QueryPerformanceCounter qStop
                        
                            ? "Grouptime=" & LTRIM$(STR$(qGroupTime)) & " and Total loop processing time (ms) =" & STR$(1000 * ((qStop - qStart)/qFreq))
                            GOTO ExitMain
                        
                            ProcClients:
                              '... do a bunch of processing and packet transmission
                              'SLEEP 48  '<--- Simulate expected worse-case processing time
                              lsA = ""
                              FOR XD = 1 TO 226400   'substitution for sleep 48
                                  lsA = BUILD$(gsX, gsY, gsZ, $CRLF)
                              NEXT XD
                        
                            RETURN
                        
                            Exitmain:
                              '? "App is ending" 'WAITKEY$
                        
                        END FUNCTION
                        Ofcourse if the grouptime exceeds 250ms then you get what you get. Hopefully your ProcClients use no sleeps. One SLEEP is easier to deal with than 5 or more in a row. Since your target was 250ms for 5 loops I dropped the sleep time to 48 but found that was unsatisfactory so I rolled my own sleep replacement. That provides some extra breathing room for scan time variations.

                        Comment


                        • #13
                          I can understand "The entire operation (loops) should take less than 250ms."

                          I can't grasp the convoluted reasoning that derived a requirement to take exactly 250 ms for the operation if it is less than 250 ms (+/-2ms)

                          Comment


                          • #14
                            It is worth noting that WaitForSingleObject is linked to the system clock so polls at 15.6ms. So, even if a signal has been issued it may take that long before WaitForSingleObject spots it. On the other hand, if we use TimeBeginPeriod/TimeEndPeriod around WaitForSingleObject then we get a 1ms polling. That will work on Windows 10 but older platforms do not see TimeBeginPeriod 'bite' until two ticks of the System Clock, so we need to employ it earlier.

                            An awful lot of Windows APIs are linked to the System Clock but MSDN neglects to tell us. They rattle on about millisecond this and millisecond that when in fact thay have a granularity of 15.6ms.

                            Comment


                            • #15
                              I regenerated #12. I found that the sleep produced too many variations so I rolled my own sleep replacement routine. Also I put in support for grpCnt = 1.
                              grpCnt = 5 ' In app, this will be 1 or 5
                              Likely the substitution for sleep 48 will have to be revised to do an equivalent run on faster machines. It is only there to simulate some processes.
                              The CPU usage is so much better now with the sleep replacement routine.

                              Stuart,
                              I can't grasp the convoluted reasoning that derived a requirement to take exactly 250 ms for the operation if it is less than 250 ms (+/-2ms)
                              Just a parameter. grpCnt = 1 is different. It needs to be 50 +/- 2ms if I understand him correctly.

                              Comment


                              • #16
                                I have just run some code which was written in 2014 and is not behaving as it did then. When it comes to timers Windows 10 saw some major changes. For example, on Windows 10 if I had a mind to I could change the timer interval to 10.345ms. That cannot be done pre Windows 10. I don't have access to pre Windows 10 so cannot prove all of what I wrote in my last post.

                                Added: On Windows 10 WaitForSingleObject is polling at 1ms. I am sure it used to poll at 15.6ms but I cannot prove it now.

                                Comment


                                • #17
                                  David,
                                  What do your notes say?

                                  Comment


                                  • #18
                                    What notes, Jim?

                                    Comment


                                    • #19
                                      It is worth noting that WaitForSingleObject is linked to the system clock so polls at 15.6ms. So, even if a signal has been issued it may take that long before WaitForSingleObject spots it.
                                      Added: On Windows 10 WaitForSingleObject is polling at 1ms. I am sure it used to poll at 15.6ms but I cannot prove it now.
                                      This is not correct.
                                      WaitForSingleObject returns in a microsecond or 2 and is not polled at the system clock rate.

                                      Comment


                                      • #20
                                        If WaitForSingleObject was polled every few milliseconds then it would be useless
                                        The program below uses 2 WaitForSingleObjects to allow 2 threads to run alternately.
                                        On my WinXP computer it counts 170,000 in 1 second. That's 340,000 WaitForSingleObjects plus overheads which is less than 3us for each WaitForSingleObject

                                        Code:
                                        'PBCC6 program
                                        #COMPILE EXE
                                        #DIM ALL
                                        
                                        
                                        #INCLUDE "win32api.inc"
                                        
                                        'test how quickly WaitForSingleObject responds.
                                        'Show that it is not polled by the OS at the system clock rate of 16ms or whatever it has been set to.
                                        
                                        GLOBAL ghThread1, ghThread2, ghEvent1, ghEvent2, gQuitAll, gCount1, gCount2 AS LONG
                                        
                                        FUNCTION PBMAIN () AS LONG
                                        
                                        LOCAL t AS DOUBLE
                                        
                                        'create 2 events, initailly signalled and needing manual reset
                                        'Using a manual reset makes the program easier to follow for those unfamiliar with Events but the program would run faster if the events were created with automatic reset
                                        ghEvent1 = CreateEvent(BYVAL 0, BYVAL 1 , BYVAL 1 ,BYVAL 0)
                                        ghEvent2 = CreateEvent(BYVAL 0, BYVAL 1 , BYVAL 1 ,BYVAL 0)
                                        
                                        'create 2 threads which are each controlled by one of the events
                                        THREAD CREATE Test1(1) TO ghThread1
                                        THREAD CREATE Test2(1) TO ghThread2
                                        
                                        'Wait about 1 second for the threads to loop as many times as possible.
                                        'Only 1 thread runs at a time. The first thread signals the second thread to continue and then the first thread waits.
                                        'The second thread then signals the first thread to continue and the second thread then waits.
                                        
                                        t = TIMER
                                        SLEEP 1000
                                        
                                        gQuitAll = 1    'cause the threads to exit
                                        
                                        t = TIMER-t     'record the actual time taken
                                        
                                        PRINT "Loops = ";gCount1, gCount2
                                        PRINT "Average time =";FORMAT$(t/(gCount1 + gCount2)*1000000,"###,###.0");"usec/count"
                                        WAITKEY$
                                        
                                        END FUNCTION
                                        
                                        
                                        
                                        THREAD FUNCTION Test1(BYVAL x AS LONG) AS LONG
                                        LOCAL RetVal AS LONG
                                        
                                        DO
                                            RetVal = WaitForSingleObject(ghEvent1,%infinite)    'wait for the event to be signalled by the other thread
                                            RetVal = ResetEvent(ghEvent1)                       'manually reset the event
                                            RetVal = SetEvent(ghEvent2)                         'set the event the other thread is waiting on
                                        
                                            INCR gCount1
                                        
                                        LOOP UNTIL gQuitAll
                                        
                                        END FUNCTION
                                        
                                        
                                        
                                        
                                        THREAD FUNCTION Test2(BYVAL x AS LONG) AS LONG
                                        LOCAL RetVal AS LONG
                                        
                                        DO
                                            RetVal = WaitForSingleObject(ghEvent2,%infinite)   'wait for the event to be signalled by the other thread
                                            RetVal = ResetEvent(ghEvent2)                      'manually reset the event
                                            RetVal = SetEvent(ghEvent1)                        'set the event the other thread is waiting on
                                        
                                            INCR gCount2
                                        
                                        LOOP UNTIL gQuitAll
                                        
                                        END FUNCTION

                                        Comment

                                        Working...
                                        X