Skip to content
Alexander Gordeyko edited this page Oct 3, 2021 · 12 revisions

CSharp

A library providing specific CSharp features.

Introduction

When necessary to use CSharp platform, one faces the trouble when some specific CSharp keywords is lacking in Haxe. The library helps to use them. By now there is just one class Synchro providing thread synchronization.

Synchro

A static class providing synchronization. It has two methods: lock and mutex.

Methods

Lock method implements a lock keyword. CSharp syntax is:

    lock(LockedObject)
    {
        /*Some code to synchronize.*/
    }

The method needs two arguments: an object (Dynamic) and a callback (Void -> Void), so syntax looks like this:

    Synchro.lock(lockedObject, function() 
    {
        /*Some code.*/
    });

or

    Synchro.lock(lockedObject, someFunc);
    ...
    function someFunc():Void
    {
        /*Some code.*/
    }

The code in callback is so-called a critical section.

Note, that lockedOblect can be only of reference type. If it is not, SynchronizationLockException will be thrown. For value type one should use an Interlocked class.

Examples

Let us consider a simple example. We need to create an array of integers in main thread and push onto it numbers in child threads. The first of all, we create an array:

    static var arr:Array<Int> = new Array();

Then create fifty threads and start them:

    for (i in 0...50) 
    {
		var thr = new Thread(new ParameterizedThreadStart(threadFunction));
		thr.Start(i);
    }

The callback is:

    static function threadFunction(i:Dynamic):Void
    {
	    Synchro.lock(arr, function()arr.push(i));
    }

And do not forget to stop the main thread and watch the output:

    Sys.getChar(false);
    trace(arr);
    Sys.getChar(false);

All the code:

    package ;
    import pony.cs.Synchro;
    import cs.system.threading.Thread;
    import cs.system.threading.ParameterizedThreadStart;

    class Main 
    {
	static var arr:Array<Int> = new Array();
	
	static function main()
	{
		for (i in 0...50) 
		{
			var thr = new Thread(new ParameterizedThreadStart(threadFunction));
			thr.Start(i);
		}
		Sys.getChar(false);
		trace(arr);
		Sys.getChar(false);
	}
	
	static function threadFunction(i:Dynamic):Void
	{
		Synchro.lock(arr, function()arr.push(i));
	}
    }

The output is:

Main.hx:25: [1,7,5,3,15,11,4,8,2,6,13,14,10,12,9,0,16,17,18,19,20,21,22,23,24,25
,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49]

For example, if we don't have lock, i. e.

    static function threadFunction(i:Dynamic):Void
    {
	    arr.push(i);
    }

we will probably see something like this:

Необработанное исключение: Необработанное исключение: Необработанное исключение:

Необработанное исключение: System.IndexOutOfRangeException: Индекс находился вне
 границ массива.
  ...

