]> git.saurik.com Git - wxWidgets.git/blob - src/mac/thread.cpp
Blind fix for bug #529874 .
[wxWidgets.git] / src / mac / thread.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: thread.cpp
3 // Purpose: wxThread Implementation
4 // Author: Original from Wolfram Gloger/Guilhem Lavaux/Vadim Zeitlin
5 // Modified by: Stefan Csomor
6 // Created: 04/22/98
7 // RCS-ID: $Id$
8 // Copyright: (c) Wolfram Gloger (1996, 1997); Guilhem Lavaux (1998),
9 // Vadim Zeitlin (1999) , Stefan Csomor (2000)
10 // Licence: wxWindows licence
11 /////////////////////////////////////////////////////////////////////////////
12
13 #ifdef __GNUG__
14 #pragma implementation "thread.h"
15 #endif
16
17 // ----------------------------------------------------------------------------
18 // headers
19 // ----------------------------------------------------------------------------
20
21 // For compilers that support precompilation, includes "wx.h".
22 #include "wx/wxprec.h"
23
24 #if defined(__BORLANDC__)
25 #pragma hdrstop
26 #endif
27
28 #ifndef WX_PRECOMP
29 #include "wx/wx.h"
30 #endif
31
32 #if wxUSE_THREADS
33
34 #include "wx/module.h"
35 #include "wx/thread.h"
36
37 #ifdef __WXMAC__
38 #include <Threads.h>
39 #include "wx/mac/uma.h"
40 #endif
41
42 // ----------------------------------------------------------------------------
43 // constants
44 // ----------------------------------------------------------------------------
45
46 // the possible states of the thread ("=>" shows all possible transitions from
47 // this state)
48 enum wxThreadState
49 {
50 STATE_NEW, // didn't start execution yet (=> RUNNING)
51 STATE_RUNNING, // thread is running (=> PAUSED, CANCELED)
52 STATE_PAUSED, // thread is temporarily suspended (=> RUNNING)
53 STATE_CANCELED, // thread should terminate a.s.a.p. (=> EXITED)
54 STATE_EXITED // thread is terminating
55 };
56
57 // ----------------------------------------------------------------------------
58 // this module globals
59 // ----------------------------------------------------------------------------
60
61 static ThreadID gs_idMainThread = kNoThreadID ;
62 static bool gs_waitingForThread = FALSE ;
63
64 // ============================================================================
65 // MacOS implementation of thread classes
66 // ============================================================================
67
68 class wxMacStCritical
69 {
70 public :
71 wxMacStCritical()
72 {
73 if ( UMASystemIsInitialized() )
74 ThreadBeginCritical() ;
75 }
76 ~wxMacStCritical()
77 {
78 if ( UMASystemIsInitialized() )
79 ThreadEndCritical() ;
80 }
81 };
82
83 // ----------------------------------------------------------------------------
84 // wxMutex implementation
85 // ----------------------------------------------------------------------------
86
87 class wxMutexInternal
88 {
89 public:
90 wxMutexInternal()
91 {
92 m_owner = kNoThreadID ;
93 }
94
95 ~wxMutexInternal()
96 {
97 }
98
99 public:
100 ThreadID m_owner ;
101 wxArrayLong m_waiters ;
102 };
103
104 wxMutex::wxMutex()
105 {
106 m_internal = new wxMutexInternal;
107
108 m_locked = 0;
109 }
110
111 wxMutex::~wxMutex()
112 {
113 if ( m_locked > 0 )
114 {
115 wxLogDebug(_T("Warning: freeing a locked mutex (%d locks)."), m_locked);
116 }
117
118 delete m_internal;
119 }
120
121 wxMutexError wxMutex::Lock()
122 {
123 wxMacStCritical critical ;
124 if ( UMASystemIsInitialized() )
125 {
126 OSErr err ;
127 ThreadID current = kNoThreadID;
128 err = ::MacGetCurrentThread(&current);
129 // if we are not the owner, add this thread to the list of waiting threads, stop this thread
130 // and invoke the scheduler to continue executing the owner's thread
131 while ( m_internal->m_owner != kNoThreadID && m_internal->m_owner != current)
132 {
133 m_internal->m_waiters.Add(current);
134 err = ::SetThreadStateEndCritical(kCurrentThreadID, kStoppedThreadState, m_internal->m_owner);
135 err = ::ThreadBeginCritical();
136 }
137 m_internal->m_owner = current;
138 }
139 m_locked++;
140
141 return wxMUTEX_NO_ERROR;
142 }
143
144 wxMutexError wxMutex::TryLock()
145 {
146 wxMacStCritical critical ;
147 if ( UMASystemIsInitialized() )
148 {
149 OSErr err ;
150 ThreadID current = kNoThreadID;
151 ::MacGetCurrentThread(&current);
152 // if we are not the owner, give an error back
153 if ( m_internal->m_owner != kNoThreadID && m_internal->m_owner != current )
154 return wxMUTEX_BUSY;
155
156 m_internal->m_owner = current;
157 }
158 m_locked++;
159
160 return wxMUTEX_NO_ERROR;
161 }
162
163 wxMutexError wxMutex::Unlock()
164 {
165 if ( UMASystemIsInitialized() )
166 {
167 OSErr err;
168 err = ::ThreadBeginCritical();
169
170 if (m_locked > 0)
171 m_locked--;
172
173 // this mutex is not owned by anybody anmore
174 m_internal->m_owner = kNoThreadID;
175
176 // now pass on to the first waiting thread
177 ThreadID firstWaiting = kNoThreadID;
178 bool found = false;
179 while (!m_internal->m_waiters.IsEmpty() && !found)
180 {
181 firstWaiting = m_internal->m_waiters[0];
182 err = ::SetThreadState(firstWaiting, kReadyThreadState, kNoThreadID);
183 // in case this was not successful (dead thread), we just loop on and reset the id
184 found = (err != threadNotFoundErr);
185 if ( !found )
186 firstWaiting = kNoThreadID ;
187 m_internal->m_waiters.RemoveAt(0) ;
188 }
189 // now we have a valid firstWaiting thread, which has been scheduled to run next, just end the
190 // critical section and invoke the scheduler
191 err = ::SetThreadStateEndCritical(kCurrentThreadID, kReadyThreadState, firstWaiting);
192 }
193 else
194 {
195 if (m_locked > 0)
196 m_locked--;
197 }
198 return wxMUTEX_NO_ERROR;
199 }
200
201 // ----------------------------------------------------------------------------
202 // wxCondition implementation
203 // ----------------------------------------------------------------------------
204
205 class wxConditionInternal
206 {
207 public:
208 wxConditionInternal()
209 {
210 m_excessSignals = 0 ;
211 }
212 ~wxConditionInternal()
213 {
214 }
215
216 bool Wait(unsigned long msectimeout)
217 {
218 wxMacStCritical critical ;
219 if ( m_excessSignals > 0 )
220 {
221 --m_excessSignals ;
222 return TRUE ;
223 }
224 else if ( msectimeout == 0 )
225 {
226 return FALSE ;
227 }
228 else
229 {
230 }
231 /*
232 waiters++;
233
234 // FIXME this should be MsgWaitForMultipleObjects() as well probably
235 DWORD rc = ::WaitForSingleObject(event, timeout);
236
237 waiters--;
238
239 return rc != WAIT_TIMEOUT;
240 */
241 return TRUE ;
242 }
243 void Signal()
244 {
245 wxMacStCritical critical ;
246 }
247
248 wxArrayLong m_waiters ;
249 wxInt32 m_excessSignals ;
250 };
251
252 wxCondition::wxCondition()
253 {
254 m_internal = new wxConditionInternal;
255 }
256
257 wxCondition::~wxCondition()
258 {
259 delete m_internal;
260 }
261
262 void wxCondition::Wait()
263 {
264 (void)m_internal->Wait(0xFFFFFFFFL);
265 }
266
267 bool wxCondition::Wait(unsigned long sec,
268 unsigned long nsec)
269 {
270 return m_internal->Wait(sec*1000 + nsec/1000000);
271 }
272
273 void wxCondition::Signal()
274 {
275 // set the event to signaled: if a thread is already waiting on it, it will
276 // be woken up, otherwise the event will remain in the signaled state until
277 // someone waits on it. In any case, the system will return it to a non
278 // signalled state afterwards. If multiple threads are waiting, only one
279 // will be woken up.
280 m_internal->Signal() ;
281 }
282
283 void wxCondition::Broadcast()
284 {
285 // this works because all these threads are already waiting and so each
286 // SetEvent() inside Signal() is really a PulseEvent() because the event
287 // state is immediately returned to non-signaled
288 for ( int i = 0; i < m_internal->m_waiters.Count(); i++ )
289 {
290 Signal();
291 }
292 }
293
294 // ----------------------------------------------------------------------------
295 // wxCriticalSection implementation
296 // ----------------------------------------------------------------------------
297
298 // it's implemented as a mutex on mac os, so it is defined in the headers
299
300 // ----------------------------------------------------------------------------
301 // wxThread implementation
302 // ----------------------------------------------------------------------------
303
304 // wxThreadInternal class
305 // ----------------------
306
307 class wxThreadInternal
308 {
309 public:
310 wxThreadInternal()
311 {
312 m_tid = kNoThreadID ;
313 m_state = STATE_NEW;
314 m_priority = WXTHREAD_DEFAULT_PRIORITY;
315 }
316
317 ~wxThreadInternal()
318 {
319 }
320
321 void Free()
322 {
323 }
324
325 // create a new (suspended) thread (for the given thread object)
326 bool Create(wxThread *thread, unsigned int stackSize);
327
328 // suspend/resume/terminate
329 bool Suspend();
330 bool Resume();
331 void Cancel() { m_state = STATE_CANCELED; }
332
333 // thread state
334 void SetState(wxThreadState state) { m_state = state; }
335 wxThreadState GetState() const { return m_state; }
336
337 // thread priority
338 void SetPriority(unsigned int priority);
339 unsigned int GetPriority() const { return m_priority; }
340
341 void SetResult( void *res ) { m_result = res ; }
342 void *GetResult() { return m_result ; }
343
344 // thread handle and id
345 ThreadID GetId() const { return m_tid; }
346
347 // thread function
348 static pascal void* MacThreadStart(wxThread* arg);
349
350 private:
351 wxThreadState m_state; // state, see wxThreadState enum
352 unsigned int m_priority; // thread priority in "wx" units
353 ThreadID m_tid; // thread id
354 void* m_result;
355 static ThreadEntryUPP s_threadEntry ;
356 };
357
358 static wxArrayPtrVoid s_threads ;
359
360 ThreadEntryUPP wxThreadInternal::s_threadEntry = NULL ;
361 pascal void* wxThreadInternal::MacThreadStart(wxThread *thread)
362 {
363 // first of all, check whether we hadn't been cancelled already
364 if ( thread->m_internal->GetState() == STATE_EXITED )
365 {
366 return (void*)-1;
367 }
368
369 void* rc = thread->Entry();
370
371 // enter m_critsect before changing the thread state
372 thread->m_critsect.Enter();
373 bool wasCancelled = thread->m_internal->GetState() == STATE_CANCELED;
374 thread->m_internal->SetState(STATE_EXITED);
375 thread->m_critsect.Leave();
376
377 thread->OnExit();
378
379 // if the thread was cancelled (from Delete()), then it the handle is still
380 // needed there
381 if ( thread->IsDetached() && !wasCancelled )
382 {
383 // auto delete
384 delete thread;
385 }
386 //else: the joinable threads handle will be closed when Wait() is done
387
388 return rc;
389 }
390 void wxThreadInternal::SetPriority(unsigned int priority)
391 {
392 // Priorities don't exist on Mac
393 }
394
395 bool wxThreadInternal::Create(wxThread *thread, unsigned int stackSize)
396 {
397 if ( s_threadEntry == NULL )
398 {
399 s_threadEntry = NewThreadEntryUPP( (ThreadEntryProcPtr) MacThreadStart ) ;
400 }
401 OSErr err = NewThread( kCooperativeThread,
402 s_threadEntry,
403 (void*) thread,
404 stackSize,
405 kNewSuspend,
406 &m_result,
407 &m_tid );
408
409 if ( err != noErr )
410 {
411 wxLogSysError(_("Can't create thread"));
412 return FALSE;
413 }
414
415 if ( m_priority != WXTHREAD_DEFAULT_PRIORITY )
416 {
417 SetPriority(m_priority);
418 }
419
420 return TRUE;
421 }
422
423 bool wxThreadInternal::Suspend()
424 {
425 OSErr err ;
426
427 ::ThreadBeginCritical();
428
429 if ( m_state != STATE_RUNNING )
430 {
431 ::ThreadEndCritical() ;
432 wxLogSysError(_("Can not suspend thread %x"), m_tid);
433 return FALSE;
434 }
435
436 m_state = STATE_PAUSED;
437
438 err = ::SetThreadStateEndCritical(m_tid, kStoppedThreadState, kNoThreadID);
439
440 return TRUE;
441 }
442
443 bool wxThreadInternal::Resume()
444 {
445 ThreadID current ;
446 OSErr err ;
447 err = MacGetCurrentThread( &current ) ;
448
449 wxASSERT( err == noErr ) ;
450 wxASSERT( current != m_tid ) ;
451
452 ::ThreadBeginCritical();
453 if ( m_state != STATE_PAUSED && m_state != STATE_NEW )
454 {
455 ::ThreadEndCritical() ;
456 wxLogSysError(_("Can not resume thread %x"), m_tid);
457 return FALSE;
458
459 }
460 err = ::SetThreadStateEndCritical(m_tid, kReadyThreadState, kNoThreadID);
461 wxASSERT( err == noErr ) ;
462
463 m_state = STATE_RUNNING;
464 ::ThreadEndCritical() ;
465 ::YieldToAnyThread() ;
466 return TRUE;
467 }
468
469 // static functions
470 // ----------------
471 wxThread *wxThread::This()
472 {
473 wxMacStCritical critical ;
474
475 ThreadID current ;
476 OSErr err ;
477
478 err = MacGetCurrentThread( &current ) ;
479
480 for ( int i = 0 ; i < s_threads.Count() ; ++i )
481 {
482 if ( ( (wxThread*) s_threads[i] )->GetId() == current )
483 return (wxThread*) s_threads[i] ;
484 }
485
486 wxLogSysError(_("Couldn't get the current thread pointer"));
487 return NULL;
488 }
489
490 bool wxThread::IsMain()
491 {
492 ThreadID current ;
493 OSErr err ;
494
495 err = MacGetCurrentThread( &current ) ;
496 return current == gs_idMainThread;
497 }
498
499 #ifdef Yield
500 #undef Yield
501 #endif
502
503 void wxThread::Yield()
504 {
505 ::YieldToAnyThread() ;
506 }
507
508 void wxThread::Sleep(unsigned long milliseconds)
509 {
510 clock_t start = clock() ;
511 do
512 {
513 YieldToAnyThread() ;
514 } while( clock() - start < milliseconds / CLOCKS_PER_SEC ) ;
515 }
516
517 int wxThread::GetCPUCount()
518 {
519 // we will use whatever MP API will be used for the new MP Macs
520 return 1;
521 }
522
523 unsigned long wxThread::GetCurrentId()
524 {
525 ThreadID current ;
526 MacGetCurrentThread( &current ) ;
527 return (unsigned long)current;
528 }
529
530 bool wxThread::SetConcurrency(size_t level)
531 {
532 wxASSERT_MSG( IsMain(), _T("should only be called from the main thread") );
533
534 // ok only for the default one
535 if ( level == 0 )
536 return 0;
537
538 // how many CPUs have we got?
539 if ( GetCPUCount() == 1 )
540 {
541 // don't bother with all this complicated stuff - on a single
542 // processor system it doesn't make much sense anyhow
543 return level == 1;
544 }
545
546 return TRUE ;
547 }
548
549 // ctor and dtor
550 // -------------
551
552 wxThread::wxThread(wxThreadKind kind)
553 {
554 m_internal = new wxThreadInternal();
555
556 m_isDetached = kind == wxTHREAD_DETACHED;
557 s_threads.Add( (void*) this ) ;
558 }
559
560 wxThread::~wxThread()
561 {
562 s_threads.Remove( (void*) this ) ;
563 delete m_internal;
564 }
565
566 // create/start thread
567 // -------------------
568
569 wxThreadError wxThread::Create(unsigned int stackSize)
570 {
571 wxCriticalSectionLocker lock(m_critsect);
572
573 if ( !m_internal->Create(this, stackSize) )
574 return wxTHREAD_NO_RESOURCE;
575
576 return wxTHREAD_NO_ERROR;
577 }
578
579 wxThreadError wxThread::Run()
580 {
581 wxCriticalSectionLocker lock(m_critsect);
582
583 if ( m_internal->GetState() != STATE_NEW )
584 {
585 // actually, it may be almost any state at all, not only STATE_RUNNING
586 return wxTHREAD_RUNNING;
587 }
588
589 // the thread has just been created and is still suspended - let it run
590 return Resume();
591 }
592
593 // suspend/resume thread
594 // ---------------------
595
596 wxThreadError wxThread::Pause()
597 {
598 wxCriticalSectionLocker lock(m_critsect);
599
600 return m_internal->Suspend() ? wxTHREAD_NO_ERROR : wxTHREAD_MISC_ERROR;
601 }
602
603 wxThreadError wxThread::Resume()
604 {
605 wxCriticalSectionLocker lock(m_critsect);
606
607 return m_internal->Resume() ? wxTHREAD_NO_ERROR : wxTHREAD_MISC_ERROR;
608 }
609
610 // stopping thread
611 // ---------------
612
613 wxThread::ExitCode wxThread::Wait()
614 {
615 // although under MacOS we can wait for any thread, it's an error to
616 // wait for a detached one in wxWin API
617 wxCHECK_MSG( !IsDetached(), (ExitCode)-1,
618 _T("can't wait for detached thread") );
619
620 ExitCode rc = (ExitCode)-1;
621
622 (void)Delete(&rc);
623
624 m_internal->Free();
625
626 return rc;
627 }
628
629 wxThreadError wxThread::Delete(ExitCode *pRc)
630 {
631 ExitCode rc = 0;
632
633 // Delete() is always safe to call, so consider all possible states
634
635 // has the thread started to run?
636 bool shouldResume = FALSE;
637
638 {
639 wxCriticalSectionLocker lock(m_critsect);
640
641 if ( m_internal->GetState() == STATE_NEW )
642 {
643 // WinThreadStart() will see it and terminate immediately
644 m_internal->SetState(STATE_EXITED);
645
646 shouldResume = TRUE;
647 }
648 }
649
650 // is the thread paused?
651 if ( shouldResume || IsPaused() )
652 Resume();
653
654 // does is still run?
655 if ( IsRunning() )
656 {
657 if ( IsMain() )
658 {
659 // set flag for wxIsWaitingForThread()
660 gs_waitingForThread = TRUE;
661
662 #if wxUSE_GUI
663 wxBeginBusyCursor();
664 #endif // wxUSE_GUI
665 }
666
667 // ask the thread to terminate
668 {
669 wxCriticalSectionLocker lock(m_critsect);
670
671 m_internal->Cancel();
672 }
673
674 #if wxUSE_GUI
675 // simply wait for the thread to terminate
676 while( TestDestroy() )
677 {
678 ::YieldToAnyThread() ;
679 }
680 #else // !wxUSE_GUI
681 // simply wait for the thread to terminate
682 while( TestDestroy() )
683 {
684 ::YieldToAnyThread() ;
685 }
686 #endif // wxUSE_GUI/!wxUSE_GUI
687
688 if ( IsMain() )
689 {
690 gs_waitingForThread = FALSE;
691
692 #if wxUSE_GUI
693 wxEndBusyCursor();
694 #endif // wxUSE_GUI
695 }
696 }
697
698 // if ( !::GetExitCodeThread(hThread, (LPDWORD)&rc) )
699 {
700 wxLogLastError("GetExitCodeThread");
701
702 rc = (ExitCode)-1;
703 }
704
705 if ( IsDetached() )
706 {
707 // if the thread exits normally, this is done in WinThreadStart, but in
708 // this case it would have been too early because
709 // MsgWaitForMultipleObject() would fail if the therad handle was
710 // closed while we were waiting on it, so we must do it here
711 delete this;
712 }
713
714 // wxASSERT_MSG( (DWORD)rc != STILL_ACTIVE,
715 // wxT("thread must be already terminated.") );
716
717 if ( pRc )
718 *pRc = rc;
719
720 return rc == (ExitCode)-1 ? wxTHREAD_MISC_ERROR : wxTHREAD_NO_ERROR;
721 }
722
723 wxThreadError wxThread::Kill()
724 {
725 if ( !IsRunning() )
726 return wxTHREAD_NOT_RUNNING;
727
728 // if ( !::TerminateThread(m_internal->GetHandle(), (DWORD)-1) )
729 {
730 wxLogSysError(_("Couldn't terminate thread"));
731
732 return wxTHREAD_MISC_ERROR;
733 }
734
735 m_internal->Free();
736
737 if ( IsDetached() )
738 {
739 delete this;
740 }
741
742 return wxTHREAD_NO_ERROR;
743 }
744
745 void wxThread::Exit(ExitCode status)
746 {
747 m_internal->Free();
748
749 if ( IsDetached() )
750 {
751 delete this;
752 }
753
754 m_internal->SetResult( status ) ;
755
756 /*
757 #if defined(__VISUALC__) || (defined(__BORLANDC__) && (__BORLANDC__ >= 0x500))
758 _endthreadex((unsigned)status);
759 #else // !VC++
760 ::ExitThread((DWORD)status);
761 #endif // VC++/!VC++
762 */
763 wxFAIL_MSG(wxT("Couldn't return from ExitThread()!"));
764 }
765
766 // priority setting
767 // ----------------
768
769 // since all these calls are execute cooperatively we don't have to use the critical section
770
771 void wxThread::SetPriority(unsigned int prio)
772 {
773 m_internal->SetPriority(prio);
774 }
775
776 unsigned int wxThread::GetPriority() const
777 {
778 return m_internal->GetPriority();
779 }
780
781 unsigned long wxThread::GetId() const
782 {
783 return (unsigned long)m_internal->GetId();
784 }
785
786 bool wxThread::IsRunning() const
787 {
788 return m_internal->GetState() == STATE_RUNNING;
789 }
790
791 bool wxThread::IsAlive() const
792 {
793 return (m_internal->GetState() == STATE_RUNNING) ||
794 (m_internal->GetState() == STATE_PAUSED);
795 }
796
797 bool wxThread::IsPaused() const
798 {
799 return m_internal->GetState() == STATE_PAUSED;
800 }
801
802 bool wxThread::TestDestroy()
803 {
804 return m_internal->GetState() == STATE_CANCELED;
805 }
806
807 // ----------------------------------------------------------------------------
808 // Automatic initialization for thread module
809 // ----------------------------------------------------------------------------
810
811 class wxThreadModule : public wxModule
812 {
813 public:
814 virtual bool OnInit();
815 virtual void OnExit();
816
817 private:
818 DECLARE_DYNAMIC_CLASS(wxThreadModule)
819 };
820
821 IMPLEMENT_DYNAMIC_CLASS(wxThreadModule, wxModule)
822
823 bool wxThreadModule::OnInit()
824 {
825 long response;
826 bool hasThreadManager ;
827 hasThreadManager = Gestalt( gestaltThreadMgrAttr, &response) == noErr && response & 1;
828 #if !TARGET_CARBON
829 #if GENERATINGCFM
830 // verify presence of shared library
831 hasThreadManager = hasThreadManager && ((Ptr)NewThread != (Ptr)kUnresolvedCFragSymbolAddress);
832 #endif
833 #endif
834 if ( !hasThreadManager )
835 {
836 wxMessageBox( "Error" , "Thread Support is not available on this System" , wxOK ) ;
837 return FALSE ;
838 }
839
840 // no error return for GetCurrentThreadId()
841 MacGetCurrentThread( &gs_idMainThread ) ;
842
843 return TRUE;
844 }
845
846 void wxThreadModule::OnExit()
847 {
848 }
849
850 // ----------------------------------------------------------------------------
851 // under MacOS we don't have currently preemptive threads, so any thread may access
852 // the GUI at any time
853 // ----------------------------------------------------------------------------
854
855 void WXDLLEXPORT wxMutexGuiEnter()
856 {
857 }
858
859 void WXDLLEXPORT wxMutexGuiLeave()
860 {
861 }
862
863 void WXDLLEXPORT wxMutexGuiLeaveOrEnter()
864 {
865 }
866
867 bool WXDLLEXPORT wxGuiOwnedByMainThread()
868 {
869 return false ;
870 }
871
872 // wake up the main thread
873 void WXDLLEXPORT wxWakeUpMainThread()
874 {
875 wxMacWakeUp() ;
876 }
877
878 bool WXDLLEXPORT wxIsWaitingForThread()
879 {
880 return false ;
881 }
882
883 #endif // wxUSE_THREADS
884
885 // vi:sts=4:sw=4:et