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