Usages of the sigdelay module can be significantly optimized for the
single-threaded case: A single pthread_sigmask call instead of several
sigaction system calls.

Therefore it's worth optimizing the single-threaded case here (e.g.
for the 'fatal-signal' module).

The declarative approach that we have offered so far, i.e. some
GNULIB_foo_SINGLE_THREADED declaration in configure.ac, does not work well
because some packages have a few multithreaded and many single-threaded
programs:
  - GNU coreutils: sort,
  - GNU gettext: msgmerge, msginit.

A declarative approach per program, i.e. allowing the programmer to
declare the single-threaded-ness of a program at the beginning of the
main() function, is not suitable either:
  - It would mean another boilerplate call in main() for most programs.
  - It would be fragile and unreliable, because a program can be linked
    to some libraries that suddenly start to make use of multiple threads.

Therefore, an automatic approach is better. This patch implements that:
It looks whether the program or some of its linked libraries makes use of
a symbol like 'pthread_create' or 'thrd_create'. Excluding libc, of course.


2026-05-16  Bruno Haible  <[email protected]>

        thread-optim: Add tests.
        * tests/test-thread-optim1.c: New file.
        * tests/test-thread-optim2.c: New file.
        * modules/thread-optim-tests: New file.

        thread-optim: Port to older glibc, musl libc, *BSD, Solaris, Android.
        * lib/thread-optim.h: Include <stdbool.h>.
        (gl_multithreaded): Define differently on ELF platforms.
        (_GL_MULTITHREADED_ALWAYS_TRUE, _GL_MULTITHREADED_VIA_ELF): New macros.
        (gl_set_multithreaded): New declaration.
        * lib/thread-optim.c: New file.
        * lib/thread-creators.gperf: New file.
        * lib/thread-optim-proto.c: New file.
        * modules/thread-optim (Files): Add these files.
        (Depends-on): Add gperf, bool, stdint-h.
        (configure.ac): Test for <link.h>, dl_iterate_phdr.
        (Makefile.am): Compile thread-optim.c. Create thread-creators.h through
        gperf.

>From 86488a20eac1fd93282a15e2b9fee0526c080027 Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Sat, 16 May 2026 12:41:01 +0200
Subject: [PATCH 1/2] thread-optim: Port to older glibc, musl libc, *BSD,
 Solaris, Android.

* lib/thread-optim.h: Include <stdbool.h>.
(gl_multithreaded): Define differently on ELF platforms.
(_GL_MULTITHREADED_ALWAYS_TRUE, _GL_MULTITHREADED_VIA_ELF): New macros.
(gl_set_multithreaded): New declaration.
* lib/thread-optim.c: New file.
* lib/thread-creators.gperf: New file.
* lib/thread-optim-proto.c: New file.
* modules/thread-optim (Files): Add these files.
(Depends-on): Add gperf, bool, stdint-h.
(configure.ac): Test for <link.h>, dl_iterate_phdr.
(Makefile.am): Compile thread-optim.c. Create thread-creators.h through
gperf.

tweak
---
 ChangeLog                 |  16 ++
 lib/thread-creators.gperf |  42 ++++
 lib/thread-optim-proto.c  | 468 ++++++++++++++++++++++++++++++++++++++
 lib/thread-optim.c        | 422 ++++++++++++++++++++++++++++++++++
 lib/thread-optim.h        |  44 ++++
 modules/thread-optim      |  24 ++
 6 files changed, 1016 insertions(+)
 create mode 100644 lib/thread-creators.gperf
 create mode 100644 lib/thread-optim-proto.c
 create mode 100644 lib/thread-optim.c

diff --git a/ChangeLog b/ChangeLog
index 1f2c85cb70..68c125138f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+2026-05-16  Bruno Haible  <[email protected]>
+
+	thread-optim: Port to older glibc, musl libc, *BSD, Solaris, Android.
+	* lib/thread-optim.h: Include <stdbool.h>.
+	(gl_multithreaded): Define differently on ELF platforms.
+	(_GL_MULTITHREADED_ALWAYS_TRUE, _GL_MULTITHREADED_VIA_ELF): New macros.
+	(gl_set_multithreaded): New declaration.
+	* lib/thread-optim.c: New file.
+	* lib/thread-creators.gperf: New file.
+	* lib/thread-optim-proto.c: New file.
+	* modules/thread-optim (Files): Add these files.
+	(Depends-on): Add gperf, bool, stdint-h.
+	(configure.ac): Test for <link.h>, dl_iterate_phdr.
+	(Makefile.am): Compile thread-optim.c. Create thread-creators.h through
+	gperf.
+
 2026-05-13  Paul Eggert  <[email protected]>
 
 	intprops-tests: check TYPE_SIGNED (char)
