Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Example Analysis of Property in iOS Multithreading

2025-01-18 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

Editor to share with you the example analysis of Property in iOS multithreading, I believe that most people do not know much about it, so share this article for your reference, I hope you can learn a lot after reading this article, let's go to know it!

Preface

Shared state, multi-thread access to an object's property, is a very common use scenario in iOS programming, let's start with the multi-thread security of Property.

Property

When we talk about property multithreading safety, many people know that adding atomic attribute to property can ensure multithreading safety to a certain extent, similar to:

@ property (atomic, strong) NSString* userName

Things are not as simple as they seem, and to analyze the performance of property in multithreaded scenarios, you need to distinguish between the types of property.

We can simply divide property into value type and object type, value type refers to primitive type, including int, long, bool and other non-object types, and the other is object type, which is declared as a pointer and can point to a memory area that conforms to the type definition.

UserName in the above code is obviously an object type, and when we access userName, it may be the userName itself or the area of memory that userName points to.

For example:

Self.userName = @ "peak"

Is assigning a value to the pointer itself. And

[self.userName rangeOfString:@ "peak"]

Is in the memory area where the access pointer points to the string, which is not the same.

So we can roughly divide property into three categories:

After classifying, we need to understand the memory model of these three types of property.

Memory Layout

When we talk about multithread safety, we are actually talking about the security of multiple threads accessing a memory area at the same time. For the same area, we have two operations, load and store. When reading and writing occur in the same area at the same time, it is possible that multithreading is unsafe. Therefore, before we start the discussion, we need to understand the above three memory models of property, which can be illustrated as follows:

In a 64-bit system, for example, the pointer NSString* is an 8-byte region of memory, int count is a 4-byte region, and @ "Peak" is a region of memory depending on the length of the string.

When we access property, we are actually accessing three memory areas in the figure above.

Self.userName = @ "peak"

Is to modify the first area.

Self.count = 10

It's modifying the second area.

[self.userName rangeOfString:@ "peak"]

Is reading the third area.

Definition of insecurity

Now that we understand the types of property and their corresponding memory models, let's take a look at the definition of insecurity. This is what Wikipedia says:

A piece of code is thread-safe if it manipulates shared data structures only in a manner that guarantees safe execution by multiple threads at the same time

This definition still looks a bit abstract, and we can interpret multithreaded insecurity as an unexpected result of multithreaded access. This unexpected result contains several scenarios, not necessarily crash, which will be analyzed one by one later.

Let's take a look at how multithreading accesses memory at the same time. Regardless of CPU cache's cache of variables, memory access can be shown in the following figure:

As you can see from the figure above, we only have one address bus and one memory. Even in a multithreaded environment, it is impossible for two threads to access the same memory area at the same time, and memory access must be serially queued through an address bus, so before continuing, let's make a few conclusions clear:

Conclusion 1: the access to memory is serial, which will not lead to the confusion of memory data or the application of crash.

Conclusion 2: if the memory length of the load or store is less than or equal to the length of the address bus, then the read and write operation is atomic and completed at one time. For example, a single read and write of bool,int,long on a 64-bit system is atomic.

Next, let's take a look at the unsafe scenarios of multithreading one by one according to the above three categories of property.

Value type Property

Let's take the Bool value type as an example, when we have two threads accessing the following property:

@ property (nonatomic, assgin) BOOL isDeleted;//thread 1bool isDeleted = self.isDeleted;//thread 2self.isDeleted = false

Thread 1 and thread 2, one load and one store, access to BOOL isDeleted may be sequenced, but they must be serially queued. And because the BOOL size is only 1 byte, the address bus of the 64-bit system can support 8 bytes in length for read and write instructions, so we can think of BOOL read and write operations as atomic, so when we declare property of type BOOL, from an atomic point of view, there is no actual difference between using atomic and nonatomic (of course, if the getter method is overloaded).

What if it's int?

@ property (nonatomic, assgin) int count;//thread 1int curCount = self.count;//thread 2self.count = 1

Similarly, the int type is 4 bytes long, and both read and write can be done with one instruction, so both read and write operations are atomic in theory. There is no difference between nonatomic and atomic from a memory access point of view.

What on earth is the use of atomic? As far as I know, it is useful in two ways:

Use one: generate getter and setter for atomic operations.

After atomic is set, the default generated getter and setter method execution is atomic. That is, when we execute the getter method on thread 1 (create the call stack, return the address, exit the stack), thread B must wait for the getter method to complete if it wants to execute the setter method. For example, in a 32-bit system, if 64-bit double is returned through getter, the address bus width is 32 bits, and reading double from memory cannot be done through atomic operations. If it is not locked through atomic, setter operations may occur in other threads in the middle of reading, resulting in outliers. If such an exception occurs, multithreading unsafe occurs.

Use 2: set up Memory Barrier

For Objective C implementations, almost all locking operations end up setting memory barrier,atomic, which essentially locks getter,setter, so memory barrier is also set. The official documents are as follows:

Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.