It just will not work in the most cases. Although you might find out a variant that will seem to be correct, it will be just because of your (you're) luckness (luckless :-)). Next example shows how a race condition can do unexpected things without an exception and how one can prevent it. We have a class called SomeClass which has many integer fields. It also has a method initializing these fields, but the author of this class did not make this method thread-safe. The class is:

	class SomeClass 
	{

		public var i:Int;
		public var i1:Int;
		public var i2:Int;
		public var i3:Int;
		public var i4:Int;
		public var i5:Int;
		public var i6:Int;
		public var i7:Int;
		public var i8:Int;
		
		public function new() 
		{
			
		}
		
		public function setIntegers(integer:Int)
		{
			i = integer;
			Sys.sleep(Math.random()/10);
			i1 = integer;
			Sys.sleep(Math.random()/10);
			i2 = integer;
			Sys.sleep(Math.random()/10);
			i3 = integer;
			Sys.sleep(Math.random()/10);
			i4 = integer;
			Sys.sleep(Math.random()/10);
			i5 = integer;
			Sys.sleep(Math.random()/10);
			i6 = integer;
			Sys.sleep(Math.random()/10);
			i7 = integer;
			Sys.sleep(Math.random()/10);
			i8 = integer;
		}
	}

If we just call setIntegers(1) in main thread we will see that all the integers become equal to 1:

        static var sc:SomeClass = new SomeClass();
	
	static function main()
	{
		sc.setIntegers(1);
		Sys.sleep(1);
		trace(sc.i);
		trace(sc.i1);
		trace(sc.i2);
		trace(sc.i3);
		trace(sc.i4);
		trace(sc.i5);
		trace(sc.i6);
		trace(sc.i7);
		trace(sc.i8);
		Sys.getChar(false);
	}

The output:

Main.hx:25: 1
Main.hx:26: 1
Main.hx:27: 1
Main.hx:28: 1
Main.hx:29: 1
Main.hx:30: 1
Main.hx:31: 1
Main.hx:32: 1
Main.hx:33: 1

But the result may be unexpected a little bit if we do the same things asynchronously. Let us have now two threads, and the first thread calls setIntegers(0) while the second thread calls setIntegers(1).

	class Main 
	{
		static var sc:SomeClass = new SomeClass();
		
		static function main()
		{
			for (i in 0...2) 
			{
				var thr = new Thread(new ParameterizedThreadStart(threadFunction));
				thr.Start(i);
			}
			Sys.sleep(1);
			trace(sc.i);
			trace(sc.i1);
			trace(sc.i2);
			trace(sc.i3);
			trace(sc.i4);
			trace(sc.i5);
			trace(sc.i6);
			trace(sc.i7);
			trace(sc.i8);
			Sys.getChar(false);
		}
		
		static function threadFunction(i:Dynamic):Void
		{
			sc.setIntegers(i);
		}
	}

The output:

Main.hx:24: 1
Main.hx:25: 0
Main.hx:26: 0
Main.hx:27: 1
Main.hx:28: 0
Main.hx:29: 0
Main.hx:30: 0
Main.hx:31: 1
Main.hx:32: 1

It happened because setIntegers was called twice and was executing asynchronously, so race condition took part. If we do not want the fields to be combination of result of threads' callbacks, we can use synchronization. Add the lock function to the callback in the following way:

	class Main 
	{
		static var sc:SomeClass = new SomeClass();
		
		static function main()
		{
			for (i in 0...2) 
			{
				var thr = new Thread(new ParameterizedThreadStart(threadFunction));
				thr.Start(i);
			}
			Sys.sleep(1);
			trace(sc.i);
			trace(sc.i1);
			trace(sc.i2);
			trace(sc.i3);
			trace(sc.i4);
			trace(sc.i5);
			trace(sc.i6);
			trace(sc.i7);
			trace(sc.i8);
			Sys.getChar(false);
		}
		
		static function threadFunction(i:Dynamic):Void
		{
			Synchro.lock(sc, function() sc.setIntegers(i));
		}
	}

The output

Main.hx:24: 1
Main.hx:25: 1
Main.hx:26: 1
Main.hx:27: 1
Main.hx:28: 1
Main.hx:29: 1
Main.hx:30: 1
Main.hx:31: 1
Main.hx:32: 1

is as it was excepted.

But one'd better to guarantee the method to be thrad-safe. To achieve it one can rebuild the method like this:

	public function setIntegers(integer:Int)
		{
			Synchro.lock(this, function()
			{
				i = integer;
				Sys.sleep(Math.random()/10);
				i1 = integer;
				Sys.sleep(Math.random()/10);
				i2 = integer;
				Sys.sleep(Math.random()/10);
				i3 = integer;
				Sys.sleep(Math.random()/10);
				i4 = integer;
				Sys.sleep(Math.random()/10);
				i5 = integer;
				Sys.sleep(Math.random()/10);
				i6 = integer;
				Sys.sleep(Math.random()/10);
				i7 = integer;
				Sys.sleep(Math.random()/10);
				i8 = integer;
			});  
		}

In this case in the application we can type just

		static function threadFunction(i:Dynamic):Void
		{
			sc.setIntegers(i);
		}

and do not mess with our head. But one should never forget that -- though it seems to be simpler -- it can cause the program to be slower a little bit; we guarantee that our method is thread-safe but what if thread-safety is not needed at all (in case of one-thread application, for example)?

The mutex method implements, as one may guess by naming, mutex. The syntax is:

    Synchro.mutex(function() 
        {
            /*Some code.*/
        });

or the variant with named function.

Note, that mutex implemented here is only local one. There is not global mutex so there is no need in using mutex instead of lock.

The only difference between these two methods is that mutex is much slower. Truth be told, one must never use this mutex. It's added just because you might have some strange, unbelievable reasons to prefer mutex to lock (maybe, the "lock" word in your country is considered to be a sponsor of terrorism or so, so it is out of law).

Links

For those who want to get more: