diff --git a/gdb/NEWS b/gdb/NEWS
index fd42864c692..fe79f1716fd 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -208,6 +208,12 @@ info main
   ** New function gdb.execute_mi(COMMAND, [ARG]...), that invokes a
      GDB/MI command and returns the output as a Python dictionary.
 
+  ** New function gdb.block_signals().  This returns a context manager
+     that blocks any signals that GDB needs to handle itself.
+
+  ** New class gdb.Thread.  This is a subclass of threading.Thread
+     that calls gdb.block_signals in its "start" method.
+
   ** gdb.parse_and_eval now has a new "global_context" parameter.
      This can be used to request that the parse only examine global
      symbols.
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 9a342f34bf0..75fae3a1895 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -190,6 +190,7 @@ optional arguments while skipping others.  Example:
 
 @menu
 * Basic Python::                Basic Python Functions.
+* Threading in GDB::		Using Python threads in GDB.
 * Exception Handling::          How Python exceptions are translated.
 * Values From Inferior::        Python representation of values.
 * Types In Python::             Python representation of types.
@@ -447,45 +448,6 @@ will be @code{None} and 0 respectively.  This is identical to
 historical compatibility.
 @end defun
 
-@findex gdb.post_event
-@defun gdb.post_event (event)
-Put @var{event}, a callable object taking no arguments, into
-@value{GDBN}'s internal event queue.  This callable will be invoked at
-some later point, during @value{GDBN}'s event processing.  Events
-posted using @code{post_event} will be run in the order in which they
-were posted; however, there is no way to know when they will be
-processed relative to other events inside @value{GDBN}.
-
-@value{GDBN} is not thread-safe.  If your Python program uses multiple
-threads, you must be careful to only call @value{GDBN}-specific
-functions in the @value{GDBN} thread.  @code{post_event} ensures
-this.  For example:
-
-@smallexample
-(@value{GDBP}) python
->import threading
->
->class Writer():
-> def __init__(self, message):
->        self.message = message;
-> def __call__(self):
->        gdb.write(self.message)
->
->class MyThread1 (threading.Thread):
-> def run (self):
->        gdb.post_event(Writer("Hello "))
->
->class MyThread2 (threading.Thread):
-> def run (self):
->        gdb.post_event(Writer("World\n"))
->
->MyThread1().start()
->MyThread2().start()
->end
-(@value{GDBP}) Hello World
-@end smallexample
-@end defun
-
 @findex gdb.write 
 @defun gdb.write (string @r{[}, stream@r{]})
 Print a string to @value{GDBN}'s paginated output stream.  The
@@ -688,6 +650,71 @@ In Python}), the @code{language} method might be preferable in some
 cases, as that is not affected by the user's language setting.
 @end defun
 
+@node Threading in GDB
+@subsubsection Threading in GDB
+
+@value{GDBN} is not thread-safe.  If your Python program uses multiple
+threads, you must be careful to only call @value{GDBN}-specific
+functions in the @value{GDBN} thread.  @value{GDBN} provides some
+functions to help with this.
+
+@defun gdb.block_signals ()
+As mentioned earlier (@pxref{Basic Python}), certain signals must be
+delivered to the @value{GDBN} main thread.  The @code{block_signals}
+function returns a context manager that will block these signals on
+entry.  This can be used when starting a new thread to ensure that the
+signals are blocked there, like:
+
+@smallexample
+with gdb.block_signals():
+   start_new_thread()
+@end smallexample
+@end defun
+
+@deftp {class} gdb.Thread
+This is a subclass of Python's @code{threading.Thread} class.  It
+overrides the @code{start} method to call @code{block_signals}, making
+this an easy-to-use drop-in replacement for creating threads that will
+work well in @value{GDBN}.
+@end deftp
+
+@defun gdb.post_event (event)
+Put @var{event}, a callable object taking no arguments, into
+@value{GDBN}'s internal event queue.  This callable will be invoked at
+some later point, during @value{GDBN}'s event processing.  Events
+posted using @code{post_event} will be run in the order in which they
+were posted; however, there is no way to know when they will be
+processed relative to other events inside @value{GDBN}.
+
+Unlike most Python APIs in @value{GDBN}, @code{post_event} is
+thread-safe.  For example:
+
+@smallexample
+(@value{GDBP}) python
+>import threading
+>
+>class Writer():
+> def __init__(self, message):
+>        self.message = message;
+> def __call__(self):
+>        gdb.write(self.message)
+>
+>class MyThread1 (threading.Thread):
+> def run (self):
+>        gdb.post_event(Writer("Hello "))
+>
+>class MyThread2 (threading.Thread):
+> def run (self):
+>        gdb.post_event(Writer("World\n"))
+>
+>MyThread1().start()
+>MyThread2().start()
+>end
+(@value{GDBP}) Hello World
+@end smallexample
+@end defun
+
+
 @node Exception Handling
 @subsubsection Exception Handling
 @cindex python exceptions
diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 6f3f1945f62..98aadb1dfea 100644
--- a/gdb/python/lib/gdb/__init__.py
+++ b/gdb/python/lib/gdb/__init__.py
@@ -13,6 +13,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import signal
+import threading
 import traceback
 import os
 import sys
@@ -259,3 +261,33 @@ def with_parameter(name, value):
         yield None
     finally:
         set_parameter(name, old_value)
+
+
+@contextmanager
+def blocked_signals():
+    """A helper function that blocks and unblocks signals."""
+    if not hasattr(signal, "pthread_sigmask"):
+        yield
+        return
+
+    to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
+    signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
+    try:
+        yield None
+    finally:
+        signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
+
+
+class Thread(threading.Thread):
+    """A GDB-specific wrapper around threading.Thread
+
+    This wrapper ensures that the new thread blocks any signals that
+    must be delivered on GDB's main thread."""
+
+    def start(self):
+        # GDB requires that these be delivered to the main thread.  We
+        # do this here to avoid any possible race with the creation of
+        # the new thread.  The thread mask is inherited by new
+        # threads.
+        with blocked_signals():
+            super().start()
diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py
index aa834cdb14c..15d1fb9e9e5 100644
--- a/gdb/python/lib/gdb/dap/startup.py
+++ b/gdb/python/lib/gdb/dap/startup.py
@@ -18,10 +18,8 @@
 import functools
 import gdb
 import queue
-import signal
 import threading
 import traceback
-from contextlib import contextmanager
 import sys
 
 
@@ -33,32 +31,12 @@ _gdb_thread = threading.current_thread()
 _dap_thread = None
 
 
-@contextmanager
-def blocked_signals():
-    """A helper function that blocks and unblocks signals."""
-    if not hasattr(signal, "pthread_sigmask"):
-        yield
-        return
-
-    to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
-    signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
-    try:
-        yield None
-    finally:
-        signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
-
-
 def start_thread(name, target, args=()):
     """Start a new thread, invoking TARGET with *ARGS there.
     This is a helper function that ensures that any GDB signals are
     correctly blocked."""
-    # GDB requires that these be delivered to the gdb thread.  We
-    # do this here to avoid any possible race with the creation of
-    # the new thread.  The thread mask is inherited by new
-    # threads.
-    with blocked_signals():
-        result = threading.Thread(target=target, args=args, daemon=True)
-        result.start()
+    result = gdb.Thread(target=target, args=args, daemon=True)
+    result.start()
 
 
 def start_dap(target):