diff --git a/lib/thread-creators.gperf b/lib/thread-creators.gperf
new file mode 100644
index 0000000000..ea90a0c762
--- /dev/null
+++ b/lib/thread-creators.gperf
@@ -0,0 +1,42 @@
+/* List of libc functions that may create threads.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as
+   published by the Free Software Foundation; either version 2.1 of the
+   License, or (at your option) any later version.
+
+   This file is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+%language=ANSI-C
+%define hash-function-name thread_creators_hash
+%define lookup-function-name thread_creators_lookup
+%readonly-tables
+%global-table
+%define word-array-name thread_creators_symbols
+%pic
+%%
+# POSIX API:
+pthread_create
+# The following functions may create a thread when you pass a 'struct sigevent'
+# with the 'sigev_notify field' set to SIGEV_THREAD.
+aio_read
+aio_read64
+aio_write
+aio_write64
+aio_fsync
+aio_fsync64
+lio_listio
+lio_listio64
+timer_create
+mq_notify
+# ISO C11 API:
+thrd_create
+# glibc API:
+getaddrinfo_a
diff --git a/lib/thread-optim-proto.c b/lib/thread-optim-proto.c
new file mode 100644
index 0000000000..2188862e5b
--- /dev/null
+++ b/lib/thread-optim-proto.c
@@ -0,0 +1,468 @@
+/* Prints the PLT/GOT or .dynsym symbols of all attached shared objects.
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as
+   published by the Free Software Foundation; either version 2.1 of the
+   License, or (at your option) any later version.
+
+   This file is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <[email protected]>, 2026.  */
+
+#define _GNU_SOURCE 1
+
+#include <link.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <elf.h>
+
+/* Old versions of <elf.h> lack these types.  */
+#define Elf32_Relr Elf32_Word
+#define Elf64_Relr Elf64_Xword
+/* Haiku lacks this macro.  */
+#ifndef PF_W
+# define PF_W 0x2
+#endif
+/* NetBSD 9 lacks this macro.  */
+#ifndef DT_GNU_HASH
+# define DT_GNU_HASH 0x6ffffef5
+#endif
+/* Many older platforms lack the definitions of these relocations.  */
+#ifndef R_X86_64_JUMP_SLOT
+# define R_X86_64_JUMP_SLOT 7
+#endif
+#ifndef R_X86_64_IRELATIVE
+# define R_X86_64_IRELATIVE 37
+#endif
+#ifndef R_386_JMP_SLOT
+# define R_386_JMP_SLOT 7
+#endif
+#ifndef R_AARCH64_JUMP_SLOT
+# define R_AARCH64_JUMP_SLOT 1026
+#endif
+#ifndef R_ARM_JUMP_SLOT
+# define R_ARM_JUMP_SLOT 22
+#endif
+#ifndef R_ALPHA_JMP_SLOT
+# define R_ALPHA_JMP_SLOT 26
+#endif
+#ifndef R_PARISC_IPLT
+# define R_PARISC_IPLT 129
+#endif
+#ifndef R_68K_JMP_SLOT
+# define R_68K_JMP_SLOT 21
+#endif
+#ifndef R_LARCH_JUMP_SLOT
+# define R_LARCH_JUMP_SLOT 5
+#endif
+#ifndef R_PPC_JMP_SLOT
+# define R_PPC_JMP_SLOT 21
+#endif
+#ifndef R_RISCV_JUMP_SLOT
+# define R_RISCV_JUMP_SLOT 5
+#endif
+#ifndef R_390_JMP_SLOT
+# define R_390_JMP_SLOT 11
+#endif
+#ifndef R_SH_JMP_SLOT
+# define R_SH_JMP_SLOT 164
+#endif
+#ifndef R_SPARC_JMP_SLOT
+# define R_SPARC_JMP_SLOT 21
+#endif
+
+/* Definition of __ELF_NATIVE_CLASS.  */
+#ifndef __ELF_NATIVE_CLASS
+/* We can't rely on PTRDIFF_WIDTH from <stdint.h> here.  */
+# if defined _LP64 || defined __LP64__ \
+     || ((defined __x86_64__ || defined _M_X64) && !(defined __ILP32__ || defined _ILP32)) \
+     || defined __aarch64__ /* arm64 */ \
+     || defined __alpha \
+     || (defined __ia64__ && !defined _ILP32) \
+     || (defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)) /* mips64 */ \
+     || defined __loongarch__ \
+     || defined __powerpc64__ \
+     || (defined __riscv && __riscv_xlen == 64) /* riscv64 */ \
+     || defined __s390x__ \
+     || (defined __sparcv9 || defined __arch64__) /* sparc64 */
+#  define __ELF_NATIVE_CLASS 64
+# else
+#  define __ELF_NATIVE_CLASS 32
+# endif
+#endif
+
+/* Macros that use __ELF_NATIVE_CLASS.  */
+/* Macros for decomposing an r_info field into symbol index and reloc type.
+   Most platforms (except FreeBSD) lack them.  */
+#if !(defined ELF_R_SYM && defined ELF_R_TYPE)
+# if __ELF_NATIVE_CLASS == 32
+#  define ELF_R_SYM ELF32_R_SYM
+#  define ELF_R_TYPE ELF32_R_TYPE
+# elif __ELF_NATIVE_CLASS == 64
+#  define ELF_R_SYM ELF64_R_SYM
+#  define ELF_R_TYPE ELF64_R_TYPE
+# endif
+#endif
+/* OpenBSD lacks this macro.  */
+#ifndef ElfW
+# define ElfW(type) ElfW_1 (__ELF_NATIVE_CLASS, type)
+# define ElfW_1(class,type) ElfW_2 (class, type)
+# define ElfW_2(class,type) Elf ## class ## _ ## type
+#endif
+
+static int
+inspect_one_GOT (struct dl_phdr_info *info, size_t size, void *data)
+{
+  if (info->dlpi_name[0] == '\0')
+    printf ("executable: ");
+  else
+    printf ("library %s: ", info->dlpi_name);
+
+  printf ("size = %zu\n", size);
+
+  ElfW(Addr) base_address = info->dlpi_addr;
+  printf ("base_address = 0x%zx\n", base_address);
+
+  size_t num_headers = info->dlpi_phnum;
+  const ElfW(Phdr) *headers = info->dlpi_phdr;
+  printf ("num_headers = %zu\n", num_headers);
+
+  const ElfW(Dyn) *dynamic_section = NULL;
+  ElfW(Word) dynamic_flags = 0;
+  for (size_t h = 0; h < num_headers; h++)
+    {
+      const ElfW(Phdr) *header = headers + h;
+      if (header->p_type == PT_DYNAMIC)
+        {
+#if 0
+          printf ("Header %zu:\n", h);
+          printf ("  type = %zu\n", (size_t) header->p_type);
+          printf ("  flags = 0x%zx\n", (size_t) header->p_flags);
+          printf ("  offset = 0x%zx\n", (size_t) header->p_offset);
+          printf ("  vaddr = 0x%zx\n", (size_t) header->p_vaddr);
+          printf ("  paddr = 0x%zx\n", (size_t) header->p_paddr);
+          printf ("  filesz = 0x%zx\n", (size_t) header->p_filesz);
+          printf ("  memsz = 0x%zx\n", (size_t) header->p_memsz);
+          printf ("  align = 0x%zx\n", (size_t) header->p_align);
+#endif
+          dynamic_section = (const ElfW(Dyn) *) (base_address + header->p_vaddr);
+          dynamic_flags = header->p_flags;
+          break;
+        }
+    }
+  printf ("Dynamic section = 0x%zx\n", (ElfW(Addr)) dynamic_section);
+  if (dynamic_section != NULL)
+    {
+      ElfW(Addr) maybe_base_address;
+#if __GLIBC__ >= 2 && !(defined __mips__ || defined __riscv)
+      /* linux-vdso.so.1 is weird.  */
+      if (dynamic_flags & PF_W)
+        maybe_base_address = 0;
+      else
+#endif
+        maybe_base_address = base_address;
+
+      /* Get the string table.  */
+      const char *strtab = NULL;
+      size_t strsz = 0;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_STRTAB)
+            strtab = (const char *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+          if (dynamic_section_entry->d_tag == DT_STRSZ)
+            strsz = dynamic_section_entry->d_un.d_val;
+        }
+
+      /* Get the symbol table.  */
+      const ElfW(Sym) *symtab = NULL;
+      size_t symentsz = 0;
+      const unsigned int *hash = NULL;
+      const unsigned int *gnu_hash = NULL;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_SYMTAB)
+            symtab = (const ElfW(Sym) *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+          if (dynamic_section_entry->d_tag == DT_SYMENT)
+            symentsz = dynamic_section_entry->d_un.d_val;
+          if (dynamic_section_entry->d_tag == DT_HASH)
+            hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+          if (dynamic_section_entry->d_tag == DT_GNU_HASH)
+            gnu_hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+        }
+      printf ("strtab = 0x%zx, total size strsz = %zu\n", (ElfW(Addr)) strtab, strsz);
+      printf ("symtab = 0x%zx, each element of size symentsz = %zu\n", (ElfW(Addr)) symtab, symentsz);
+      /* Check the symentsz.  */
+      if (!(symentsz == sizeof (ElfW(Sym))))
+        /* Invalid symentsz.
+           Should not happen with properly formed ELF files.  */
+        abort ();
+      /* How many symbols in symtab?
+         The old method uses DT_HASH, newer binaries use DT_GNU_HASH.  */
+      size_t num_symtab_entries = (size_t)-1;
+      if (hash != NULL)
+        {
+          /* The DT_HASH pointer points to
+               struct elf_hash_table
+               {
+                 uint32_t nbucket;
+                 uint32_t nchain;
+                 uint32_t bucket[nbucket];
+                 uint32_t chain[nchain];
+               }
+             where nchain is the number of entries in the symbol table.
+             See <https://flapenguin.me/elf-dt-hash>.  */
+          num_symtab_entries = hash[1];
+          printf ("num_symtab_entries according to HASH = %zu\n", num_symtab_entries);
+          /* On Linux/s390x this value is too small.  The one from GNU_HASH is correct.  */
+        }
+      if (gnu_hash != NULL)
+        {
+          /* The DT_GNU_HASH pointer points to
+               struct gnu_hash_table
+               {
+                 uint32_t nbuckets;
+                 uint32_t symoffset;
+                 uint32_t bloom_size;
+                 uint32_t bloom_shift;
+                 ElfW(Word) bloom[bloom_size];
+                 uint32_t buckets[nbuckets];
+                 uint32_t chain[];
+               }
+             See <https://flapenguin.me/elf-dt-gnu-hash> and
+             binutils/bfd/elflink.c:bfd_elf_size_dynsym_hash_dynstr().  */
+          unsigned int nbuckets = gnu_hash[0];
+          unsigned int symoffset = gnu_hash[1];
+          unsigned int bloom_size = gnu_hash[2];
+          const unsigned int *buckets_start = &gnu_hash[4 + bloom_size * (__ELF_NATIVE_CLASS / 32)];
+          size_t gnu_num_symtab_entries;
+          if (nbuckets == 1 && symoffset == 1 && bloom_size == 1 && gnu_hash[3] == 0
+              && buckets_start[0] == 0)
+            gnu_num_symtab_entries = 0;
+          else
+            {
+              const unsigned int *chain_start = &buckets_start[nbuckets];
+              const unsigned int *chain_p = chain_start;
+              for (unsigned int bucket = 0; bucket < nbuckets; bucket++)
+                if (buckets_start[bucket] != 0)
+                  {
+                    while ((*chain_p & 1) == 0)
+                      chain_p++;
+                    chain_p++;
+                  }
+              gnu_num_symtab_entries = symoffset + (chain_p - chain_start);
+            }
+          printf ("num_symtab_entries according to GNU_HASH = %zu\n", gnu_num_symtab_entries);
+          if (gnu_num_symtab_entries != 0)
+            num_symtab_entries = gnu_num_symtab_entries;
+        }
+
+      /* List the symbols in the .dynsym section.
+         $ readelf -a -X a.out  */
+      if (num_symtab_entries > 0 && num_symtab_entries < (size_t)-1)
+        {
+          printf ("Symbols in the .dynsym section:\n");
+          for (size_t symbol_index = 1; symbol_index < num_symtab_entries; symbol_index++)
+            {
+              const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+              if (symtab_entry->st_shndx == SHN_UNDEF)
+                {
+                  ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+                  const char *symbol_name = strtab + symbol_name_offset;
+                  printf ("Symtab[%zu]: %s\n", symbol_index, symbol_name);
+                }
+            }
+        }
+
+#if defined __mips__ /* MIPS does not have the usual GOT structure */
+
+#if 0 /* works but not useful */
+
+      const void *got_entries = NULL;
+      size_t num_got_entries = 0;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_PLTGOT)
+            {
+              printf ("PLTGOT value: 0x%zx\n", dynamic_section_entry->d_un.d_ptr);
+              got_entries = (const void *) dynamic_section_entry->d_un.d_ptr;
+            }
+          if (dynamic_section_entry->d_tag == DT_MIPS_LOCAL_GOTNO)
+            {
+              printf ("DT_MIPS_LOCAL_GOTNO value: 0x%zx\n", dynamic_section_entry->d_un.d_val);
+              num_got_entries = dynamic_section_entry->d_un.d_val;
+            }
+        }
+
+#endif
+
+#else
+
+      int jump_relocation_type = 0;
+      const void *jump_relocations = NULL;
+      size_t jump_relocations_size = 0;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_PLTREL)
+            {
+              printf ("PLTREL value: 0x%zx", dynamic_section_entry->d_un.d_val);
+#if defined __x86_64__ || defined __aarch64__ || defined __alpha || defined __hppa || defined __m68k__ || defined _ARCH_PPC || defined __riscv || defined __s390__ || defined __sh__ || defined __sparc
+              if (dynamic_section_entry->d_un.d_val == DT_RELA)
+                printf (" = DT_RELA");
+#elif defined __i386__ || defined __arm__
+              if (dynamic_section_entry->d_un.d_val == DT_REL)
+                printf (" = DT_REL");
+#endif
+              jump_relocation_type = dynamic_section_entry->d_un.d_val;
+              printf ("\n");
+            }
+          else if (dynamic_section_entry->d_tag == DT_PLTRELSZ)
+            {
+              printf ("PLTRELSZ value: 0x%zx\n", dynamic_section_entry->d_un.d_val);
+              jump_relocations_size = dynamic_section_entry->d_un.d_val;
+            }
+          else if (dynamic_section_entry->d_tag == DT_JMPREL)
+            {
+              printf ("JMPREL value: 0x%zx\n", dynamic_section_entry->d_un.d_ptr);
+#if __GLIBC__ >= 2 && !defined __riscv
+              jump_relocations = (const void *) dynamic_section_entry->d_un.d_ptr;
+#else /* glibc/riscv, musl libc, FreeBSD, NetBSD, OpenBSD, Solaris, Haiku, Android */
+              jump_relocations = (const void *) (base_address + dynamic_section_entry->d_un.d_ptr);
+#endif
+            }
+          else if (dynamic_section_entry->d_tag == DT_PLTGOT)
+            {
+              /* This points to the GOT. It contains the real function addresses,
+                 but no symbolic information.  */
+              printf ("PLTGOT value: 0x%zx\n", dynamic_section_entry->d_un.d_ptr);
+            }
+        }
+      if (jump_relocations != NULL)
+        {
+          if (!(jump_relocation_type == DT_REL || jump_relocation_type == DT_RELA))
+            /* No valid jump_relocation_type.
+               Should not happen with properly formed ELF files.  */
+            abort ();
+
+          size_t num_jump_relocations =
+            jump_relocations_size
+            / (jump_relocation_type == DT_RELA ? sizeof (ElfW(Rela)) : sizeof (ElfW(Rel)));
+
+          for (size_t j = 0; j < num_jump_relocations; j++)
+            {
+              ElfW(Addr) got_element;
+              ElfW(Relr) r_info;
+              if (jump_relocation_type == DT_RELA)
+                {
+                  const ElfW(Rela) *jump_reloc = (const ElfW(Rela) *) jump_relocations + j;
+                  got_element = base_address + jump_reloc->r_offset;
+                  r_info = jump_reloc->r_info;
+                }
+              else /* jump_relocation_type == DT_REL */
+                {
+                  const ElfW(Rel) *jump_reloc = (const ElfW(Rel) *) jump_relocations + j;
+                  got_element = base_address + jump_reloc->r_offset;
+                  r_info = jump_reloc->r_info;
+                }
+              unsigned int symbol_index = ELF_R_SYM (r_info);
+              unsigned int reloc_type = ELF_R_TYPE (r_info);
+              (void) reloc_type;
+              /* We can ignore relocations not connected to a symbol.  */
+              if (symbol_index != 0)
+                {
+                  if (!(symbol_index < num_symtab_entries))
+                    /* Out-of-range reference to the symtab.
+                       Should not happen with properly formed ELF files.  */
+                    abort ();
+                  const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+                  ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+                  if (!(symbol_name_offset < strsz))
+                    /* Out-of-range reference to the strtab.
+                       Should not happen with properly formed ELF files.  */
+                    abort ();
+                  const char *symbol_name = strtab + symbol_name_offset;
+                  printf ("Jump reloc: address = 0x%zx, reloc_type = 0x%x",
+                          got_element, reloc_type);
+#if defined __x86_64__
+                  if (reloc_type == R_X86_64_JUMP_SLOT)
+                    printf (" = R_X86_64_JUMP_SLOT");
+                  else if (reloc_type == R_X86_64_IRELATIVE)
+                    printf (" = R_X86_64_IRELATIVE");
+#elif defined __i386__
+                  if (reloc_type == R_386_JMP_SLOT)
+                    printf (" = R_386_JMP_SLOT");
+#elif defined __aarch64__
+                  if (reloc_type == R_AARCH64_JUMP_SLOT)
+                    printf (" = R_AARCH64_JUMP_SLOT");
+#elif defined __arm__
+                  if (reloc_type == R_ARM_JUMP_SLOT)
+                    printf (" = R_ARM_JUMP_SLOT");
+#elif defined __alpha
+                  if (reloc_type == R_ALPHA_JMP_SLOT)
+                    printf (" = R_ALPHA_JMP_SLOT");
+#elif defined __hppa
+                  if (reloc_type == R_PARISC_IPLT)
+                    printf (" = R_PARISC_IPLT");
+#elif defined __m68k__
+                  if (reloc_type == R_68K_JMP_SLOT)
+                    printf (" = R_68K_JMP_SLOT");
+#elif defined __loongarch__
+                  if (reloc_type == R_LARCH_JUMP_SLOT)
+                    printf (" = R_LARCH_JUMP_SLOT");
+#elif defined _ARCH_PPC
+                  if (reloc_type == R_PPC_JMP_SLOT)
+                    printf (" = R_PPC_JMP_SLOT");
+#elif defined __riscv
+                  if (reloc_type == R_RISCV_JUMP_SLOT)
+                    printf (" = R_RISCV_JUMP_SLOT");
+#elif defined __s390__
+                  if (reloc_type == R_390_JMP_SLOT)
+                    printf (" = R_390_JMP_SLOT");
+#elif defined __sh__
+                  if (reloc_type == R_SH_JMP_SLOT)
+                    printf (" = R_SH_JMP_SLOT");
+#elif defined __sparc
+                  if (reloc_type == R_SPARC_JMP_SLOT)
+                    printf (" = R_SPARC_JMP_SLOT");
+#endif
+                  printf (", symbol_index = 0x%x = %s",
+                          symbol_index, symbol_name);
+                  printf ("\n");
+                }
+            }
+        }
+
+#endif
+    }
+  printf ("\n");
+
+  return 0;
+}
+
+int
+main ()
+{
+  /* For debugging: Prevent fully buffered mode of stdout.  */
+  setvbuf (stdout, NULL, _IOLBF, 0);
+
+  dl_iterate_phdr (inspect_one_GOT, NULL);
+
+  return 0;
+}
diff --git a/lib/thread-optim.c b/lib/thread-optim.c
new file mode 100644
index 0000000000..88812307ed
--- /dev/null
+++ b/lib/thread-optim.c
@@ -0,0 +1,422 @@
+/* Optimization of multithreaded code.
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as
+   published by the Free Software Foundation; either version 2.1 of the
+   License, or (at your option) any later version.
+
+   This file is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <[email protected]>, 2026.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "thread-optim.h"
+
+static int mt_override = -1;
+
+void
+gl_set_multithreaded (bool mt)
+{
+  mt_override = (mt ? 1 : 0);
+}
+
+#ifdef _GL_MULTITHREADED_VIA_ELF
+
+/* Three possible approaches come to mind for automatically determining
+   whether a process is single-threaded:
+
+     * Look at the number of entries in the /proc/self/task/ directory.
+       If there is only one, the process is (currently) single-threaded.
+       Drawbacks:
+         - It works only on Linux.
+         - It is relatively expensive (several system calls).
+
+     * Look at the state of the PLT and GOT entries for the functions
+       'pthread_create' and 'thrd_create'.  If they are both still in the
+       initial state, neither of them has been called so far, and the
+       process therefore is (currently) single-threaded.
+       Drawbacks:
+         - It does not work on modern Linux distros, because they link
+           with options '-z now -z relro' ('-z relro' for security
+           reasons, and '-z now' because it it required for '-z now')
+           and '-z now' activates early binding instead of lazy binding
+           in the PLT and GOT.
+         - The code for doing this is architecture dependent (because
+           the instructions in the PLT are obviously arch dependent)
+           and OS dependent (because OpenBSD has a different structure
+           for the PLT than the other OSes).  Additionally it requires
+           inline assembly in order to get the value of the
+           'pthread_create@plt' and 'thrd_create@plt' symbols.
+
+     * Look at whether the symbols 'pthread_create' and 'thrd_create'
+       occur in the PLT and GOT.  If neither of them occurs (in the
+       executable and in the linked shared libraries), the process
+       cannot create threads.
+       Drawbacks:
+         - This approach works only under the following assumptions:
+           . These two entry points are the only facilities in libc
+             that create threads.
+           . The process will not invoke dlopen() to attach other
+             shared libraries.
+           . The program is not statically linked.
+
+   Here we implement the third approach.  We mitigate the drawback
+   by allowing the programmer to override the result if the assumptions
+   are not met.  */
+
+#include <link.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <elf.h>
+
+/* Old versions of <elf.h> lack these types.  */
+#define Elf32_Relr Elf32_Word
+#define Elf64_Relr Elf64_Xword
+/* Haiku lacks this macro.  */
+#ifndef PF_W
+# define PF_W 0x2
+#endif
+/* NetBSD 9 lacks this macro.  */
+#ifndef DT_GNU_HASH
+# define DT_GNU_HASH 0x6ffffef5
+#endif
+
+/* Definition of __ELF_NATIVE_CLASS.  */
+#ifndef __ELF_NATIVE_CLASS
+# if PTRDIFF_WIDTH == 64
+#  define __ELF_NATIVE_CLASS 64
+# else
+#  define __ELF_NATIVE_CLASS 32
+# endif
+#endif
+
+/* Macros that use __ELF_NATIVE_CLASS.  */
+/* Macros for decomposing an r_info field into symbol index and reloc type.
+   Most platforms (except FreeBSD) lack them.  */
+#if !(defined ELF_R_SYM && defined ELF_R_TYPE)
+# if __ELF_NATIVE_CLASS == 32
+#  define ELF_R_SYM ELF32_R_SYM
+#  define ELF_R_TYPE ELF32_R_TYPE
+# elif __ELF_NATIVE_CLASS == 64
+#  define ELF_R_SYM ELF64_R_SYM
+#  define ELF_R_TYPE ELF64_R_TYPE
+# endif
+#endif
+/* OpenBSD lacks this macro.  */
+#ifndef ElfW
+# define ElfW(type) ElfW_1 (__ELF_NATIVE_CLASS, type)
+# define ElfW_1(class,type) ElfW_2 (class, type)
+# define ElfW_2(class,type) Elf ## class ## _ ## type
+#endif
+
+#include "thread-creators.h"
+
+/* We inspect the GOT.  This is more robust than inspecting the PLT, because
+     - When the executable is built with option '-fno-plt', there is no PLT,
+       only a GOT.
+     - The ELF data structures accessible from dl_iterate_phdr() don't
+       actually contain a pointer to the PLT.  In order to recognize the PLT,
+       we would have to look for architecture specific relocations.  */
+
+/* Inspects the GOT of a single ELF file in memory.
+   Returns 1 if a thread creator symbol is seen,
+           -1 if the executable is statically linked, or
+           0 otherwise.  */
+static int
+inspect_one_GOT (struct dl_phdr_info *info, size_t size, void *data)
+{
+  /* IMPORTANT: Before changing this code, make the corresponding changes
+     to thread-optim-proto.c and test them.  */
+
+  /* Skip the processing of libc.so.
+     * On specific platforms, this is necessary:
+         - FreeBSD 12/arm64 has lio_listio in the GOT of libc.so.
+         - NetBSD 10 has timer_create in the GOT of libc.so.
+         - Android has pthread_create in the GOT of libc.so.
+       This does not make the process multithreaded.
+     * On the other platforms, it is just an optimization, to not scan
+       the (possibly many) symbols of libc.so.  */
+  {
+    const char *filename = info->dlpi_name;
+    const char *last_slash = strrchr (filename, '/');
+    if (last_slash != NULL
+        && strncmp (last_slash + 1, "libc.so", 7) == 0
+        && (last_slash[1 + 7] == '\0' || last_slash[1 + 7] == '.'))
+      return 0;
+  }
+
+  ElfW(Addr) base_address = info->dlpi_addr;
+
+  size_t num_headers = info->dlpi_phnum;
+  const ElfW(Phdr) *headers = info->dlpi_phdr;
+
+  const ElfW(Dyn) *dynamic_section = NULL;
+  ElfW(Word) dynamic_flags _GL_ATTRIBUTE_MAYBE_UNUSED = 0;
+  for (size_t h = 0; h < num_headers; h++)
+    {
+      const ElfW(Phdr) *header = headers + h;
+      if (header->p_type == PT_DYNAMIC)
+        {
+          dynamic_section = (const ElfW(Dyn) *) (base_address + header->p_vaddr);
+          dynamic_flags = header->p_flags;
+          break;
+        }
+    }
+  if (dynamic_section != NULL)
+    {
+      ElfW(Addr) maybe_base_address;
+#if __GLIBC__ >= 2 && !(defined __mips__ || defined __riscv)
+      /* linux-vdso.so.1 is weird.  */
+      if (dynamic_flags & PF_W)
+        maybe_base_address = 0;
+      else
+#endif
+        maybe_base_address = base_address;
+
+      /* Get the string table.  */
+      const char *strtab = NULL;
+      size_t strsz = 0;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_STRTAB)
+            strtab = (const char *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+          if (dynamic_section_entry->d_tag == DT_STRSZ)
+            strsz = dynamic_section_entry->d_un.d_val;
+        }
+
+      /* Get the symbol table.  */
+      const ElfW(Sym) *symtab = NULL;
+      size_t symentsz = 0;
+      const unsigned int *hash = NULL;
+      const unsigned int *gnu_hash = NULL;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_SYMTAB)
+            symtab = (const ElfW(Sym) *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+          if (dynamic_section_entry->d_tag == DT_SYMENT)
+            symentsz = dynamic_section_entry->d_un.d_val;
+          if (dynamic_section_entry->d_tag == DT_HASH)
+            hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+          if (dynamic_section_entry->d_tag == DT_GNU_HASH)
+            gnu_hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+        }
+      if (!(symentsz == sizeof (ElfW(Sym))))
+        /* Invalid symentsz.
+           Should not happen with properly formed ELF files.  */
+        abort ();
+      size_t num_symtab_entries = (size_t)-1;
+      if (hash != NULL)
+        {
+          /* The DT_HASH pointer points to
+               struct elf_hash_table
+               {
+                 uint32_t nbucket;
+                 uint32_t nchain;
+                 uint32_t bucket[nbucket];
+                 uint32_t chain[nchain];
+               }
+             where nchain is the number of entries in the symbol table.
+             See <https://flapenguin.me/elf-dt-hash>.  */
+          num_symtab_entries = hash[1];
+        }
+      if (gnu_hash != NULL)
+        {
+          /* The DT_GNU_HASH pointer points to
+               struct gnu_hash_table
+               {
+                 uint32_t nbuckets;
+                 uint32_t symoffset;
+                 uint32_t bloom_size;
+                 uint32_t bloom_shift;
+                 ElfW(Word) bloom[bloom_size];
+                 uint32_t buckets[nbuckets];
+                 uint32_t chain[];
+               }
+             See <https://flapenguin.me/elf-dt-gnu-hash> and
+             binutils/bfd/elflink.c:bfd_elf_size_dynsym_hash_dynstr().  */
+          unsigned int nbuckets = gnu_hash[0];
+          unsigned int symoffset = gnu_hash[1];
+          unsigned int bloom_size = gnu_hash[2];
+          const unsigned int *buckets_start = &gnu_hash[4 + bloom_size * (__ELF_NATIVE_CLASS / 32)];
+          size_t gnu_num_symtab_entries;
+          if (nbuckets == 1 && symoffset == 1 && bloom_size == 1 && gnu_hash[3] == 0
+              && buckets_start[0] == 0)
+            gnu_num_symtab_entries = 0;
+          else
+            {
+              const unsigned int *chain_start = &buckets_start[nbuckets];
+              const unsigned int *chain_p = chain_start;
+              for (unsigned int bucket = 0; bucket < nbuckets; bucket++)
+                if (buckets_start[bucket] != 0)
+                  {
+                    while ((*chain_p & 1) == 0)
+                      chain_p++;
+                    chain_p++;
+                  }
+              gnu_num_symtab_entries = symoffset + (chain_p - chain_start);
+            }
+          if (gnu_num_symtab_entries != 0)
+            num_symtab_entries = gnu_num_symtab_entries;
+        }
+
+# if defined __mips__
+
+      /* MIPS does not have the usual GOT structure.
+         Instead, inspect the symbols in the .dynsym section.  This happens
+         to be the same set of symbols as the symbols in the symtab marked as
+         UNDEF.  This is a somewhat larger set of symbols, but still good
+         enough.  */
+      for (size_t symbol_index = 1; symbol_index < num_symtab_entries; symbol_index++)
+        {
+          const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+          if (symtab_entry->st_shndx == SHN_UNDEF)
+            {
+              ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+              const char *symbol_name = strtab + symbol_name_offset;
+              if (thread_creators_lookup (symbol_name, strlen (symbol_name)) != NULL)
+                /* Found a jump relocation to a thread creator symbol.  */
+                return 1;
+            }
+        }
+
+#else
+
+      int jump_relocation_type = 0;
+      const void *jump_relocations = NULL;
+      size_t jump_relocations_size = 0;
+      for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+           dynamic_section_entry->d_tag != DT_NULL;
+           dynamic_section_entry++)
+        {
+          if (dynamic_section_entry->d_tag == DT_PLTREL)
+            {
+              /* The value should be DT_REL or DT_RELA.  */
+              jump_relocation_type = dynamic_section_entry->d_un.d_val;
+            }
+          else if (dynamic_section_entry->d_tag == DT_PLTRELSZ)
+            {
+              jump_relocations_size = dynamic_section_entry->d_un.d_val;
+            }
+          else if (dynamic_section_entry->d_tag == DT_JMPREL)
+            {
+# if __GLIBC__ >= 2 && !defined __riscv
+              jump_relocations = (const void *) dynamic_section_entry->d_un.d_ptr;
+# else /* glibc/riscv, musl libc, FreeBSD, NetBSD, OpenBSD, Solaris, Haiku, Android */
+              jump_relocations = (const void *) (base_address + dynamic_section_entry->d_un.d_ptr);
+# endif
+            }
+          /* The entry with tag DT_PLTGOT contains a pointer to the GOT
+             in memory.  But we don't need it actually.  */
+        }
+      if (jump_relocations != NULL)
+        {
+          if (!(jump_relocation_type == DT_REL || jump_relocation_type == DT_RELA))
+            /* No valid jump_relocation_type.
+               Should not happen with properly formed ELF files.  */
+            abort ();
+
+          size_t num_jump_relocations =
+            jump_relocations_size
+            / (jump_relocation_type == DT_RELA ? sizeof (ElfW(Rela)) : sizeof (ElfW(Rel)));
+
+          for (size_t j = 0; j < num_jump_relocations; j++)
+            {
+              ElfW(Addr) got_element _GL_ATTRIBUTE_MAYBE_UNUSED;
+              ElfW(Relr) r_info;
+              if (jump_relocation_type == DT_RELA)
+                {
+                  const ElfW(Rela) *jump_reloc = (const ElfW(Rela) *) jump_relocations + j;
+                  got_element = base_address + jump_reloc->r_offset;
+                  r_info = jump_reloc->r_info;
+                }
+              else /* jump_relocation_type == DT_REL */
+                {
+                  const ElfW(Rel) *jump_reloc = (const ElfW(Rel) *) jump_relocations + j;
+                  got_element = base_address + jump_reloc->r_offset;
+                  r_info = jump_reloc->r_info;
+                }
+              unsigned int symbol_index = ELF_R_SYM (r_info);
+              unsigned int reloc_type = ELF_R_TYPE (r_info);
+              (void) reloc_type;
+              /* We can ignore relocations not connected to a symbol.  */
+              if (symbol_index != 0)
+                {
+                  if (!(symbol_index < num_symtab_entries))
+                    /* Out-of-range reference to the symtab.
+                       Should not happen with properly formed ELF files.  */
+                    abort ();
+                  const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+                  ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+                  if (!(symbol_name_offset < strsz))
+                    /* Out-of-range reference to the strtab.
+                       Should not happen with properly formed ELF files.  */
+                    abort ();
+                  const char *symbol_name = strtab + symbol_name_offset;
+                  if (thread_creators_lookup (symbol_name, strlen (symbol_name)) != NULL)
+                    /* Found a jump relocation to a thread creator symbol.  */
+                    return 1;
+                }
+            }
+        }
+
+#endif
+    }
+  else
+    {
+      if (info->dlpi_name[0] == '\0')
+        /* The executable is statically linked.  */
+        return -1;
+    }
+
+  return 0;
+}
+
+/* Returns true if the process is possibly multithreaded throughout its
+   lifetime (assuming the process will not invoke dlopen() to attach other
+   shared libraries).  */
+static bool
+is_multithreaded_uncached (void)
+{
+  int result = dl_iterate_phdr (inspect_one_GOT, NULL);
+  /* result is 1 if a thread creator symbol is seen in some of the GOTs,
+               -1 if the executable is statically linked, or
+               0 otherwise.  */
+  return result != 0;
+}
+
+bool
+gl_multithreaded (void)
+{
+  /* Consider the override.  */
+  if (mt_override >= 0)
+    return mt_override;
+  else
+    {
+      /* Cache the result from is_multithreaded_uncached.  */
+      static int volatile cached_result = -1;
+
+      if (cached_result < 0)
+        cached_result = is_multithreaded_uncached ();
+
+      return cached_result;
+    }
+}
+
+#endif /* _GL_MULTITHREADED_VIA_ELF */
diff --git a/lib/thread-optim.h b/lib/thread-optim.h
index 104fd90fc9..e4c0e03f5e 100644
--- a/lib/thread-optim.h
+++ b/lib/thread-optim.h
@@ -55,11 +55,55 @@
  #error "Please include config.h first."
 #endif
 
