Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Short answer: never let two threads call members on the same System.Drawing.Graphics instance concurrently. Prefer one independent graphics destination per worker. If a shared destination is unavoidable, serialize every operation on that instance with one shared lock, coordinate disposal with the same lock, and do not use an ObjectBusy result as a retry-based synchronization scheme.

Graphics.CopyFromScreen copies a rectangle of screen pixels to a graphics drawing surface. Microsoft documents overloads using source and destination Point values plus a Size, and overloads that accept a CopyPixelOperation for combining source and destination colors. A failed transfer can raise Win32Exception; an invalid copy-operation value can raise InvalidEnumArgumentException. See the Microsoft API reference.

What must be synchronized

The synchronization unit is the graphics object and the resources it uses, not merely the line containing CopyFromScreen. GDI+ provides no automatic synchronization for a shared object. Microsoft’s GDI+ guidance says the application must protect each member access or method call when multiple threads can reach one GDI+ object, using a critical section or another standard synchronization mechanism. The guidance also says to synchronize before the call rather than coordinating around an ObjectBusy status: that status is not a replacement for a lock.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Windows GDI objects are likewise not serialized across threads. Microsoft recommends avoiding shared GDI objects where practical, or supplying application-level synchronization when sharing is required. Deleting an object while another thread uses it can produce unpredictable results.

What the call actually does

The method performs a bit-block transfer of color data for a screen rectangle onto a destination Graphics surface. The source location, destination location and region size define the transfer. For example, source=(100,100), destination=(0,0) and size=(800,600) copy an 800-by-600 region beginning at screen coordinate (100,100) into the destination’s origin.

What is not established by the API page

The API example uses a Windows Forms paint event, but that example does not establish a universal rule that every call must run on a particular UI thread. Your destination surface, framework and ownership model determine where it may be created and used. Verify those rules for the target Windows Forms, WPF or other framework. This is a Windows graphics technique; do not treat System.Drawing.Common as a general cross-platform screen-capture solution.

Design 1: give each thread its own graphics resources

Separate resources are usually the simplest design. Each worker owns its destination image and graphics object, so no two threads enter the same GDI+ object. Each worker still owns and disposes its resources deterministically.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
using System.Drawing;

static void CaptureWorker(Rectangle source, string outputPath)
{
    using var bitmap = new Bitmap(source.Width, source.Height,
                                   System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
    using (var graphics = Graphics.FromImage(bitmap))
    {
        graphics.CopyFromScreen(source.Location,
                                Point.Empty,
                                source.Size,
                                CopyPixelOperation.SourceCopy);
    }

    bitmap.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png);
}

var first = new Rectangle(0, 0, 800, 600);
var second = new Rectangle(800, 0, 800, 600);
var t1 = Task.Run(() => CaptureWorker(first, "left.png"));
var t2 = Task.Run(() => CaptureWorker(second, "right.png"));
await Task.WhenAll(t1, t2);

This example gives each task a separate Bitmap and Graphics. If your application later combines the images, protect the combining destination separately, or perform the merge after both workers finish. Do not pass one bitmap or graphics object into both workers unless you also adopt the shared-resource pattern below.

Design 2: serialize a genuinely shared Graphics instance

Sometimes a single destination is required—for example, two producers feed one shared canvas. Put every operation on that instance behind one lock object. The lock must be shared by all code paths, not recreated inside each method.

using System.Drawing;

public sealed class SharedScreenCanvas : IDisposable
{
    private readonly object _graphicsLock = new();
    private readonly Bitmap _bitmap;
    private readonly Graphics _graphics;
    private bool _disposed;

    public SharedScreenCanvas(int width, int height)
    {
        _bitmap = new Bitmap(width, height,
            System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
        _graphics = Graphics.FromImage(_bitmap);
    }

    public void Capture(Rectangle source, Point destination)
    {
        lock (_graphicsLock)
        {
            ThrowIfDisposed();
            _graphics.CopyFromScreen(source.Location,
                                     destination,
                                     source.Size,
                                     CopyPixelOperation.SourceCopy);
        }
    }

    public void Save(string path)
    {
        lock (_graphicsLock)
        {
            ThrowIfDisposed();
            _bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);
        }
    }

    public void Dispose()
    {
        lock (_graphicsLock)
        {
            if (_disposed) return;
            _graphics.Dispose();
            _bitmap.Dispose();
            _disposed = true;
        }
    }

    private void ThrowIfDisposed()
    {
        if (_disposed) throw new ObjectDisposedException(nameof(SharedScreenCanvas));
    }
}

using var canvas = new SharedScreenCanvas(1600, 600);
var left = Task.Run(() => canvas.Capture(
    new Rectangle(0, 0, 800, 600), new Point(0, 0)));
var right = Task.Run(() => canvas.Capture(
    new Rectangle(800, 0, 800, 600), new Point(800, 0)));
await Task.WhenAll(left, right);
canvas.Save("combined.png");

The lock serializes the calls, so the two captures do not execute simultaneously. It also protects saving and disposal because those operations touch the same bitmap and graphics state. A lock cannot make two operations parallel; it makes their ordering safe.

Rules for the shared pattern

  • Use one private lock object for the lifetime of the shared resource.
  • Require every member access that can touch the shared Graphics, its bitmap or related GDI object to take that lock.
  • Keep the critical section focused: validate arguments and perform unrelated work outside it.
  • Never dispose the graphics or its backing image while another operation can still use them. Dispose through the same lock and stop accepting new work first.
  • Do not expose the raw Graphics publicly; otherwise callers can bypass your synchronization.
  • Do not lock on this, a publicly accessible object or a string. A private readonly object prevents outside code from creating lock-order surprises.

