Hi all,

Is System.Drawing by any means thread-safe?

Can I create some threads, create a Graphics object in each of them and then work with it from within that thread? Is this supposed to work or am I doing something really stupid here?

I ask because I've noticed random crashes in a WinForms app on OS X (but it happens on Linux too). Most of the time it crashes in System.Drawing.Graphics' DrawString or MeasureString methods and seems to occur then a background worker thread is working in parallel with UI thread.

I've tried to do a stress-test of System.Drawing in a sample multi-threaded program. See attached `test-multi-threaded-drawing.cs'.

On my Linux box it crashes all the time. I get a wide variety of errors from gdb stacktraces with SIGSEGV or SIGABRT in the end, to SIGILL with .Net stack trace.

Uncommenting these lock {} lines in the ThreadProc helps, but not an option for my real app, as there's simply no single place a lock could be added.

I've also tried writing some code in plain C which links to libgdiplus directly: see `threads-gdiplus.c'. It happily crashes just like the C# version.

My tests show that even using unsynchronized GdipGetImageGraphicsContext / GdipDeleteGraphics (no fonts stuff touched) can easily lead to crashes.

From what I've seen, cairo seems to be thread-safe: see attached[1] `cairo-multi-thread-text.c'.

Also, there's a few locking used around thread-unsafe fontconfig calls in libgdiplus itself. I didn't examined the whole code, so there's possibly other places in it missing locking primitives.

I would appreciate any help on this subject!

--
Regards,
Alex
[1] originally found in the cairo bugzilla for a few-years-old bug; my sligthly enhanced version
/* gmcs test-multi-threaded-drawing.cs -r:System.Drawing,System.Windows.Forms */
using System;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;

namespace test {
	public class MainForm : Form {
		private static int threadCount = 0;

		private object consoleLock = new Object();
		private object hwndLock = new Object();

		public static void Main(string[] args) {
			threadCount = args.Length == 0 ? 6 : int.Parse(args[0]);

			Application.Run(new MainForm());
		}

		protected override void OnLoad(EventArgs e) {
			base.OnLoad(e);

			for (int i = 0; i < threadCount; ++i) {
				Thread t = new Thread(ThreadProc);
				t.Start(i);
			}
		}

		void ThreadProc(object data) {
			int threadnum = (int) data;

			Random rnd = new Random((int) DateTime.Now.Ticks);
			int count = rnd.Next(250, 1000);
			lock (consoleLock) {
				Console.WriteLine("thread{0} start: {1}", threadnum, count);
			}

			for (int i = 0; i < count; ++i) {
				string str = CreateRandomString(rnd);

				using (Graphics g = GetGraphicsForMeasurement()) {
					using (Font font = CreateRandomFont(rnd)) {
						//lock (hwndLock) {
						SizeF sz = g.MeasureString(str, font);

						using (Bitmap bmp = new Bitmap((int) sz.Width, (int) sz.Height)) {
							using (Graphics gfx = Graphics.FromImage(bmp)) {
								using (Brush b = Brushes.Red) {
									gfx.DrawString(str, font, b, new PointF(0f, 0f));
								}
							}
						}
						//}
					}
				}
			}

			lock (consoleLock) {
				Console.WriteLine("thread{0} done", threadnum);
			}
		}

		string CreateRandomString(Random rnd) {
			int len = 1 + rnd.Next(60);
			StringBuilder sb = new StringBuilder(len);
			for (int j = 0; j < len; ++j) {
				int ch = (rnd.Next() & 1) == 1 ? 0x41 : 0x61; // 'A' or 'a'
				sb.Append(Char.ConvertFromUtf32(ch + rnd.Next(26)));
			}
			return sb.ToString();
		}

		Graphics GetGraphicsForMeasurement() {
#if NO_GRAPHICS_FROM_HWND
			Bitmap tmp = new Bitmap(1, 1);
			return Graphics.FromImage(tmp);
#else
			Graphics g;
			lock (hwndLock) {
				g = Graphics.FromHwnd(this.Handle);
			}
			return g;
#endif
		}

		Font CreateRandomFont(Random rnd) {
			return new Font("Sans", (int) (8 + rnd.NextDouble()*10));
		}
	}
}
/* gcc `pkg-config --cflags --libs cairo` -lpthread multi-thread-text.c -o multi-thread-text */

