What Is a Thread? Key Things to Know About Using Threads in Software Development

What Is a Thread? Key Things to Know About Using Threads in Software Development
Almost every modern application does several things at once: a web server handles hundreds of requests, a mobile app downloads data without freezing the screen, a video editor renders a preview while you keep working. Much of this is made possible by threads. In this article we will look at what a thread is, how it differs from a process, why threads are useful, and which pitfalls developers run into when working with them.
What is a thread?
A thread (thread of execution) is the smallest unit of work that the operating system can schedule on a CPU. Simply put, it is a sequence of instructions that runs independently of other such sequences.
Every program starts with one thread, the main thread. The program can then create additional threads, and they will run alongside the main one.
Process vs. thread
To understand threads, compare them with processes:
- Process is a running program with its own isolated memory space, open files, and system resources. Processes do not see each other's memory (unless special mechanisms are used).
- Thread lives inside a process. All threads of one process share the same memory: global variables, heap objects, open files and connections. Each thread has only its own stack, registers, and instruction pointer.
Shared memory is both the main advantage and the main source of problems with threads.
Why use threads?
- Responsiveness. Long operations run in a background thread while the main thread keeps the interface responsive.
- Waiting for I/O. While one thread waits for a database, disk or network response, other threads can do useful work. This is the typical case for web servers.
- Using multiple CPU cores. CPU-heavy tasks (image processing, calculations) can be split across threads that run truly in parallel on different cores.
- Cheaper than processes. Threads start faster and use less memory than separate processes.
Concurrency vs. parallelism
These two terms are often confused:
- Concurrency means several tasks are in progress during the same period of time. On a single core the OS switches between them so fast that it looks simultaneous.
- Parallelism means tasks literally run at the same moment on different cores.
Threads give you concurrency always, and parallelism only when there are several cores and the language runtime allows it (more on that below).
Main problems when working with threads
1. Race condition
A race condition happens when the result depends on the order in which threads execute. A classic example is incrementing a shared counter:
counter = 0
threads = 10.times.map do
Thread.new do
100_000.times { counter += 1 }
end
end
threads.each(&:join)
puts counter # 1_000_000 is NOT guaranteed
The operation counter += 1 is actually three steps: read the value, add one, write it back. If two threads read the same value at the same time, one of the increments is lost. Such bugs are especially dangerous because they appear randomly and are hard to reproduce.
2. Deadlock
A deadlock occurs when two threads wait for each other forever. Thread A holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1. The program simply hangs.
The simplest way to prevent it is to always acquire locks in the same order across the whole program.
3. Starvation and livelock
Starvation means a thread never gets access to a resource because others keep taking it. Livelock means threads are constantly reacting to each other and changing state, but no real progress is made.
4. Hard debugging
Multithreaded bugs often disappear when you add logging or run the debugger, because timing changes. That is why it is better to design thread safety from the start than to hunt for bugs later.
Synchronization tools
- Mutex (lock) allows only one thread at a time to enter a critical section.
- Semaphore limits the number of threads that can access a resource at once (for example, no more than 5 simultaneous connections).
- Condition variable lets a thread sleep until another thread signals that a condition has been met.
- Atomic operations perform simple actions (increment, compare-and-swap) as one indivisible step without explicit locks.
- Thread-safe queues are a convenient way to pass data between threads without sharing mutable state.
Here is the fixed counter example using a mutex:
counter = 0
mutex = Mutex.new
threads = 10.times.map do
Thread.new do
100_000.times do
mutex.synchronize { counter += 1 }
end
end
end
threads.each(&:join)
puts counter # always 1_000_000
Thread pools
Creating a new thread for every small task is wasteful: each thread costs memory and time to create. Instead, a thread pool is used: a fixed number of threads is created in advance, and tasks are placed in a queue and picked up by free threads.
An example in Ruby using the concurrent-ruby gem (it is already a Rails dependency), downloading several pages in parallel:
require "concurrent"
require "net/http"
urls = %w[https://example.com https://example.org https://example.net]
pool = Concurrent::FixedThreadPool.new(5)
futures = urls.map do |url|
Concurrent::Promises.future_on(pool, url) do |u|
[u, Net::HTTP.get(URI(u)).bytesize]
end
end
futures.each do |future|
url, size = future.value!
puts "#{url}: #{size} bytes"
end
pool.shutdown
pool.wait_for_termination
The GVL in Ruby
In Ruby it is important to understand how threads actually run:
- MRI/CRuby, the standard Ruby interpreter, has the GVL (Global VM Lock), which allows only one thread at a time to execute Ruby code.
- JRuby and TruffleRuby have no GVL, and their threads run truly in parallel.
What does this mean in practice? In MRI threads work great for I/O-bound tasks (network, database, files), because the lock is released while waiting. For CPU-bound tasks they give almost no speedup, and it is better to use multiple processes (forking workers in Puma, Unicorn or Passenger) or Ractors, available since Ruby 3.0.
Important: the GVL does not make your code thread-safe. Race conditions like the counter example are still possible, because threads can switch between any two operations.
Languages such as Java, C#, Go, Rust, and C++ run threads truly in parallel, so CPU-heavy work scales across cores.
Threads in web applications (the Rails example)
In web development threads are everywhere, even if you do not create them yourself:
- Puma, the default Rails server, handles requests using several processes (workers), each with several threads. The number of threads is set in
config/puma.rb. - Phusion Passenger in the open-source edition uses a multi-process model; multithreading is available in the Enterprise edition.
- Sidekiq processes background jobs in threads.
Practical consequences for a Rails developer:
- Do not store request data in class variables, global variables or constants that change at runtime: another thread may overwrite them. For per-request state use
ActiveSupport::CurrentAttributes. - The database connection pool size (
poolindatabase.yml) must be no smaller than the number of threads per process, otherwise threads will wait for a free connection. - Check that the gems you use are thread-safe.
# config/puma.rb
max_threads = ENV.fetch("RAILS_MAX_THREADS", 5)
threads max_threads, max_threads
workers ENV.fetch("WEB_CONCURRENCY", 2)
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
Alternatives to classic threads
- Async/await and event loops (the
asyncgem and the Falcon server in Ruby, Node.js): one thread handles thousands of I/O operations by switching between tasks at waiting points. - Lightweight (green) threads: goroutines in Go, virtual threads in Java 21+, Fibers in Ruby. They are managed by the runtime rather than the OS and are much cheaper, so you can have hundreds of thousands of them.
- Actor model (Erlang/Elixir, Akka): no shared memory at all, only message passing between isolated actors.
Best practices
- Minimize shared mutable state. The less data threads share, the fewer problems. Prefer immutable objects.
- Keep critical sections short. Hold a lock only as long as necessary and never do slow I/O inside it.
- Acquire locks in a consistent order to avoid deadlocks.
- Use thread pools instead of creating threads manually for every task.
- Prefer high-level tools: thread-safe queues, executors, futures, and ready-made concurrent collections.
- Always handle exceptions in threads. An error in a background thread can disappear silently if nobody checks it.
- Pick the right tool for the job: threads or async for I/O-bound work, processes or truly parallel threads for CPU-bound work.
- Test under load. Concurrency bugs often appear only with many simultaneous requests.
Conclusion
A thread is a lightweight unit of execution inside a process that shares memory with other threads. Threads make applications faster and more responsive, but shared memory brings race conditions, deadlocks and bugs that are hard to catch. Understand how your language and runtime handle threads, protect shared data with synchronization, prefer thread pools and high-level abstractions, and threads will become a reliable tool rather than a source of headaches.
Back