+#include <stdbool.h>
+
 #if HAVE_SYS_SINGLE_THREADED_H /* glibc >= 2.32 */
+
 # include <sys/single_threaded.h>
 # define gl_multithreaded()  (!__libc_single_threaded)
+
+#elif defined __APPLE__ && defined __MACH__ /* macOS */
+
+/* One of the most basic libraries on macOS, CoreFoundation,
+   creates new threads without being asked for.  Therefore,
+   assume that the process might be multithreaded.  */
+# define gl_multithreaded()  1
+# define _GL_MULTITHREADED_ALWAYS_TRUE
+
+#elif defined __ELF__ /* Linux, Hurd, FreeBSD, NetBSD, OpenBSD, Solaris, ... */\
+      && HAVE_LINK_H \
+      && HAVE_DL_ITERATE_PHDR /* not Solaris 10, not Haiku */
+
+/* We can detect whether pthread_create() can be invoked, by looking
+   at the contents of the PLT of the executable and of each shared object.  */
+# ifdef __cplusplus
+extern "C" {
+# endif
+extern bool gl_multithreaded (void);
+# define _GL_MULTITHREADED_VIA_ELF
+# ifdef __cplusplus
+}
+# endif
+
 #else
+
+/* On other platforms, such as Windows, threads are created for relatively
+   simple tasks, such as events, alarms, or interruptible sleep.  Therefore,
+   assume that the process might be multithreaded.  */
 # define gl_multithreaded()  1