Choosing coordinates, size and copy mode

Source and destination geometry

Use a positive, nonzero Size that fits the intended destination. Screen coordinates can be negative on a multi-monitor desktop when a monitor is positioned left or above the primary display. The source rectangle must correspond to the desktop coordinate space available to the process.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Copy operation

The overload without an explicit operation uses the API’s default transfer behavior. CopyPixelOperation.SourceCopy makes the intent explicit by replacing destination pixels with source pixels. Other enum values combine source and destination colors; pass only a defined CopyPixelOperation value or the API can throw InvalidEnumArgumentException.

Destination ownership

A Graphics created from an image is an ordinary disposable GDI+ resource. In a UI, a control’s paint surface can have framework-specific lifetime and threading rules. Do not infer that a worker may retain a paint-event graphics object after the event, and do not update controls from a worker without following that framework’s dispatch mechanism.

Coordinating two workers without races

Use task completion to coordinate work rather than polling the graphics object. Start both workers, await Task.WhenAll, then read or save the result. If one capture fails, observe both task exceptions and decide whether to discard the partial output.

try
{
    await Task.WhenAll(t1, t2);
}
catch (Exception ex)
{
    // Log the failing capture and dispose all owned resources.
    Console.Error.WriteLine(ex);
}

If captures must occur in a strict order, use a queue or acquire the same lock in the required order. Avoid holding the graphics lock while waiting for another task that might itself need that lock; that creates a deadlock risk.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Performance and reliability trade-offs

Separate resources

  • Allows actual overlap when the capture regions and destinations are independent.
  • Uses more bitmap and GDI handles.
  • Requires a clear ownership and disposal policy for each worker.

One synchronized resource

  • Preserves one destination surface and straightforward composition.
  • Serializes graphics calls, so throughput is bounded by the longest critical section.
  • Reduces race risk only if every access, including save, clear, transform changes and disposal, uses the same lock.

Keep capture regions no larger than needed, avoid repeated allocation inside a tight loop, and measure the complete operation—including encoding and disk I/O—rather than assuming the screen transfer is the only cost. If you need high-frequency capture, monitor handle usage and memory pressure as part of the application’s normal diagnostics.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common failures

Win32Exception from CopyFromScreen

Microsoft documents Win32Exception when the transfer fails. Check that the source rectangle is valid for the current desktop, the destination graphics and backing image are still alive, and the process is running in the Windows environment your capture design expects. Log the exception’s native error information and the rectangle values.

Intermittent corruption or “busy” behavior

This usually indicates unsynchronized access to a shared GDI+ object. Put the complete operation behind the single shared lock. Do not catch an ObjectBusy result and blindly retry; Microsoft specifically advises synchronizing before the call.

ObjectDisposedException

A worker is using a graphics object or bitmap after its owner disposed it. Move disposal into the owner’s shutdown path, wait for all capture tasks to finish, and use the same lock for disposal in a shared-resource class.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Only one monitor or the wrong area is captured

Verify virtual-screen coordinates and monitor arrangement. A monitor left of the primary commonly has a negative X coordinate. Compute the rectangle from the display configuration rather than assuming every screen starts at (0,0).

UI freezes

A lock prevents races but does not make capture nonblocking. Do the capture and encoding work away from the UI message loop when your framework permits it, then marshal only the finished result back to the UI. Respect the destination object’s framework-specific affinity and lifetime.

Works on Windows, fails elsewhere

This API depends on Windows screen and GDI/GDI+ behavior. Confirm the target framework and operating system before choosing it; do not promise portability based solely on compiling System.Drawing.Common.

Testing a two-thread implementation

  1. Run two workers repeatedly against non-overlapping source rectangles and verify both output dimensions and pixel placement.
  2. Run both workers against the same destination while enabling the shared lock; confirm there is no corruption across many iterations.
  3. Remove synchronization in a test branch only to demonstrate why the invariant matters; never ship that version.
  4. Dispose the canvas while work is pending and verify your shutdown code waits or rejects new work without accessing freed resources.
  5. Test negative monitor coordinates, a source rectangle that reaches a display edge, minimized or changing display layouts, and failed output paths.

Or skip the browser setup

If your goal is a website image rather than a desktop-pixel capture, ScreenshotNeo provides a single HTTP request and an MCP server for AI agents. It removes cookie-consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Every plan includes its features, with 1,000 shots per month free without a card and paid plans starting at $5 for 3,000 shots.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read the parameter and response details in the ScreenshotNeo documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also supports full-page and element captures, device and viewport settings, lazy-image loading, dark mode, custom CSS and JavaScript, waits, request blocking, cookies and headers, PDFs, caching, signed links, asynchronous jobs and bulk capture. Its MCP tools—take_screenshot, get_page_info and capture_pdf—let Claude, Cursor and other MCP clients request captures directly. Create a free ScreenshotNeo account to get 1,000 screenshots per month with no card.

FAQ

Can I make Graphics thread-safe by declaring it static?

No. Static storage changes lifetime and reachability, not synchronization. A shared static graphics object still requires the same application-level locking and disposal discipline.

Should I use Monitor.TryEnter and skip a frame when it is busy?

Only if dropping a frame is an explicit product decision. It is not a substitute for protecting the object. For correctness, acquire the lock and perform the operation under synchronization.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can two threads capture different screen rectangles at the same time?

Yes, when they use independent destination graphics resources and their lifetimes do not overlap unsafely. If they share one graphics object, calls must be serialized.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.