XmlSchema and XmlSchemaSet thread safety

Here's a good word of warning: even if an object "feels" read-only because you're not calling code to modify it, if it's not documented as safe for use from multiple threads, then you shouldn't risk it.

In particular, I'd like to talk about XmlSchema and XmlSchemaSet today. Building these has a cost associated, and so it's nice to be able to build them once and then reuse them. But you have to be very careful in doing this. The docs say that all instance methods are not safe for multiple thread usage, but you don't really use them directly during validation, so it's hard to tell from the outside what's safe and what's not.

In a nutshell, the only thing you can do that is safe for concurrent usage is to use a validating reader. Here's the sample code to try this out (for some reason, this "breaks" more on 64-bit machines, but it's unsafe on all architectures).

First, a little helper to create an XmlSchema.

private

XmlSchema CreateSchema()
{
  string schemaText = @"<?xml version='1.0'?>
<xs:schema id='play' targetNamespace='https://tempuri.org/play.xsd'
elementFormDefault='qualified' xmlns='https://tempuri.org/play.xsd'
xmlns:xs='https://www.w3.org/2001/XMLSchema'>
<xs:element name='myShoeSize'>
<xs:complexType>
<xs:simpleContent>
<xs:extension base='xs:decimal'>
<xs:attribute name='sizing' type='xs:string' />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:schema>";

  using (StringReader reader = new StringReader(schemaText))
{
    return XmlSchema.Read(reader, null);
}
}

Next, a simple XmlSchemaSet.

private XmlSchemaSet CreateSchemaSet(XmlSchema schema)
{
  XmlSchemaSet set = new XmlSchemaSet();
set.Add(schema);
set.Compile();
  return set;
}

Finally, some validation:

private void ValidateDocument(XmlSchemaSet set)
{
  string doc = @"<myShoeSize xmlns='https://tempuri.org/play.xsd' sizing='123' />";
  XmlReaderSettings settings = new XmlReaderSettings();
settings.Schemas = set;
settings.ValidationEventHandler += new ValidationEventHandler(settings_ValidationEventHandler);
  using (StringReader reader = new StringReader(doc))
  using (XmlReader x = XmlReader.Create(reader, settings))
{
    while (x.Read()) { }
}
}

private int failCount;
void settings_ValidationEventHandler(object sender, ValidationEventArgs e)
{
System.Threading.Interlocked.Increment(ref failCount);
}

Now, armed with these, I will show you some code that is thread-safe, but that a single line reorder would cause to break.

XmlSchema schema = CreateSchema();
Thread[] threads = new Thread[10];
XmlSchemaSet set = CreateSchemaSet(schema);
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread((x) =>
{
      for (int j = 0; j < 1000; j++)
{
        // If the CreateSchemaSet were here
        // instead of outside this would break!
//
        // Don't add the schema to the
        // XmlSchemaSet from multiple threads!
        //
        // XmlSchemaSet set = CreateSchemaSet(schema);
//
        ValidateDocument(set);
}
});
}

Array.ForEach(threads, (t) => t.Start());
Array.ForEach(threads, (t) => t.Join());
this.Text = "Failure count: " + failCount; 

The part before the thread creation is all thread-safe; the stuff inside the callback is happening on multiple threads at the same time. You can only use the set for validation here! 

Enjoy!