+# define _GL_MULTITHREADED_ALWAYS_TRUE
+
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Allows programs to override the result of gl_multithreaded().  */
+extern void gl_set_multithreaded (bool mt);
+
+#ifdef __cplusplus
+}
 #endif
 
 #endif /* _THREAD_OPTIM_H */
diff --git a/modules/thread-optim b/modules/thread-optim
index caf45189c4..5acbbe5855 100644
--- a/modules/thread-optim
+++ b/modules/thread-optim
@@ -3,13 +3,37 @@ Optimization of multithreaded code.
 
 Files:
 lib/thread-optim.h
+lib/thread-optim.c
+lib/thread-creators.gperf
+lib/thread-optim-proto.c
 
 Depends-on:
+gperf
+bool
+stdint-h
 
 configure.ac:
 AC_CHECK_HEADERS([sys/single_threaded.h])
+AC_CHECK_HEADERS([link.h])
+dnl Solaris 10 has dl_iterate_phdr in a separate library libdl.
+dnl Haiku has dl_iterate_phdr in a separate library libbsd.
+dnl We don't want a link dependency here.
+AC_CHECK_FUNCS([dl_iterate_phdr])
 
 Makefile.am:
+lib_SOURCES += thread-optim.c
+
+$(srcdir)/thread-creators.h: $(srcdir)/thread-creators.gperf
+	$(V_GPERF)$(GPERF) -m 10 $(srcdir)/thread-creators.gperf > $(srcdir)/thread-creators.h-t1 \
+	&& sed -e 's/^const/static const/' \
+	       -e 's|\([ "]\)[^ "]*/thread-creators\.gperf\([ "]\)|\1thread-creators.gperf\2|' \
+	       < $(srcdir)/thread-creators.h-t1 > $(srcdir)/thread-creators.h-t2 \
+	&& rm -f $(srcdir)/thread-creators.h-t1 \
+	&& mv $(srcdir)/thread-creators.h-t2 $(srcdir)/thread-creators.h
+BUILT_SOURCES        += thread-creators.h
+MOSTLYCLEANFILES     += thread-creators.h-t1 thread-creators.h-t2
+MAINTAINERCLEANFILES += thread-creators.h
+EXTRA_DIST           += thread-creators.h
 
 Include:
 "thread-optim.h"