What is the use of memory barrier?

Memory barrier can ensure the order of memory operations, according to the order in which our code is written. It sounds a little weird, but the fact is that the compiler will optimize our code and change the order of machine instructions that our code ends up translating in scenarios that it thinks are reasonable. That is, the code is as follows:

Self.intA = 0; / / line 1self.intB = 1; / / line 2

The compiler may execute line2 before line1 in some scenarios, because it believes that there is no dependency between An and B, although at the time of code execution, there is some dependency between intA and intB on another thread, which must require line1 to execute before line2.

If you set property to atomic, that is, after setting memory barrier, you can ensure that the execution of line1 must precede line2. Of course, this scenario is very rare. One is that variables are accessed across threads, and the other is that both conditions are indispensable due to compiler optimization. In this extreme scenario, atomic does make our code a little more multithreaded safe, but I haven't encountered such a scenario since I wrote iOS code, and the chances are that the compiler is smart enough to set up memory barrier where we need it.

Is the use of atomic must be multithreaded safe? Let's take a look at the following code:

@ property (atomic, assign) int intA;//thread Afor (int I = 0; I

< 10000; i ++) { self.intA = self.intA + 1; NSLog(@"Thread A: %d\n", self.intA);}//thread Bfor (int i = 0; i < 10000; i ++) { self.intA = self.intA + 1; NSLog(@"Thread B: %d\n", self.intA);} 即使我将intA声明为atomic,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1;不是原子操作,虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作,当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值。这种场景我们也可以称之为多线程不安全。 指针Property 指针Property一般指向一个对象,比如: @property (atomic, strong) NSString* userName; 无论iOS系统是32位系统还是64位,一个指针的值都能通过一个指令完成load或者store。但和primitive type不同的是,对象类型还有内存管理的相关操作。在MRC时代,系统默认生成的setter类似如下: - (void)setUserName:(NSString *)userName { if(_uesrName != userName) { [userName retain]; [_userName release]; _userName = userName; }} 不仅仅是赋值操作,还会有retain,release调用。如果property为nonatomic,上述的setter方法就不是原子操作,我们可以假设一种场景,线程1先通过getter获取当前_userName,之后线程2通过setter调用[_userName release];,线程1所持有的_userName就变成无效的地址空间了,如果再给这个地址空间发消息就会导致crash,出现多线程不安全的场景。 到了ARC时代,Xcode已经替我们处理了retain和release,绝大部分时候我们都不需要去关心内存的管理,但retain,release其实还是存在于最后运行的代码当中,atomic和nonatomic对于对象类的property声明理论上还是存在差异,不过我在实际使用当中,将NSString*设置为nonatomic也从未遇到过上述多线程不安全的场景,极有可能ARC在内存管理上的优化已经将上述场景处理过了,所以我个人觉得,如果只是对对象类property做read,write,atomic和nonatomic在多线程安全上并没有实际差别。 指针Property指向的内存区域 这一类多线程的访问场景是我们很容易出错的地方,即使我们声明property为atomic,依然会出错。因为我们访问的不是property的指针区域,而是property所指向的内存区域。可以看如下代码: @property (atomic, strong) NSString* stringA;//thread Afor (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.stringA = @"a very long string"; } else { self.stringA = @"string"; } NSLog(@"Thread A: %@\n", self.stringA);}//thread Bfor (int i = 0; i < 100000; i ++) { if (self.stringA.length >

= 10) {NSString* subStr = [self.stringA substringWithRange:NSMakeRange (0,10)];} NSLog (@ "Thread B:% @\ n", self.stringA);}

Although stringA is the property of atomic, and thread B makes a length judgment when fetching substring, thread B is still very easy to crash because self.stringA = @ "a very long string" when reading length one moment; thread An already says self.stringA = @ "string" when it takes substring the next moment; it immediately appears the Exception,crash of out of bounds, and multithreading is not safe.

In the same scenario, there are also times when manipulating collection classes, such as:

@ property (atomic, strong) NSArray* arr;//thread Afor (int I = 0; I

< 100000; i ++) { if (i % 2 == 0) { self.arr = @[@"1", @"2", @"3"]; } else { self.arr = @[@"1"]; } NSLog(@"Thread A: %@\n", self.arr);}//thread Bfor (int i = 0; i < 100000; i ++) { if (self.arr.count >

= 2) {NSString* str = [self.arr objectAtIndex:1];} NSLog (@ "Thread B:% @\ n", self.arr);}

By the same token, even if we make the count judgment before accessing objectAtIndex, thread B is still easy to crash because the area of memory pointed to by the arr between the two lines of code has been modified by other threads.

So you see, what you really need to worry about is the access to this kind of memory area, even if it is declared as atomic, it is useless. Most of the inexplicable multithreaded crash in App belongs to this category. Once you access this kind of memory area in a multithreaded scenario, you should be careful. How to avoid this kind of crash will be discussed later.

Summary of Property multithread safety:

In short, the function of atomic is to add a lock to getter and setter. Atomic can only ensure that the code is safe when it enters the getter or setter function. Once getter and setter are out, multithreading security can only be guaranteed by the programmer himself. So there is no direct relationship between the atomic attribute and multithreaded safety using property. In addition, locking in atomic also brings some performance loss, so when we write iOS code, we generally declare property as nonatomic, and add additional locks to synchronize when we need to do multi-thread safety.

How to achieve multi-thread safety?

Discussion here, in fact, how to achieve multi-thread safety is also relatively clear, the keyword is atomicity (atomicity), as long as atomic, as small as a primitive type variable access, up to a long section of code logic execution, atomic performance to ensure the serial execution of the code, to ensure that half of the code execution, there will not be another thread involved.

Atomicity is a relative concept, and the granularity of the object it aims at can be large or small.

For example, the following code:

If (self.stringA.length > = 10) {NSString* subStr = [self.stringA substringWithRange:NSMakeRange (0,10)];}

It's non-atomic.

But after locking:

/ / thread A [_ lock lock]; for (int I = 0; I

< 100000; i ++) { if (i % 2 == 0) { self.stringA = @"a very long string"; } else { self.stringA = @"string"; } NSLog(@"Thread A: %@\n", self.stringA);}[_lock unlock];//thread B[_lock lock];if (self.stringA.length >

= 10) {NSString* subStr = [self.stringA substringWithRange:NSMakeRange (0,10)];} [_ lock unlock]

The whole piece of code is atomic and can be considered multithreaded safe.

Another example is:

If (self.arr.count > = 2) {NSString* str = [self.arr objectAtIndex:1];}

It's non-atomic.

And

/ / thread A [_ lock lock]; for (int I = 0; I

< 100000; i ++) { if (i % 2 == 0) { self.arr = @[@"1", @"2", @"3"]; } else { self.arr = @[@"1"]; } NSLog(@"Thread A: %@\n", self.arr);}[_lock unlock]; //thread B[_lock lock];if (self.arr.count >

= 2) {NSString* str = [self.arr objectAtIndex:1];} [_ lock unlock]

Is atomic. Note that both reading and writing need to be locked.

This is why when we do multithreading safety, instead of adding the atomic keyword to property, we declare property as nonatomic (nonatomic has no lock overhead of getter,setter), and then lock it ourselves.

How do I use which lock?

There are many ways for iOS to lock code, and the common ones are:

@ synchronized (token)

NSLock

Dispatch_semaphore_t

OSSpinLock

All of these locks can bring atomicity, and the loss of performance is smaller from top to bottom.

My personal advice is that when writing application layer code, which one is easy to use except OSSpinLock. Compared with the performance differences of these locks, the correctness of code logic is more important. And the performance differences between them are imperceptible to users most of the time.

Of course, we will encounter a few scenarios that need to pursue the performance of the code, such as writing framework, or in scenarios where multiple threads read and write shared data frequently, we need to have a rough idea of the loss caused by locks.

There is a data in the official document, using Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5 test, the acquisition of mutex has about the wear and tear of 0.2ms, we can think that the loss caused by locks is about the level of ms.

Atomic Operations

In fact, in addition to various locks, there is another way to acquire atomicity on iOS. Using Atomic Operations is about an order of magnitude smaller than the loss of locks, and the use of these Atomic Operations can be seen in some third-party Framework code in pursuit of high performance. These atomic operation can be found in / usr/include/libkern/OSAtomic.h:

such as

_ intA + +

It's non-atomic.

And

OSAtomicIncrement32 (& (_ intA))

Is atomic and multithreaded safe.

Atomic Operation can only be applied to 32-bit or 64-bit data types, and locks are still used in scenarios where objects such as NSString or NSArray are used by multithreading.

Most Atomic Operation have two versions of OSAtomicXXX,OSAtomicXXXBarrier. Barrier is the memory barrier mentioned earlier. Using the version of Barrier when there are dependencies between multiple variables in multithreading ensures the correct order of dependencies.

For writing application layer multithread safe code, I still recommend that you use @ synchronized,NSLock or dispatch_semaphore_t. Multithreading safety is more important than multithreading performance. You should pursue the latter when the former is fully guaranteed and there is still room for the latter.

Try to avoid multithreaded design.

No matter how much code we have written, we must admit that multithreaded safety is a complex issue. As programmers, we should avoid multithreaded design as much as possible, rather than pursuing clever lock skills.

Later, I will write an article that introduces functional programming and its core ideas. Even if we use non-functional programming languages, such as Objective C, it can greatly help us avoid the problem of multi-thread safety.

These are all the contents of the article "sample Analysis of Property in iOS Multithreading". Thank you for reading! I believe we all have a certain understanding, hope to share the content to help you, if you want to learn more knowledge, welcome to follow the industry information channel!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report