/*
 * Copyright © 2005 Red Hat, Inc.
 *
 * Permission to use, copy, modify, distribute, and sell this software
 * and its documentation for any purpose is hereby granted without
 * fee, provided that the above copyright notice appear in all copies
 * and that both that copyright notice and this permission notice
 * appear in supporting documentation, and that the name of
 * Red Hat, Inc. not be used in advertising or publicity pertaining to
 * distribution of the software without specific, written prior
 * permission. Red Hat, Inc. makes no representations about the
 * suitability of this software for any purpose.  It is provided "as
 * is" without express or implied warranty.
 *
 * RED HAT, INC. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
 * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS, IN NO EVENT SHALL RED HAT, INC. BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
 * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Author: Carl D. Worth <cwo...@cworth.org>
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <cairo.h>
#include <pthread.h>

static void *
start (void *closure)
{
    int i;

    for (i = 0; i < 1000; ++i) {
      cairo_surface_t *surface;
      cairo_t *cr;

      surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100);
      cr = cairo_create (surface);

      cairo_move_to (cr, 10, 10);

      cairo_set_font_size (cr, 10);
      cairo_show_text (cr, "Hello world.\n");

      cairo_set_font_size (cr, 9);
      cairo_show_text (cr, "Hello world.\n");

      cairo_set_font_size (cr, 8);
      cairo_show_text (cr, "Hello world.\n");

      cairo_destroy (cr);
      cairo_surface_destroy (surface);
    }

    return NULL;
}

int
main (int argc, char *argv[0])
{
    int i, num_threads;
    pthread_t *pthread;

    if (argc > 1) {
	num_threads = atoi (argv[1]);
    } else {
	num_threads = 6;
	printf ("Running with default value of %d threads.\n"
		"To change, call: %s <number_of_threads>\n",
		num_threads, argv[0]);
    }

    pthread = malloc (num_threads * sizeof (pthread_t));
    assert (pthread != NULL);

    for (i = 0; i < num_threads; i++)
	pthread_create (&pthread[i], NULL, start, NULL);

    for (i = 0; i < num_threads; i++) {
	pthread_join (pthread[i], NULL);
	printf("joined thread%d\n", i);
    }

    return 0;
}
/* gcc threads-gdiplus.c `pkg-config --cflags glib-2.0` -Wall -lgdiplus -lpthread */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

#include <gdiplus/GdiPlusFlat.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void
fatal(char const* func, GpStatus err)
{
  fprintf(stderr, "%s: %d\n", func, err);
  exit(1);
}

static void*
start(void* closure)
{
  GpStatus err;

  GpBitmap* img;
  int width = 128;
  int height = 32;
  int stride = width*4;

  GpGraphics* gfx;

  GpFontFamily* family;
  GpFont* font;

  WCHAR const str[] = {'H','e','l','l','o',',',' ','W','o','r','l','d','!'};
  size_t len = sizeof(str)/sizeof(WCHAR);
  RectF rect;
  RectF bounds;

  int i;

  for (i = 0; i < 1000; ++i) {

    err = GdipCreateBitmapFromScan0(width, height, stride, PixelFormat32bppARGB, /* scan0 = */ NULL, &img);
    if (err != Ok)
      fatal("GdipCreateBitmapFromScan0", err);

    //assert(pthread_mutex_lock(&mutex) == 0);

    err = GdipGetImageGraphicsContext(img, &gfx);
    if (err != Ok)
      fatal("GdipGetImageGraphicsContext", err);

    //assert(pthread_mutex_unlock(&mutex) == 0);


    err = GdipGetGenericFontFamilySansSerif(&family);
    if (err != Ok)
      fatal("GdipGetGenericFontFamilySansSerif", err);

    err = GdipCreateFont(family, 12.0, FontStyleRegular, UnitPoint, &font);
    if (err != Ok)
      fatal("GdipCreateFont", err);



    rect.X = 0;
    rect.Y = 0;
    rect.Width = width;
    rect.Height = height;

    //assert(pthread_mutex_lock(&mutex) == 0);

    err = GdipMeasureString(gfx, str, len, font, &rect, /* format = */ NULL, &bounds,
                            /* codepoints = */ NULL, /* lines = */ NULL);
    if (err != Ok)
      fatal("GdipMeasureString", err);

    //assert(pthread_mutex_unlock(&mutex) == 0);


    err = GdipDeleteFont(font);
    if (err != Ok)
      fatal("GdipDeleteFont", err);



    err = GdipDeleteGraphics(gfx);
    if (err != Ok)
      fatal("GdipDeleteGraphics", err);


    err = GdipDisposeImage(img);
    if (err != Ok)
      fatal("GdipDisposeImage", err);

  }

  return NULL;
}

int
main(int argc, char *argv[0])
{
  int i, num_threads;
  pthread_t* pthread;

  GpStatus err;
  ULONG_PTR gdiptok;
  GdiplusStartupInput gdipinput;
  GdiplusStartupOutput gdipoutput;

  err = GdiplusStartup(&gdiptok, &gdipinput, &gdipoutput);
  if (err != Ok)
    fatal("GdiplusStartup", err);


  if (argc > 1) {
    num_threads = atoi(argv[1]);
  } else {
    num_threads = 6;
    printf("Running with default value of %d threads.\n"
           "To change, call: %s <number_of_threads>\n",
           num_threads, argv[0]);
  }

  pthread = malloc(num_threads * sizeof(pthread_t));
  assert(pthread != NULL);

  for (i = 0; i < num_threads; i++)
    pthread_create(&pthread[i], NULL, start, NULL);

  for (i = 0; i < num_threads; i++) {
    pthread_join(pthread[i], NULL);
    printf("joined thread%d\n", i);
  }

  pthread_mutex_destroy(&mutex);

  return 0;
}
_______________________________________________
Mono-devel-list mailing list
Mono-devel-list@lists.ximian.com
http://lists.ximian.com/mailman/listinfo/mono-devel-list

Reply via email to