-- 
2.54.0

>From d9e15740b500990c96623285a8c26a92e0c6da0f Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Sat, 16 May 2026 12:42:39 +0200
Subject: [PATCH 2/2] thread-optim: Add tests.

* tests/test-thread-optim1.c: New file.
* tests/test-thread-optim2.c: New file.
* modules/thread-optim-tests: New file.
---
 ChangeLog                  |  5 ++++
 modules/thread-optim-tests | 14 ++++++++++
 tests/test-thread-optim1.c | 36 ++++++++++++++++++++++++++
 tests/test-thread-optim2.c | 52 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 107 insertions(+)
 create mode 100644 modules/thread-optim-tests
 create mode 100644 tests/test-thread-optim1.c
 create mode 100644 tests/test-thread-optim2.c

diff --git a/ChangeLog b/ChangeLog
index 68c125138f..c51d480e87 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2026-05-16  Bruno Haible  <[email protected]>
 
+	thread-optim: Add tests.
+	* tests/test-thread-optim1.c: New file.
+	* tests/test-thread-optim2.c: New file.
+	* modules/thread-optim-tests: New file.
+
 	thread-optim: Port to older glibc, musl libc, *BSD, Solaris, Android.
 	* lib/thread-optim.h: Include <stdbool.h>.
 	(gl_multithreaded): Define differently on ELF platforms.
