summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrpj <rpj>1999-08-12 00:53:52 +0000
committerrpj <rpj>1999-08-12 00:53:52 +0000
commitf92b4771baf8faccc197ae06922690d1cef74dad (patch)
tree8b06f29abbaf8d1cc027bab0731a9fe79db69413
parent77d0a6ff70ef2bb480c927e563340fc501ec0930 (diff)
1999-08-12 Ross Johnson <rpj@ixobrychus.canberra.edu.au>snap-1999-05-30-patches
* exit.c (pthread_exit): Check for implicitly created threads to avoid raising an unhandled exception. 1999-07-12 Ross Johnson <rpj@ixobrychus.canberra.edu.au> * condvar.c (pthread_cond_destroy): Add critical section. (cond_timedwait): Add critical section; check for timeout waiting on semaphore. (pthread_cond_broadcast): Add critical section. - Peter Slacik <Peter.Slacik@tatramed.sk> 1999-07-09 Ross Johnson <rpj@ixobrychus.canberra.edu.au> The following changes fix a bug identified by Lorin Hochstein <lmh@xiphos.ca> and solved by John Bossom <John.Bossom@Cognos.COM>. The problem was that cleanup handlers were not executed when pthread_exit() was called. * implement.h (pthread_t_): Add exceptionInformation element for C++ per-thread exception information. (general): Define and rename exceptions. * misc.c (CancelableWait): _PTHREAD_EPS_CANCEL (SEH) and Pthread_exception_cancel (C++) used to identify the exception. * cancel.c (pthread_testcancel): _PTHREAD_EPS_CANCEL (SEH) and Pthread_exception_cancel (C++) used to identify the exception. * exit.c (pthread_exit): throw/raise an exception to return to _pthread_threadStart() to exit the thread. _PTHREAD_EPS_EXIT (SEH) and Pthread_exception_exit (C++) used to identify the exception. * private.c (_pthread_threadStart): Add pthread_exit exception trap; clean up and exit the thread directly rather than via pthread_exit().
-rw-r--r--ANNOUNCE53
-rw-r--r--CONTRIBUTORS13
-rw-r--r--ChangeLog40
-rw-r--r--MAINTAINERS16
-rw-r--r--cancel.c4
-rw-r--r--condvar.c52
-rw-r--r--exit.c57
-rw-r--r--implement.h22
-rw-r--r--misc.c4
-rw-r--r--private.c73
-rw-r--r--pthread.h10
-rw-r--r--tests/ChangeLog4
-rw-r--r--tests/Makefile3
13 files changed, 279 insertions, 72 deletions
diff --git a/ANNOUNCE b/ANNOUNCE
index b7d9442..810342c 100644
--- a/ANNOUNCE
+++ b/ANNOUNCE
@@ -9,8 +9,8 @@
We are pleased to announce the availability of a new snapshot of
Pthreads-win32, an Open Source Software (OSS) implementation of the
Threads component of the POSIX 1003.1c 1995 Standard for Microsoft's
-Win32 environment. Some functions from POSIX 1003.1b are supported
-including semaphores.
+Win32 environment. Some functions from POSIX 1003.1b are also
+supported including semaphores.
Pthreads-win32 is free software, distributed under the GNU Library
General Public License (LGPL).
@@ -27,7 +27,7 @@ Change Summary (since the last snapshot)
Some minor bugs have been fixed. See the ChangeLog file for details.
-Some more POSIX 1b functions are now included but ony return an
+Some more POSIX 1b functions are now included but only return an
error (ENOSYS) if called. They are:
sem_open
@@ -267,7 +267,7 @@ MSVC:
MSVC works.
Mingw32: (ftp://ftp.xraylith.wisc.edu/pub/khan/gnu-win32/mingw32/egcs-1.1.1/)
-Mingw32 must use the thread-safe MSVCRT library. You can link
+Mingw32 must use the thread-safe MSVCRT library (see the FAQ). You can link
against the export library libpthread32.a built under Mingw32
but you must run your application with the version of pthread.dll built
with MSVC.
@@ -281,41 +281,46 @@ pthreads-win32 against Cygwin.
Generally:
For convenience, the following pre-built files are available on the FTP site
-(see above):
+(see Availability above):
pthread.h - for POSIX 1c threads
semaphore.h - for POSIX 1b semaphores
sched.h - for POSIX 1b scheduling
- pthread.dll - built with MSVC cl compiler
- pthread.lib - built with MSVC cl compiler
- libpthread32.a - built with Mingw32 (use with MSVC pthread.dll)
-
+ pthread.dll - built with MSVC cl compiler on NT4.0
+ pthread.lib - built with MSVC cl compiler on NT4.0
+ libpthread32.a - built with Mingw32 on Win98 (use with
+ MSVC pthread.dll for runtime)
+
These are the only files you need in order to build POSIX threads
applications for Win32 using either MSVC or Mingw32.
See the FAQ file in the source tree for additional information.
-
-Building the library with MSVC
--------------------------------
-
-There is currently no Makefile for building the library under MSVC.
-There is a batch file "buildlib.bat" which will build the library.
-
-Why you can't build the library with Cygwin or Mingw32 (yet)
-------------------------------------------------------------
-
+Why you can't build the DLL itself with Mingw32 (yet)
+-----------------------------------------------------
+
The library makes use of exception handling internally (Win32 SEH if
compiled with MSVC and C++ EH otherwise). Unfortunately, current
versions of egcs (g++) do not have thread-safe exception handling
-and therefore the DLL pthread.dll cannot be built under either
-Mingw32 or Cygwin.
-
-
+and therefore the DLL pthread.dll cannot be built.
+
+However, it is possible to build applications which make use of
+pthreads-win32, using Mingw32's gcc or g++.
+
+
+Building the library with MSVC
+------------------------------
+
+There is currently no Makefile for building the library under MSVC.
+This is partly because my access to MSVC is via telnet to a remote NT
+machine. There is a batch file "buildlib.bat" which will build the
+library.
+
+
Documentation
-------------
-
+
Currently, there is no documentation included in the package apart
from the copious comments in the source code.
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
new file mode 100644
index 0000000..2fd2592
--- /dev/null
+++ b/CONTRIBUTORS
@@ -0,0 +1,13 @@
+Contributors (in approximate order of appearance)
+
+Ben Elliston bje@cygnus.com
+Ross Johnson rpj@ise.canberra.edu.au
+Robert Colquhoun rjc@trump.net.au
+John E. Bossom John.Bossom@cognos.com
+Anders Norlander anorland@hem2.passagen.se
+Tor Lillqvist tml@iki.fi
+Kevin Ruland Kevin.Ruland@anheuser-busch.com
+Mike Russo miker@eai.com
+Mark E. Armstrong avail@pacbell.net
+Lorin Hochstein lmh@xiphos.ca
+Peter Slacik Peter.Slacik@tatramed.sk \ No newline at end of file
diff --git a/ChangeLog b/ChangeLog
index 611a83f..bdaf3b6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,43 @@
+1999-08-12 Ross Johnson <rpj@ixobrychus.canberra.edu.au>
+
+ * exit.c (pthread_exit): Check for implicitly created threads
+ to avoid raising an unhandled exception.
+
+1999-07-12 Ross Johnson <rpj@ixobrychus.canberra.edu.au>
+
+ * condvar.c (pthread_cond_destroy): Add critical section.
+ (cond_timedwait): Add critical section; check for timeout
+ waiting on semaphore.
+ (pthread_cond_broadcast): Add critical section.
+ - Peter Slacik <Peter.Slacik@tatramed.sk>
+
+1999-07-09 Ross Johnson <rpj@ixobrychus.canberra.edu.au>
+
+ The following changes fix a bug identified by
+ Lorin Hochstein <lmh@xiphos.ca> and solved by
+ John Bossom <John.Bossom@Cognos.COM>.
+
+ The problem was that cleanup handlers were not executed when
+ pthread_exit() was called.
+
+ * implement.h (pthread_t_): Add exceptionInformation element for
+ C++ per-thread exception information.
+ (general): Define and rename exceptions.
+
+
+ * misc.c (CancelableWait): _PTHREAD_EPS_CANCEL (SEH) and
+ Pthread_exception_cancel (C++) used to identify the exception.
+
+ * cancel.c (pthread_testcancel): _PTHREAD_EPS_CANCEL (SEH) and
+ Pthread_exception_cancel (C++) used to identify the exception.
+
+ * exit.c (pthread_exit): throw/raise an exception to return to
+ _pthread_threadStart() to exit the thread. _PTHREAD_EPS_EXIT (SEH)
+ and Pthread_exception_exit (C++) used to identify the exception.
+
+ * private.c (_pthread_threadStart): Add pthread_exit exception trap;
+ clean up and exit the thread directly rather than via pthread_exit().
+
Sun May 30 00:25:02 1999 Ross Johnson <rpj@ixobrychus.canberra.edu.au>
* semaphore.h (mode_t): Conditionally typedef it.
diff --git a/MAINTAINERS b/MAINTAINERS
index 43d3142..d253c1f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1,14 +1,4 @@
- Maintainers
+CVS Repository maintainers
-Write permission
-
-Ben Elliston bje@cygnus.com
-Ross Johnson rpj@ise.canberra.edu.au
-
-Contributors (in order of appearance)
-
-Robert Colquhoun rjc@trump.net.au
-John E. Bossom John.Bossom@cognos.com
-Anders Norlander anorland@hem2.passagen.se
-Tor Lillqvist tml@iki.fi
-Kevin Ruland Kevin.Ruland@anheuser-busch.com
+Ross Johnson rpj@ise.canberra.edu.au
+Ben Elliston bje@cygnus.com
diff --git a/cancel.c b/cancel.c
index dac8e66..c3711e2 100644
--- a/cancel.c
+++ b/cancel.c
@@ -195,7 +195,7 @@ pthread_testcancel (void)
DWORD exceptionInformation[3];
- exceptionInformation[0] = (DWORD) (0);
+ exceptionInformation[0] = (DWORD) (_PTHREAD_EPS_CANCEL);
exceptionInformation[1] = (DWORD) (0);
exceptionInformation[2] = (DWORD) (0);
@@ -209,7 +209,7 @@ pthread_testcancel (void)
#ifdef __cplusplus
- throw Pthread_exception();
+ throw Pthread_exception_cancel();
#endif /* __cplusplus */
diff --git a/condvar.c b/condvar.c
index 580c379..c84a32a 100644
--- a/condvar.c
+++ b/condvar.c
@@ -459,14 +459,21 @@ pthread_cond_destroy (pthread_cond_t * cond)
if (cv != (pthread_cond_t) _PTHREAD_OBJECT_AUTO_INIT)
{
+ if (pthread_mutex_lock(&(cv->waitersLock)) != 0)
+ {
+ return EINVAL;
+ }
+
if (cv->waiters > 0)
{
+ (void) pthread_mutex_unlock(&(cv->waitersLock));
return EBUSY;
}
(void) sem_destroy (&(cv->sema));
- (void) pthread_mutex_destroy (&(cv->waitersLock));
(void) CloseHandle (cv->waitersDone);
+ (void) pthread_mutex_unlock(&(cv->waitersLock));
+ (void) pthread_mutex_destroy (&(cv->waitersLock));
free(cv);
}
@@ -510,10 +517,22 @@ cond_timedwait (pthread_cond_t * cond,
cv = *cond;
/*
- * OK to increment cond->waiters because the caller locked 'mutex'
+ * It's not OK to increment cond->waiters while the caller locked 'mutex',
+ * there may be other threads just waking up (with 'mutex' unlocked)
+ * and cv->... data is not protected.
*/
+ if (pthread_mutex_lock(&(cv->waitersLock)) != 0)
+ {
+ return EINVAL;
+ }
+
cv->waiters++;
+ if (pthread_mutex_unlock(&(cv->waitersLock)) != 0)
+ {
+ return EINVAL;
+ }
+
/*
* We keep the lock held just long enough to increment the count of
* waiters by one (above).
@@ -527,6 +546,7 @@ cond_timedwait (pthread_cond_t * cond,
* Wait to be awakened by
* pthread_cond_signal, or
* pthread_cond_broadcast
+ * timeout
*
* Note:
* _pthread_sem_timedwait is a cancellation point,
@@ -548,10 +568,10 @@ cond_timedwait (pthread_cond_t * cond,
if ((internal_result = pthread_mutex_lock (&(cv->waitersLock))) == 0)
{
/*
- * By making the waiter responsible for decrementing
- * its count we don't have to worry about having an internal
- * mutex.
+ * The waiter is responsible for decrementing
+ * its count, protected by an internal mutex.
*/
+
cv->waiters--;
lastWaiter = cv->wasBroadcast && (cv->waiters == 0);
@@ -559,7 +579,7 @@ cond_timedwait (pthread_cond_t * cond,
internal_result = pthread_mutex_unlock (&(cv->waitersLock));
}
- if (result == 0 && internal_result == 0)
+ if ((result == 0 || result == ETIMEDOUT) && internal_result == 0)
{
if (lastWaiter)
{
@@ -576,7 +596,7 @@ cond_timedwait (pthread_cond_t * cond,
/*
* We must always regain the external mutex, even when
- * errors occur because that's the guarantee that we give
+ * errors occur, because that's the guarantee that we give
* to our callers
*/
(void) pthread_mutex_lock (mutex);
@@ -760,6 +780,7 @@ pthread_cond_signal (pthread_cond_t * cond)
/*
* No-op if the CV is static and hasn't been initialised yet.
+ * Assuming that race conditions are harmless.
*/
if (cv == (pthread_cond_t) _PTHREAD_OBJECT_AUTO_INIT)
{
@@ -768,6 +789,7 @@ pthread_cond_signal (pthread_cond_t * cond)
/*
* If there aren't any waiters, then this is a no-op.
+ * Assuming that race conditions are harmless.
*/
if (cv->waiters > 0)
{
@@ -817,6 +839,7 @@ pthread_cond_broadcast (pthread_cond_t * cond)
*/
{
int result = 0;
+ int wereWaiters = FALSE;
pthread_cond_t cv;
if (cond == NULL || *cond == NULL)
@@ -828,13 +851,20 @@ pthread_cond_broadcast (pthread_cond_t * cond)
/*
* No-op if the CV is static and hasn't been initialised yet.
+ * Assuming that any race condition is harmless.
*/
if (cv == (pthread_cond_t) _PTHREAD_OBJECT_AUTO_INIT)
{
return 0;
}
+ if (pthread_mutex_lock(&(cv->waitersLock)) == EINVAL)
+ {
+ return EINVAL;
+ }
+
cv->wasBroadcast = TRUE;
+ wereWaiters = (cv->waiters > 0);
/*
* Wake up all waiters
@@ -843,14 +873,16 @@ pthread_cond_broadcast (pthread_cond_t * cond)
? 0
: EINVAL);
- if (cv->waiters > 0 && result == 0)
+ (void) pthread_mutex_unlock(&(cv->waitersLock));
+
+ if (wereWaiters && result == 0)
{
/*
* Wait for all the awakened threads to acquire their part of
* the counting semaphore
*/
- if (WaitForSingleObject (cv->waitersDone, INFINITE) ==
- WAIT_OBJECT_0)
+ if (WaitForSingleObject (cv->waitersDone, INFINITE)
+ == WAIT_OBJECT_0)
{
result = 0;
}
diff --git a/exit.c b/exit.c
index 46c2b3f..5035718 100644
--- a/exit.c
+++ b/exit.c
@@ -51,7 +51,60 @@ pthread_exit (void *value_ptr)
* ------------------------------------------------------
*/
{
- _pthread_callUserDestroyRoutines((pthread_t) pthread_getspecific(_pthread_selfThreadKey));
+ pthread_t self = pthread_self();
+
+ /* If the current thread is implicit it was not started through
+ pthread_create(), therefore we cleanup and end the thread
+ here. Otherwise we raise an exception to unwind the exception
+ stack. The exception will be caught by _pthread_threadStart(),
+ which will cleanup and end the thread for us.
+ */
+
+ if (self->implicit)
+ {
+ _pthread_callUserDestroyRoutines(self);
+
+ _endthreadex ((unsigned) value_ptr);
+
+ /* Never reached */
+ }
+ else
+ {
+#ifdef _MSC_VER
+
+ DWORD exceptionInformation[3];
+
+ exceptionInformation[0] = (DWORD) (_PTHREAD_EPS_EXIT);
+ exceptionInformation[1] = (DWORD) (value_ptr);
+ exceptionInformation[2] = (DWORD) (0);
+
+ RaiseException (
+ EXCEPTION_PTHREAD_SERVICES,
+ 0,
+ 3,
+ exceptionInformation);
+
+#else /* ! _MSC_VER */
+
+#ifdef __cplusplus
+
+ self->exceptionInformation = value_ptr;
+ throw Pthread_exception_exit();
+
+#else /* ! __cplusplus */
+
+ (void) pthread_pop_cleanup( 1 );
+
+ _pthread_callUserDestroyRoutines(self);
+
+ _endthreadex ((unsigned) value_ptr);
+
+#endif /* __cplusplus */
+
+#endif /* _MSC_VER */
+
+ }
+
+ /* Never reached. */
- _endthreadex ((unsigned) value_ptr);
}
diff --git a/implement.h b/implement.h
index a719167..f18c7d5 100644
--- a/implement.h
+++ b/implement.h
@@ -77,6 +77,9 @@ struct pthread_t_ {
sigset_t sigmask;
#endif /* HAVE_SIGSET_T */
int implicit:1;
+#ifdef __cplusplus
+ void * exceptionInformation;
+#endif
void *keys;
};
@@ -256,18 +259,27 @@ struct ThreadKeyAssoc {
*/
#define EXCEPTION_PTHREAD_SERVICES \
MAKE_SOFTWARE_EXCEPTION( SE_ERROR, \
- PTHREAD_SERVICES_FACILITY, \
- PTHREAD_SERVICES_ERROR )
+ _PTHREAD_SERVICES_FACILITY, \
+ _PTHREAD_SERVICES_ERROR )
+#define _PTHREAD_SERVICES_FACILITY 0xBAD
+#define _PTHREAD_SERVICES_ERROR 0xDEED
-#define PTHREAD_SERVICES_FACILITY 0xBAD
-#define PTHREAD_SERVICES_ERROR 0xDEED
+/*
+ * Services available through EXCEPTION_PTHREAD_SERVICES
+ */
+#define _PTHREAD_EPS_CANCEL 0
+#define _PTHREAD_EPS_EXIT 1
#else
#ifdef __cplusplus
-class Pthread_exception {};
+/*
+ * Exceptions similar to the SEH exceptions above.
+ */
+class Pthread_exception_cancel {};
+class Pthread_exception_exit {};
#else /* __cplusplus */
diff --git a/misc.c b/misc.c
index a37db5c..208541a 100644
--- a/misc.c
+++ b/misc.c
@@ -314,7 +314,7 @@ CancelableWait (HANDLE waitHandle, DWORD timeout)
DWORD exceptionInformation[3];
- exceptionInformation[0] = (DWORD) (0);
+ exceptionInformation[0] = (DWORD) (_PTHREAD_EPS_CANCEL);
exceptionInformation[1] = (DWORD) (0);
exceptionInformation[2] = (DWORD) (0);
@@ -328,7 +328,7 @@ CancelableWait (HANDLE waitHandle, DWORD timeout)
#ifdef __cplusplus
- throw Pthread_exception();
+ throw Pthread_exception_cancel();
#endif /* __cplusplus */
diff --git a/private.c b/private.c
index 9a69768..1b6ba64 100644
--- a/private.c
+++ b/private.c
@@ -139,22 +139,43 @@ _pthread_processTerminate (void)
} /* processTerminate */
+#ifdef _MSC_VER
+
+static DWORD
+ExceptionFilter (EXCEPTION_POINTERS * ep, DWORD * ei)
+{
+ DWORD param;
+ DWORD numParams = ep->ExceptionRecord->NumberParameters;
+
+ numParams = (numParams > 3) ? 3 : numParams;
+
+ for (param = 0; param < numParams; param++)
+ {
+ ei[param] = ep->ExceptionRecord->ExceptionInformation[param];
+ }
+
+ return EXCEPTION_EXECUTE_HANDLER;
+}
+
+#endif /* _MSC_VER */
+
void *
_pthread_threadStart (ThreadParms * threadParms)
{
- pthread_t tid;
+ pthread_t self;
void *(*start) (void *);
void *arg;
+ DWORD ei[3];
void * status;
- tid = threadParms->tid;
+ self = threadParms->tid;
start = threadParms->start;
arg = threadParms->arg;
free (threadParms);
- pthread_setspecific (_pthread_selfThreadKey, tid);
+ pthread_setspecific (_pthread_selfThreadKey, self);
#ifdef _MSC_VER
@@ -166,13 +187,32 @@ _pthread_threadStart (ThreadParms * threadParms)
(*start) (arg);
status = (void *) 0;
}
- __except (EXCEPTION_EXECUTE_HANDLER)
+ __except (ExceptionFilter(GetExceptionInformation(), ei))
{
- /*
- * A system unexpected exception had occurred running the user's
- * routine. We get control back within this block.
- */
- status = PTHREAD_CANCELED;
+ DWORD ec = GetExceptionCode();
+
+ if (ec == EXCEPTION_PTHREAD_SERVICES)
+ {
+ switch (ei[0])
+ {
+ case _PTHREAD_EPS_CANCEL:
+ status = PTHREAD_CANCELED;
+ break;
+ case _PTHREAD_EPS_EXIT:
+ status = (void *) ei[1];
+ break;
+ default:
+ status = PTHREAD_CANCELED;
+ }
+ }
+ else
+ {
+ /*
+ * A system unexpected exception had occurred running the user's
+ * routine. We get control back within this block.
+ */
+ status = PTHREAD_CANCELED;
+ }
}
#else /* _MSC_VER */
@@ -187,13 +227,20 @@ _pthread_threadStart (ThreadParms * threadParms)
(*start) (arg);
status = (void *) 0;
}
- catch (Pthread_exception)
+ catch (Pthread_exception_cancel)
{
/*
* Thread was cancelled.
*/
status = PTHREAD_CANCELED;
}
+ catch (Pthread_exception_exit)
+ {
+ /*
+ * Thread was exited via pthread_exit().
+ */
+ status = self->exceptionInformation;
+ }
catch (...)
{
/*
@@ -214,9 +261,11 @@ _pthread_threadStart (ThreadParms * threadParms)
#endif /* __cplusplus */
-#endif /* _WIN32 */
+#endif /* _MSC_VER */
+
+ _pthread_callUserDestroyRoutines(self);
- pthread_exit (status);
+ _endthreadex ((unsigned) status);
/*
* Never reached.
diff --git a/pthread.h b/pthread.h
index 8ce4d5e..29961f2 100644
--- a/pthread.h
+++ b/pthread.h
@@ -540,6 +540,14 @@ struct sched_param {
* WIN32 SEH
* C
* C++
+ *
+ * Please note that exiting a push/pop block via
+ * "return", "exit", "break", or "continue" will
+ * lead to different behaviour amongst applications
+ * depending upon whether the library was built
+ * using SEH, C++, or C. For example, a library built
+ * with SEH will call the cleanup routine, while both
+ * C++ and C built versions will not.
*/
typedef struct _pthread_cleanup_t _pthread_cleanup_t;
@@ -550,7 +558,7 @@ struct _pthread_cleanup_t
void *arg;
#if !defined(_MSC_VER) && !defined(__cplusplus)
_pthread_cleanup_t *prev;
-#endif
+#endif /* !_MSC_VER && ! __cplusplus */
};
#ifdef _MSC_VER
diff --git a/tests/ChangeLog b/tests/ChangeLog
index 22b7028..4140f0c 100644
--- a/tests/ChangeLog
+++ b/tests/ChangeLog
@@ -1,3 +1,7 @@
+Mon May 31 10:25:01 1999 Ross Johnson <rpj@ixobrychus.canberra.edu.au>
+
+ * Makefile (GLANG): Add GCC language option.
+
Sat May 29 23:29:04 1999 Ross Johnson <rpj@ixobrychus.canberra.edu.au>
* runall.bat (condvar5): Add new test.
diff --git a/tests/Makefile b/tests/Makefile
index f96d830..7a348ab 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -11,8 +11,9 @@ ECHO = @echo
#
# Mingw32
#
+GLANG = c
CC = gcc
-CFLAGS = -g -O2 -UNDEBUG -Wall -o $@ $^
+CFLAGS = -g -O2 -UNDEBUG -Wall -x $(GLANG) -o $@ $^
BUILD_DIR = ..
INCLUDES = -I.
LIBS = ./libpthread32.a