github.com/geniusesgroup/libgo@v0.0.0-20220713101832-828057a9d3d4/timer/timing-heap.go (about) 1 /* For license and copyright information please see LEGAL file in repository */ 2 3 package timer 4 5 import ( 6 "sync" 7 "sync/atomic" 8 "unsafe" 9 10 "../cpu" 11 "../race" 12 "../time/monotonic" 13 ) 14 15 // TimingHeap ... 16 // Active timers live in the timers field as heap structure. 17 // Inactive timers live there too temporarily, until they are removed. 18 // Due to atomic function need memory alignment, Don't change fields order. 19 // 20 // https://github.com/search?l=go&q=timer&type=Repositories 21 // https://github.com/RussellLuo/timingwheel/blob/master/delayqueue/delayqueue.go 22 type TimingHeap struct { 23 coreID uint64 // CPU core number this heap run on it 24 25 // The when field of the first entry on the timer heap. 26 // This is updated using atomic functions. 27 // This is 0 if the timer heap is empty. 28 timer0When int64 29 30 // The earliest known when field of a timer with 31 // timerModifiedEarlier status. Because the timer may have been 32 // modified again, there need not be any timer with this value. 33 // This is updated using atomic functions. 34 // This is 0 if there are no timerModifiedEarlier timers. 35 timerModifiedEarliest int64 36 37 // Number of timers in P's heap. 38 // Modified using atomic instructions. 39 numTimers int32 40 41 // Number of timerDeleted timers in P's heap. 42 // Modified using atomic instructions. 43 deletedTimers int32 44 45 // Race context used while executing timer functions. 46 timerRaceCtx uintptr 47 48 // Lock for timers. We normally access the timers while running 49 // on this P, but the scheduler can also do it from a different P. 50 timersLock sync.Mutex 51 // Must hold timersLock to access. 52 // https://en.wikipedia.org/wiki/Heap_(data_structure)#Comparison_of_theoretic_bounds_for_variants 53 // Balancing a heap is done by th.siftUp or th.siftDown methods 54 timers []timerBucket 55 } 56 57 type timerBucket struct { 58 timer *Timer 59 // Two reason to have timer when here: 60 // - hot cache to prevent dereference timer to get when field 61 // - It can be difference with timer when filed in timerModifiedXX status. 62 when int64 63 } 64 65 func (th *TimingHeap) Init() { 66 // TODO::: let application flow choose timers init cap or force it? 67 // th.timers = make([]timerBucket, 1024) 68 th.coreID = cpu.ActiveCoreID() 69 } 70 71 // addTimer adds t to the timers queue. 72 // The caller must have locked the th.timersLock 73 func (th *TimingHeap) addTimer(t *Timer) { 74 th.timersLock.Lock() 75 76 th.cleanTimers() 77 78 var timerWhen = t.when 79 t.timers = th 80 var i = len(th.timers) 81 th.timers = append(th.timers, timerBucket{t, timerWhen}) 82 83 th.siftUpTimer(i) 84 if t == th.timers[0].timer { 85 atomic.StoreInt64(&th.timer0When, timerWhen) 86 } 87 atomic.AddInt32(&th.numTimers, 1) 88 89 th.timersLock.Unlock() 90 } 91 92 // deleteTimer removes timer i from the timers heap. 93 // It returns the smallest changed index in th.timers 94 // The caller must have locked the th.timersLock 95 func (th *TimingHeap) deleteTimer(i int) int { 96 th.timers[i].timer.timers = nil 97 98 var last = len(th.timers) - 1 99 if i != last { 100 th.timers[i] = th.timers[last] 101 } 102 th.timers[last].timer = nil 103 th.timers = th.timers[:last] 104 105 var smallestChanged = i 106 if i != last { 107 // Moving to i may have moved the last timer to a new parent, 108 // so sift up to preserve the heap guarantee. 109 smallestChanged = th.siftUpTimer(i) 110 th.siftDownTimer(i) 111 } 112 if i == 0 { 113 th.updateTimer0When() 114 } 115 atomic.AddInt32(&th.numTimers, -1) 116 return smallestChanged 117 } 118 119 // deleteTimer0 removes timer 0 from the timers heap. 120 // It reports whether it saw no problems due to races. 121 // The caller must have locked the th.timersLock 122 func (th *TimingHeap) deleteTimer0() { 123 th.timers[0].timer.timers = nil 124 125 var last = len(th.timers) - 1 126 if last > 0 { 127 th.timers[0] = th.timers[last] 128 } 129 th.timers[last].timer = nil 130 th.timers = th.timers[:last] 131 if last > 0 { 132 th.siftDownTimer(0) 133 } 134 th.updateTimer0When() 135 atomic.AddInt32(&th.numTimers, -1) 136 } 137 138 // cleanTimers cleans up the head of the timer queue. This speeds up 139 // programs that create and delete timers; leaving them in the heap 140 // slows down addTimer. Reports whether no timer problems were found. 141 // The caller must have locked the th.timersLock 142 func (th *TimingHeap) cleanTimers() { 143 if len(th.timers) == 0 { 144 return 145 } 146 147 for { 148 // This loop can theoretically run for a while, and because 149 // it is holding timersLock it cannot be preempted. 150 // If someone is trying to preempt us, just return. 151 // We can clean the timers later. 152 // if gp.preemptStop { 153 // return 154 // } 155 156 var timerBucket = th.timers[0] 157 var timer = timerBucket.timer 158 var status = timer.status.Load() 159 switch status { 160 case status_Deleted: 161 if !timer.status.CompareAndSwap(status, status_Removing) { 162 continue 163 } 164 th.deleteTimer0() 165 if !timer.status.CompareAndSwap(status_Removing, status_Removed) { 166 badTimer() 167 } 168 atomic.AddInt32(&th.deletedTimers, -1) 169 case status_ModifiedEarlier, status_ModifiedLater: 170 if !timer.status.CompareAndSwap(status, status_Moving) { 171 continue 172 } 173 // Now we can change the when field of timerBucket. 174 th.timers[0].when = timer.when 175 // Move timer to the right position. 176 th.deleteTimer0() 177 th.addTimer(timer) 178 if !timer.status.CompareAndSwap(status_Moving, status_Waiting) { 179 badTimer() 180 } 181 default: 182 // Head of timers does not need adjustment. 183 return 184 } 185 } 186 } 187 188 // moveTimers moves a slice of timers to the timers heap. 189 // The slice has been taken from a different Timers. 190 // This is currently called when the world is stopped, but the caller 191 // is expected to have locked the th.timersLock 192 func (th *TimingHeap) moveTimers(timers []timerBucket) { 193 for _, timerBucket := range timers { 194 var timer = timerBucket.timer 195 loop: 196 for { 197 var status = timer.status.Load() 198 switch status { 199 case status_Waiting, status_ModifiedEarlier, status_ModifiedLater: 200 if !timer.status.CompareAndSwap(status, status_Moving) { 201 continue 202 } 203 timer.timers = nil 204 th.addTimer(timer) 205 if !timer.status.CompareAndSwap(status_Moving, status_Waiting) { 206 badTimer() 207 } 208 break loop 209 case status_Deleted: 210 if !timer.status.CompareAndSwap(status, status_Removed) { 211 continue 212 } 213 timer.timers = nil 214 // We no longer need this timer in the heap. 215 break loop 216 case status_Modifying: 217 // Loop until the modification is complete. 218 osyield() 219 case status_Unset, status_Removed: 220 // We should not see these status values in a timers heap. 221 badTimer() 222 case status_Running, status_Removing, status_Moving: 223 // Some other P thinks it owns this timer, 224 // which should not happen. 225 badTimer() 226 default: 227 badTimer() 228 } 229 } 230 } 231 } 232 233 // adjustTimers looks through the timers for any timers that have been modified to run earlier, 234 // and puts them in the correct place in the heap. While looking for those timers, 235 // it also moves timers that have been modified to run later, and removes deleted timers. 236 // The caller must have locked the th.timersLock 237 func (th *TimingHeap) adjustTimers(now int64) { 238 // If we haven't yet reached the time of the first status_ModifiedEarlier 239 // timer, don't do anything. This speeds up programs that adjust 240 // a lot of timers back and forth if the timers rarely expire. 241 // We'll postpone looking through all the adjusted timers until 242 // one would actually expire. 243 var first = atomic.LoadInt64(&th.timerModifiedEarliest) 244 if first == 0 || int64(first) > now { 245 if verifyTimers { 246 th.verifyTimerHeap() 247 } 248 return 249 } 250 251 // We are going to clear all status_ModifiedEarlier timers. 252 atomic.StoreInt64(&th.timerModifiedEarliest, 0) 253 254 var moved []*Timer 255 var timers = th.timers 256 var timersLen = len(timers) 257 for i := 0; i < timersLen; i++ { 258 var timerBucket = timers[i] 259 var timer = timerBucket.timer 260 var status = timer.status.Load() 261 switch status { 262 case status_Deleted: 263 if timer.status.CompareAndSwap(status, status_Removing) { 264 var changed = th.deleteTimer(i) 265 if !timer.status.CompareAndSwap(status_Removing, status_Removed) { 266 badTimer() 267 } 268 atomic.AddInt32(&th.deletedTimers, -1) 269 // Go back to the earliest changed heap entry. 270 // "- 1" because the loop will add 1. 271 i = changed - 1 272 } 273 case status_ModifiedEarlier, status_ModifiedLater: 274 if timer.status.CompareAndSwap(status, status_Moving) { 275 // Take t off the heap, and hold onto it. 276 // We don't add it back yet because the 277 // heap manipulation could cause our 278 // loop to skip some other timer. 279 var changed = th.deleteTimer(i) 280 moved = append(moved, timer) 281 // Go back to the earliest changed heap entry. 282 // "- 1" because the loop will add 1. 283 i = changed - 1 284 } 285 case status_Unset, status_Running, status_Removing, status_Removed, status_Moving: 286 badTimer() 287 case status_Waiting: 288 // OK, nothing to do. 289 case status_Modifying: 290 // Check again after modification is complete. 291 osyield() 292 i-- 293 default: 294 badTimer() 295 } 296 } 297 298 if len(moved) > 0 { 299 th.addAdjustedTimers(moved) 300 } 301 302 if verifyTimers { 303 th.verifyTimerHeap() 304 } 305 } 306 307 // addAdjustedTimers adds any timers we adjusted in th.adjustTimers 308 // back to the timer heap. 309 func (th *TimingHeap) addAdjustedTimers(moved []*Timer) { 310 for _, t := range moved { 311 th.addTimer(t) 312 if !t.status.CompareAndSwap(status_Moving, status_Waiting) { 313 badTimer() 314 } 315 } 316 } 317 318 // runTimer examines the first timer in timers. If it is ready based on now, 319 // it runs the timer and removes or updates it. 320 // Returns 0 if it ran a timer, -1 if there are no more timers, or the time 321 // when the first timer should run. 322 // The caller must have locked the th.timersLock 323 // If a timer is run, this will temporarily unlock the timers. 324 func (th *TimingHeap) runTimer(now int64) int64 { 325 for { 326 var timerBucket = th.timers[0] 327 var timer = timerBucket.timer 328 var status = timer.status.Load() 329 switch status { 330 case status_Waiting: 331 if timer.when > now { 332 // Not ready to run. 333 return timer.when 334 } 335 336 if !timer.status.CompareAndSwap(status, status_Running) { 337 continue 338 } 339 // Note that runOneTimer may temporarily unlock th.timersLock 340 th.runOneTimer(timer, now) 341 return 0 342 343 case status_Deleted: 344 if !timer.status.CompareAndSwap(status, status_Removing) { 345 continue 346 } 347 th.deleteTimer0() 348 if !timer.status.CompareAndSwap(status_Removing, status_Removed) { 349 badTimer() 350 } 351 atomic.AddInt32(&th.deletedTimers, -1) 352 if len(th.timers) == 0 { 353 return -1 354 } 355 356 case status_ModifiedEarlier, status_ModifiedLater: 357 if !timer.status.CompareAndSwap(status, status_Moving) { 358 continue 359 } 360 th.deleteTimer0() 361 th.addTimer(timer) 362 if !timer.status.CompareAndSwap(status_Moving, status_Waiting) { 363 badTimer() 364 } 365 366 case status_Modifying: 367 // Wait for modification to complete. 368 osyield() 369 case status_Unset, status_Removed: 370 // Should not see a new or inactive timer on the heap. 371 badTimer() 372 case status_Running, status_Removing, status_Moving: 373 // These should only be set when timers are locked, 374 // and we didn't do it. 375 badTimer() 376 default: 377 badTimer() 378 } 379 } 380 } 381 382 // runOneTimer runs a single timer. 383 // The caller must have locked the th.timersLock 384 // This will temporarily unlock the timers while running the timer function. 385 func (th *TimingHeap) runOneTimer(t *Timer, now int64) { 386 if race.DetectorEnabled { 387 ppcur := getg().m.p.ptr() 388 if ppcur.timerRaceCtx == 0 { 389 ppcur.timerRaceCtx = racegostart(abi.FuncPCABIInternal(runtimer) + sys.PCQuantum) 390 } 391 race.Acquirectx(ppcur.timerRaceCtx, unsafe.Pointer(t)) 392 } 393 394 if t.period > 0 && t.periodNumber != 0 { 395 // Leave in heap but adjust next time to fire. 396 var delta = t.when - now 397 t.when += t.period * (1 + -delta/t.period) 398 if t.when < 0 { // check for overflow. 399 t.when = maxWhen 400 } 401 th.siftDownTimer(0) 402 if !t.status.CompareAndSwap(status_Running, status_Waiting) { 403 badTimer() 404 } 405 th.updateTimer0When() 406 if t.periodNumber > 0 { 407 t.periodNumber-- 408 } 409 } else { 410 // Remove from heap. 411 th.deleteTimer0() 412 if !t.status.CompareAndSwap(status_Running, status_Unset) { 413 badTimer() 414 } 415 } 416 417 if race.DetectorEnabled { 418 // Temporarily use the current P's racectx for g0. 419 var gp = getg() 420 if gp.racectx != 0 { 421 panic("timer - runOneTimer: unexpected racectx") 422 } 423 gp.racectx = gp.m.p.ptr().timerRaceCtx 424 } 425 426 var callback = t.callback 427 var arg = t.arg 428 th.timersLock.Unlock() 429 callback(arg) 430 th.timersLock.Lock() 431 432 if race.DetectorEnabled { 433 var gp = getg() 434 gp.racectx = 0 435 } 436 } 437 438 // clearDeletedTimers removes all deleted timers from the timers heap. 439 // This is used to avoid clogging up the heap if the program 440 // starts a lot of long-running timers and then stops them. 441 // For example, this can happen via context.WithTimeout. 442 // 443 // This is the only function that walks through the entire timer heap, 444 // other than moveTimers which only runs when the world is stopped. 445 // 446 // The caller must have locked the th.timersLock 447 func (th *TimingHeap) clearDeletedTimers() { 448 // We are going to clear all status_ModifiedEarlier timers. 449 // Do this now in case new ones show up while we are looping. 450 atomic.StoreInt64(&th.timerModifiedEarliest, 0) 451 452 var cdel = int32(0) 453 var to = 0 454 var changedHeap = false 455 var timers = th.timers 456 var timersLen = len(timers) 457 nextTimer: 458 for i := 0; i < timersLen; i++ { 459 var timerBucket = timers[i] 460 var timer = timerBucket.timer 461 for { 462 var status = timer.status.Load() 463 switch status { 464 case status_Waiting: 465 if changedHeap { 466 timers[to] = timerBucket 467 th.siftUpTimer(to) 468 } 469 to++ 470 continue nextTimer 471 case status_ModifiedEarlier, status_ModifiedLater: 472 if timer.status.CompareAndSwap(status, status_Moving) { 473 timerBucket.when = timer.when 474 timers[to] = timerBucket 475 th.siftUpTimer(to) 476 to++ 477 changedHeap = true 478 if !timer.status.CompareAndSwap(status_Moving, status_Waiting) { 479 badTimer() 480 } 481 continue nextTimer 482 } 483 case status_Deleted: 484 if timer.status.CompareAndSwap(status, status_Removing) { 485 timer.timers = nil 486 cdel++ 487 if !timer.status.CompareAndSwap(status_Removing, status_Removed) { 488 badTimer() 489 } 490 changedHeap = true 491 continue nextTimer 492 } 493 case status_Modifying: 494 // Loop until modification complete. 495 osyield() 496 case status_Unset, status_Removed: 497 // We should not see these status values in a timer heap. 498 badTimer() 499 case status_Running, status_Removing, status_Moving: 500 // Some other P thinks it owns this timer, 501 // which should not happen. 502 badTimer() 503 default: 504 badTimer() 505 } 506 } 507 } 508 509 // Set remaining slots in timers slice to nil, 510 // so that the timer values can be garbage collected. 511 for i := to; i < len(timers); i++ { 512 timers[i].timer = nil 513 } 514 515 atomic.AddInt32(&th.deletedTimers, -cdel) 516 atomic.AddInt32(&th.numTimers, -cdel) 517 518 timers = timers[:to] 519 th.timers = timers 520 th.updateTimer0When() 521 522 if verifyTimers { 523 th.verifyTimerHeap() 524 } 525 } 526 527 // verifyTimerHeap verifies that the timer heap is in a valid state. 528 // This is only for debugging, and is only called if verifyTimers is true. 529 // The caller must have locked the th.timersLock 530 func (th *TimingHeap) verifyTimerHeap() { 531 var timers = th.timers 532 var timersLen = len(timers) 533 // First timer has no parent, so i must be start from 1. 534 for i := 1; i < timersLen; i++ { 535 var timerBucket = th.timers[0] 536 var timer = timerBucket.timer 537 538 // The heap is 4-ary. See siftUpTimer and siftDownTimer. 539 var p = (i - 1) / 4 540 if timer.when < timers[p].when { 541 print("timer: bad timer heap at ", i, ": ", p, ": ", th.timers[p].when, ", ", i, ": ", timer.when, "\n") 542 panic("timer: bad timer heap") 543 } 544 } 545 var numTimers = int(atomic.LoadInt32(&th.numTimers)) 546 if timersLen != numTimers { 547 println("timer: heap len", len(th.timers), "!= numTimers", numTimers) 548 panic("timer: bad timer heap len") 549 } 550 } 551 552 // updateTimer0When sets the timer0When field by check first timer in queue. 553 // The caller must have locked the th.timersLock 554 func (th *TimingHeap) updateTimer0When() { 555 if len(th.timers) == 0 { 556 atomic.StoreInt64(&th.timer0When, 0) 557 } else { 558 atomic.StoreInt64(&th.timer0When, th.timers[0].when) 559 } 560 } 561 562 // updateTimerModifiedEarliest updates the th.timerModifiedEarliest value. 563 // The timers will not be locked. 564 func (th *TimingHeap) updateTimerModifiedEarliest(nextWhen int64) { 565 for { 566 var old = atomic.LoadInt64(&th.timerModifiedEarliest) 567 if old != 0 && int64(old) < nextWhen { 568 return 569 } 570 if atomic.CompareAndSwapInt64(&th.timerModifiedEarliest, old, nextWhen) { 571 return 572 } 573 } 574 } 575 576 // sleepUntil returns the time when the next timer should fire. 577 func (th *TimingHeap) sleepUntil() (until int64) { 578 until = int64(maxWhen) 579 580 var timer0When = atomic.LoadInt64(&th.timer0When) 581 if timer0When != 0 && timer0When < until { 582 until = timer0When 583 } 584 585 timer0When = atomic.LoadInt64(&th.timerModifiedEarliest) 586 if timer0When != 0 && timer0When < until { 587 until = timer0When 588 } 589 return 590 } 591 592 // noBarrierWakeTime looks at timers and returns the time when we should wake up. 593 // This function is invoked when dropping a Timers, and must run without any write barriers. 594 // Unlike th.sleepUntil(), It returns 0 if there are no timers. 595 func (th *TimingHeap) noBarrierWakeTime() (until int64) { 596 until = atomic.LoadInt64(&th.timer0When) 597 var nextAdj = atomic.LoadInt64(&th.timerModifiedEarliest) 598 if until == 0 || (nextAdj != 0 && nextAdj < until) { 599 until = nextAdj 600 } 601 return 602 } 603 604 // checkTimers runs any timers for the P that are ready. 605 // If now is not 0 it is the current time. 606 // It returns the passed time or the current time if now was passed as 0. 607 // and the time when the next timer should run or 0 if there is no next timer, 608 // and reports whether it ran any timers. 609 // If the time when the next timer should run is not 0, 610 // it is always larger than the returned time. 611 // We pass now in and out to avoid extra calls of monotonic.RuntimeNano(). 612 func (th *TimingHeap) checkTimers(now int64) (rnow, pollUntil int64, ran bool) { 613 // If it's not yet time for the first timer, or the first adjusted 614 // timer, then there is nothing to do. 615 var next = th.noBarrierWakeTime() 616 if next == 0 { 617 // No timers to run or adjust. 618 return now, 0, false 619 } 620 621 if now == 0 { 622 now = monotonic.RuntimeNano() 623 } 624 if now < next { 625 // Next timer is not ready to run, but keep going 626 // if we would clear deleted timers. 627 // This corresponds to the condition below where 628 // we decide whether to call clearDeletedTimers. 629 if atomic.LoadInt32(&th.deletedTimers) <= atomic.LoadInt32(&th.numTimers)/4 { 630 return now, next, false 631 } 632 } 633 634 th.timersLock.Lock() 635 636 if len(th.timers) > 0 { 637 th.adjustTimers(now) 638 for len(th.timers) > 0 { 639 // Note that th.runTimer may temporarily unlock th.timersLock. 640 var tw = th.runTimer(now) 641 if tw != 0 { 642 if tw > 0 { 643 pollUntil = tw 644 } 645 break 646 } 647 ran = true 648 } 649 } 650 651 // If this is the local P, and there are a lot of deleted timers, 652 // clear them out. We only do this for the local P to reduce 653 // lock contention on timersLock. 654 if int(atomic.LoadInt32(&th.deletedTimers)) > len(th.timers)/4 { 655 th.clearDeletedTimers() 656 } 657 658 th.timersLock.Unlock() 659 660 return now, pollUntil, ran 661 } 662 663 // Check for deadlock situation 664 func (th *TimingHeap) checkDead() { 665 // Maybe jump time forward for playground. 666 if faketime != 0 { 667 var when = th.sleepUntil() 668 669 faketime = when 670 671 var mp = mget() 672 if mp == nil { 673 // There should always be a free M since 674 // nothing is running. 675 throw("timers - checkDead: no m for timer") 676 } 677 return 678 } 679 680 // There are no goroutines running, so we can look at the P's. 681 if len(th.timers) > 0 { 682 return 683 } 684 } 685 686 // badTimer is called if the timer data structures have been corrupted, 687 // presumably due to racy use by the program. We panic here rather than 688 // panicing due to invalid slice access while holding locks. 689 // See issue #25686. 690 func badTimer() { 691 panic("timers: data corruption") 692 } 693 694 // Heap maintenance algorithms. 695 // These algorithms check for slice index errors manually. 696 // Slice index error can happen if the program is using racy 697 // access to timers. We don't want to panic here, because 698 // it will cause the program to crash with a mysterious 699 // "panic holding locks" message. Instead, we panic while not 700 // holding a lock. 701 702 // siftUpTimer puts the timer at position i in the right place 703 // in the heap by moving it up toward the top of the heap. 704 // It returns the smallest changed index. 705 func (th *TimingHeap) siftUpTimer(i int) int { 706 var timers = th.timers 707 var timerWhen = timers[i].when 708 709 var tmp = timers[i] 710 for i > 0 { 711 var p = (i - 1) / 4 // parent 712 if timerWhen >= timers[p].when { 713 break 714 } 715 timers[i] = timers[p] 716 i = p 717 } 718 if tmp != timers[i] { 719 timers[i] = tmp 720 } 721 return i 722 } 723 724 // siftDownTimer puts the timer at position i in the right place 725 // in the heap by moving it down toward the bottom of the heap. 726 func (th *TimingHeap) siftDownTimer(i int) { 727 var timers = th.timers 728 var timersLen = len(timers) 729 var timerWhen = timers[i].when 730 731 var tmp = timers[i] 732 for { 733 var c = i*4 + 1 // left child 734 var c3 = c + 2 // mid child 735 if c >= timersLen { 736 break 737 } 738 var w = timers[c].when 739 if c+1 < timersLen && timers[c+1].when < w { 740 w = timers[c+1].when 741 c++ 742 } 743 if c3 < timersLen { 744 var w3 = timers[c3].when 745 if c3+1 < timersLen && timers[c3+1].when < w3 { 746 w3 = timers[c3+1].when 747 c3++ 748 } 749 if w3 < w { 750 w = w3 751 c = c3 752 } 753 } 754 if w >= timerWhen { 755 break 756 } 757 timers[i] = timers[c] 758 i = c 759 } 760 if tmp != timers[i] { 761 timers[i] = tmp 762 } 763 }