diff --git a/modules/thread-optim-tests b/modules/thread-optim-tests
new file mode 100644
index 0000000000..0de5bfa8b3
--- /dev/null
+++ b/modules/thread-optim-tests
@@ -0,0 +1,14 @@
+Files:
+tests/test-thread-optim1.c
+tests/test-thread-optim2.c
+tests/macros.h
+
+Depends-on:
+thread
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-thread-optim1 test-thread-optim2
+check_PROGRAMS += test-thread-optim1 test-thread-optim2
+test_thread_optim2_LDADD = $(LDADD) @LIBMULTITHREAD@
diff --git a/tests/test-thread-optim1.c b/tests/test-thread-optim1.c
new file mode 100644
index 0000000000..3fc33a8ec2
--- /dev/null
+++ b/tests/test-thread-optim1.c
@@ -0,0 +1,36 @@
+/* Test of optimization of multithreaded code..
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <[email protected]>, 2026.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "thread-optim.h"
+
+#include "macros.h"
+
+int
+main ()
+{
+#ifdef _GL_MULTITHREADED_ALWAYS_TRUE
+  ASSERT (gl_multithreaded ());
+#else
+  ASSERT (! gl_multithreaded ());
+#endif
+
+  return test_exit_status;
+}
diff --git a/tests/test-thread-optim2.c b/tests/test-thread-optim2.c
new file mode 100644
index 0000000000..bd2e3b2955
--- /dev/null
+++ b/tests/test-thread-optim2.c
@@ -0,0 +1,52 @@
+/* Test of optimization of multithreaded code..
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <[email protected]>, 2026.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "thread-optim.h"
+
+#include "glthread/thread.h"
+
+#include "macros.h"
+
+static int dummy;
+
+static void *
+thread_func (void *arg)
+{
+  return arg;
+}
+
+int
+main ()
+{
+#if !defined __gnu_hurd__
+# if defined _GL_MULTITHREADED_ALWAYS_TRUE || defined _GL_MULTITHREADED_VIA_ELF
+  ASSERT (gl_multithreaded ());
+# else
+  ASSERT (! gl_multithreaded ());
+# endif
+#endif
+
+  (void) gl_thread_create (thread_func, &dummy);
+
+  ASSERT (gl_multithreaded ());
+
+  return test_exit_status;
+}
-- 
2.54.0

Reply via email to