Those are asyncs, not preemptive threads. Asyncs are kind of cooperative, no real parallelism.
protected_preemptive_string.zip (6.5 KB)
Your playground redesigned:
I removed the sleeps because thereās no need using this kind of design
Not true in Swift 6, actually you can have true concurrency with mutable objects safely controlled by Actors (which basically serialize access to the mutable object). Itās like using semaphores in Xojo, but built into the language at a deeper level, so you donāt have to worry about it (quite so much) - but this is getting quite off topic
Back to Xojo - I think the question is āwhat can we safely do across preemptive threadsā - some of the writing I quoted above suggests to me that Xojo has tried to make big parts of the framework thread safe, so that explicit use of semaphore (etc.) is not needed.
Just assume that anything shared a preemptive thread writes to, it must be protected/syncād and you should be ok. Also beware about āconsumableā things, a thread can consume things from the shadows causing side effects.
That first quote is actually mine.
Interesting note: I replaced the builtin function String.CountFields()
:
var n as integer = foobar.CountFields("|")
with my own version:
var n as integer = foobar.MyCountFields("|")
Public Function MyCountFields(extends s as string, delimiter as string) As integer
#Pragma DisableBackgroundTasks
dim u as integer = s.Length -1
var n as integer
for i as integer = 0 to u
if s.Middle(i,1) = delimiter then
n = n +1
end if
next
return n
End Function
And now, no more crashing. I wonder if this is some bug specific to String.CountFields()?
[Edit: bugfix]
Correction: I was wrong. MyCountFields() was buggy, and was returning the wrong field count, which was triggering a lot of system.debugLog() calls, which was affecting the timing, and it wasnāt crashing as a result.
If I put in the correct version that does the calculation properly:
Public Function MyCountFields(extends s as string, delimiter as string) As integer
#Pragma DisableBackgroundTasks
dim u as integer = s.Length -1
if u < 0 then
return 0
end if
var n as integer
for i as integer = 0 to u
if s.Middle(i,1) = delimiter then
n = n +1
end if
next
return n + 1
End Function
Then I am seeing the crash still:
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_platform.dylib 0x7ff80ea7a6a2 _platform_memmove$VARIANT$Haswell + 194
1 XojoFramework 0x109861416 StringOpsClassic::ConstructFromBuffer(char const*, unsigned int, int) + 70
2 XojoFramework 0x109870a95 string::ConstructFromBuffer(char const*, unsigned long, unsigned int) + 77
3 XojoFramework 0x109862596 StringOpsClassic::MidNBytes(StringStorageBase*, long long, long long) + 172
4 preemptiveString4 0x107f93961 String.$Middle%s%si8i8 + 33
5 preemptiveString4 0x10846067d Globals.$MyCountFields%i8%ss + 461
6 preemptiveString4 0x108456b65 Window1.Window1.pbPreemptive_Pressed%%o<Window1.Window1>o<DesktopButton> + 1653
7
Debugging this preemptive thread stuff is tricky!
What are you trying to achieve? Are you are trying to find some timing coincidence that enables it run a bit more in your machine before crashing without using proper locks? Or are you saying it still crashes using proper locks?
The opposite, Iām trying to find as many ways to crash it as possible.
Xojo wrote
For the most part, our entire framework is now safe for concurrent use with preemptive threads, with a few exceptions [ā¦] When sharing resources such as MemoryBlocks, Dictionaries, or Arrays among threads, you may need to use
CriticalSections
orSemaphores
to ensure safe access.
My experience so far is very different, so Iām reporting these discrepancies as bugs.
If Xojo comes back and says āoops, we mis-spoke, the framework is not generally thread-safeā then Iāll change course.
Letās reach the bottom of this.
In my experience, they will appreciate that.
Iām running into something similar, but it is not something I can reliably reproduce: I have the main thread plus a few other threads all running at the same time. Iāve converted all my non-main threads to be pre-emptive, and Iām using semaphores for the obvious resources the threads need to share.
For instance, I read and write to a hardware device, so attempts to communicate with that device from any thread have to do a criticalSection.enter, to their work, then criticalSection.leave when finished. All threads share the same critical section(s) for the various shared resources they need to access.
However, there is one resource I have not wrapped with a critical section: my logger. Iāve written a logging class, and just have a single instance of it as a property on my app class. So from any where in the app (or in any thread) Iāve just called:
app.logger.writeLog(āmessageā, priority, etc, etc)
It seems that very occasionally Iāll run into a situation where a thread will try to write to the log at the same time that either another thread or the main thread is trying to do so.
Is this a classic situation of āWell duh! Thatās what Semaphores are for, you dummy!ā or is this one that based on the docs indicating that things should generally be thread safe youād expect should work without issue?
Here is a portion of the error report that Apple produces (no errors ever appear in the debugger - the app simply quits):
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 ??? 0x7ff89cda6a84 ???
1 libsystem_kernel.dylib 0x7ff80cbd8b52 __pthread_kill + 10
2 libsystem_pthread.dylib 0x7ff80cc12f85 pthread_kill + 262
3 libsystem_c.dylib 0x7ff80cb33b19 abort + 126
4 libsystem_malloc.dylib 0x7ff80ca32ab1 malloc_vreport + 857
5 libsystem_malloc.dylib 0x7ff80ca5a376 malloc_zone_error + 183
6 libsystem_malloc.dylib 0x7ff80ca49544 nanov2_guard_corruption_detected + 34
7 libsystem_malloc.dylib 0x7ff80ca4950e nanov2_allocate_outlined + 407
8 libsystem_malloc.dylib 0x7ff80ca48962 nanov2_malloc_type_zero_on_alloc + 556
9 libc++abi.dylib 0x7ff80cbcd324 operator new(unsigned long) + 52
10 libc++.1.dylib 0x7ff80cb50120 std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::__grow_by_and_replace(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, char const*) + 162
11 libc++.1.dylib 0x7ff80cb50bdd std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::append(char const*, unsigned long) + 95
12 XojoFramework 0x112c8e26a DebuggerPacketBuilder::AddEncodedString(int, void const*, unsigned long) + 354
13 XojoFramework 0x112c978cf DebuggerLogMessage(int, string) + 141
14 XojoFramework 0x112d175dd SystemDebugLogger + 56
15 AcuGraph 5.debug 0x100b70007 System.DebugLog%%s + 39 <-- That's my code, calling into System.DebugLog()
@Kimball_Larsen - Please create an issue with your investigations. Thanks
Have you tried throwing a Semaphore or CriticalSection in the app.Logger.WriteLog method?
The function already is using a critical section - but only around where it writes the message out to a specific log file or database⦠at the end of the method (outside the criticalsection use) I also had a call to System.debugLog, which is where the crash was happening. I extended the use of the criticalSection to also encapsulate the System.debugLog call, and that seems to have solve the problem.
I tried this afternoon to come up with a sample app which writes to System.debugLog from lots of preemptive threads, but so far it does not exhibit the same issue. I may play with putting a more sophisticated sample together in the morning.
Fwiw, Iāve used DebugLog heavily without an issue.
Could the problem lie in the source of the text rather than the call itself?
This sounds like you have only one critical section and Enter / Leave that when accessing any one of a set of resources. That can be a bad idea. Take for example two files. One thread writing to the first file can happen perfectly happily while a second thread writes to a second file. If you use only one critical section then one write will block the second and vice versa. This can significatly slow down your application. Each āresourceā should have a separate critical section, unless they are part of a group that is alway accessed together.
Secondly you have to protect access by the main thread also, in the same way you do for other threads.
Update: Xojo has just said that setting a string is apparently not a thread-safe operation, and therefore this crash reported is not a bug.
If they stick with this decision, this means to me that the thread safety of the framework has been vastly over-hyped, and statements like this:
For the most part, our entire framework is now safe for concurrent use with preemptive threads, with a few exceptions like XojoScript and Runtime.IterateObjects.
Are quite misleading.
Please add your comments to https://tracker.xojo.com/xojoinc/xojo/-/issues/77525 to give Xojo some feedback.
For the most part, our entire framework is now safeā¦
I guess one could argue that while they said the framework is safe, they never said the language is safe
But seriously, I hope Xojo can chime in with some guidance:
- what is actually expected to be safe in 2024R3
- are there plans to make other things safe in subsequent versions?
I think having atomic, thread-safe Strings would be wonderful in the long run, but we can work around it if thatās not possible right now.
To clarify, setting a string is not an atomic operation. If youāre modifying this string concurrently in preemptive threads, itās unsafe. In fact, I would argue that this is unsafe in any programming language.