To increase the responsiveness of the application, it is necessary to correctly break the execution of tasks into several threads. The set of technologies in the hands of the iOS developer is the following. The methods are represented by increasing the level of abstraction.
performSelector
and various parameters (for example, performSelectorOnMainThread:withObject:waitUntilDone:
. DocumentationNSInvocationOperation
and NSBlockOperation
.This article will talk about GCD issues.
Libdispatch is Apple's library for multi-threading. GCD was first introduced in Mac OS X 10.6. The source code for the libdispatch library, which implements GCD services, was released under the Apache license on September 10, 2009. There are also library versions for other Unix operating systems, such as FreeBSD and Linux. For the rest, there is no support. True, there are unofficial builds of libdispatch from users.
Let's talk about the internal structure of the library. We make an assumption on the basis of which technology it was developed. Options: pthreads, background selectors, NSThread. The second option is definitely not suitable - since the basis of libdispatch is working with blocks. Then from the assumptions remains NSThread or pthreads . Now consider in more detail.
It all started when a collection of header files of all libraries and protocols in Obj-C was discovered for one of the most recent versions of the operating system (at that time it was iOS 10). The project contains public frameworks - most of those with which almost all developers are familiar, from AVFoundation to WebKit. Surprisingly, even in public frameworks there are such properties and methods that are not available in the original Apple documentation. For example, the trustedTimestamp property of a CLLocation object.
Further, a large section of private libraries is found, for example, PhysicsKit. By the way, there is an interesting timeline for the life of private frameworks - I recommend reading it. It is worth it to spend a few hours and study the interesting and partially opened insides of iOS (do not rejoice too much, there are only generated header files). The rest is reserved for libraries and protocols. There are not so many libraries there, and their naming is similar: lib + name. For example, libobjc or libxpc. But there are so many protocols there that even github does not display them all.
And yes, among other things, libdispatch was discovered. As for the other libraries in the repository, for it there are only header files. Among them there are no hints on the library device. The generated header files for classes in most cases contain several standard methods, among which are: debugDescription
, description
, hash
and superclass
. In this case, the only option is to research the open source Apple.
Consider what the libdispatch repository consists of. These are source codes and header files of several levels. Levels include a public (what you used to think of as libdispatch), an internal and private level of access. It is worth paying attention to the documentation that is provided for the command line utility. Among other things, you can stumble upon the configuration files cmake and xcodeconfig, as well as tests in large quantities.
The most interesting places for us:
The library repository is actively maintained - regular commits from developers several times a month, which are actively repairing broken support and compilation on various platforms. The project is considered complete, and some problems, such as building a library on El Capitan , remain unresolved until now.
Consider what a queue in libdispatch is. The queue is defined by three macros, the definition can be found in the file - queue_internal.h .
Determination of the queue begins with the inclusion of DISPATCH_STRUCT_HEADER
- this is done for all objects of the project. This general header consists of the definition of OS_OBJECT_HEADER
( OS_OBJECT_HEADER
itself OS_OBJECT_HEADER
required for a virtual table of operations — vtable and reference counting), several fields, including the field of the target queue. The target queue (target queue) is represented by one of the base queues — usually the default queue.
Next, the queue is determined by the macros DISPATCH_QUEUE_HEADER
and DISPATCH_QUEUE_CACHELINE_PADDING
. The latter is needed to make sure that the structure fits optimally in the processor's cache line. The DISPATCH_QUEUE_HEADER
macro is used to define queue metadata that includes the width (the number of threads in the pool), the debugging number, the regular number, and the list of tasks to be executed.
The base type for the work is represented as continuation. It is defined as the inclusion of a single DISPATCH_CONTINUATION_HEADER
macro. The macro definition includes a pointer to an operation table, various flags, a priority, pointers to a context, functions, data, and the next operation.
By examining the private header files and library sources, it was discovered that libdispatch can be compiled using the libpqw library or the POSIX Thread API.
So, the latest version of GCD is built over a wrapper over the pthread library - libpwq , which also includes Apple. The main idea of ​​the library is to add a level of abstraction. The first version was released in 2011, at the moment the latest stable version is 0.9 from 2014.
The library is a direct add-on over <pthread.h>
, introducing a new level of abstraction. It involves working not with threads, but with task queues: creating, setting priorities, adding tasks for execution. For example, adding a task is done by calling pthread_workqueue_additem_np
, where the queue is passed, a pointer to the functions for the task and its arguments.
Inside the library, the main manager is a manager who operates with queues and a list of tasks. The manager always has at least one working queue. The queue is represented by a regular structure with an identifier, a priority (there are only three - high, low and default priority), various flags and a pointer to the first task. Tasks are organized as a list. The task itself is a structure with a function pointer, flags, arguments, and a pointer to the next task, if any.
Of course, it is possible to compile libdispatch without the libpwq library, in which case pthreads will be used. This is due to the fact that it was announced much earlier than the release of this library (on Mac OS X Snow Leopard in 2009).
Let's take a look at some existing solution in libdispatch as an example implementation. Take everyone's favorite call
DispatchQueue.main.async { // some asynchronous code... }
Actually implementation is trivial. The swift wrapper itself will be discussed later in this article. We can only say that CDispatch is a compiled GCD C library for the Swift project.
public class DispatchQueue : DispatchObject { ... } public extension DispatchQueue { ... public class var main: DispatchQueue { return DispatchQueue(queue: _swift_dispatch_get_main_queue()) } ... @available(OSX 10.10, iOS 8.0, *) public func async(execute workItem: DispatchWorkItem) { CDispatch.dispatch_async(self.__wrapped, workItem._block) } ... }
In the code section above, we see how the main queue is created and what the asynchronous code call is. Analysis of the GCD device under the hood will start from the well-known dispatch_async.
The basic call tree from the moment of launching an asynchronous task to the moment of creating a thread (pthread_create) or sending a task to a lower-level library (libpwq) will be as follows:
dispatch_async
_dispatch_continuation_async
_dispatch_continuation_async2
_dispatch_async_f2
_dispatch_continuation_push
dx_push
macro_dispatch_queue_push
_dispatch_queue_push_inline
dx_wakeup
macro_dispatch_queue_class_wakeup
_dispatch_queue_class_wakeup_with_override
_dispatch_queue_class_wakeup_with_override_slow
_dispatch_root_queue_push_override_stealer
_dispatch_root_queue_push_inline
_dispatch_global_queue_poke
_dispatch_global_queue_poke_slow
pthread_create
or pthread_workqueue_additem_np
Let's go through the structure of the most interesting challenges. Original dispatch_async
method:
void dispatch_async(dispatch_queue_t dq, dispatch_block_t work) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DISPATCH_OBJ_CONSUME_BIT; _dispatch_continuation_init(dc, dq, work, 0, 0, dc_flags); _dispatch_continuation_async(dq, dc); }
What is going on here? First, memory is allocated to a previously defined type - continuation. It is worth recalling the accepted concept, according to which _t
means a pointer to a _s
structure. In this case, somewhere in the header files there will be a definition (for example, typedef struct dispatch_queue_s *dispatch_queue_t;
). Secondly, we set flags to initialize this structure, which are transmitted along with the type of the block and the queue for the execution of the block instructions. For example, the fourth parameter determines the priority, which is set to 0 by default.
By allocating memory for the structure and initializing it, control is passed on to two functions ( _dispatch_continuation_async
and _dispatch_continuation_async2
). The first function is a noninline ( noinline
) stub for calling another already inline ( inline
) function, simultaneously dereferencing the flags, and checking for the presence of a barrier. The task of the second function is to perform the appropriate checks and send continuations for asynchronous execution to the queue. _dispatch_continuation_push
means using the _dispatch_continuation_push
function. This happens only if the queue is not full or in the absence of a barrier.
In the case of a barrier, control can be transferred to the _dispatch_async_f2
function, where the check is performed and the QoS level for the continuation is set - otherwise the priority. However, the following function is still called _dispatch_continuation_push
, which calls the macro dx_push
under it. The macro expands to a rather cumbersome construction, and ultimately this leads to a call to the _dispatch_queue_push_inline
function. Her unwrapped wrapper is intentionally skipped.
The main reason why functions are so often embedded in one un-recessed is a reduction in the number of calls, but at the same time control the complexity of the code. In this way, an acceptable balance is achieved. The cumbersome constructions with dereferencing pointers and their pointers easily fit into small functions, which the compiler then embeds into the call places. Well, deal with a smaller amount of data from the point of view of a person is always easier.
The _dispatch_queue_push_inline
function _dispatch_queue_push_inline
built on a large number of macros. Among the most interesting low-level constructions (which, by the way, are used throughout the entire libdispatch source code) are the following:
atomic_load_explicit
is in the standard library for atomic work and provides atomic pointer dereferencing. Any pointer logic in a project uses calls from the header file - <stdatomic.h>
;__builtin_expect()
and __builtin_unreachable()
functions, as well as other __builtin
constructs, are related to low-level optimizations for the compiler — to branch prediction .The main task of this function is to check for a queue overflow or for a blocked barrier and transfer control. The control then goes to the _dispatch_async_f_redirect
function and checks whether the continuation is redirected to the same queue. This function also updates the start and end of the queue - an atomic change of pointers.
This is followed by another dx_wakeup
macro - or the _dispatch_queue_class_wakeup
call. This is one of the main methods in which the processing of tasks in the queue. It checks the conditions of the barriers, the status of the queue, and in case of non-compliance with the conditions, the task can again go to the queue through the already known dx_push
.
If the conditions are met, the task is passed to the _dispatch_queue_class_wakeup_with_override
method, which is a wrapper over _dispatch_queue_class_wakeup_with_override_slow
with the task's priority change and the possibility of overwriting them. The presence of slow
in the name correlates with the mechanism of embedding functions - the logic is divided into several functions in order to simplify its support.
Next are calls that directly process tasks in a loop and contain a large amount of logic for interacting and setting pointers, checking various flags, and so on. During execution, the last call becomes pthread_create
or pthread_workqueue_additem_np
. Of course, if the dispatch is built without using libpwq, then the internal flow control manager comes into play, and its principle of operation is similar to that described above.
The rest of the calls are intentionally missed, since the goal of this description is to show that even in the case of a normal call of the code for asynchronous execution, this turns into multi-layered logic.
And now let's take a quick look at what features of the intermediate swift-library, which directly interacts with libdispatch. As you know, it appeared from the third version of Swift. It is a wrapper over the original libdispatch with the addition of nice swift enumerations and the imposition of functionality in extensions. All this, of course, is one of the main tasks of the library - providing a convenient API for working with GCD.
Let's start with a file in which the cumbersome types of libdispatch data turn into elegant Swift classes - Wrapper.swift
. This file can serve as a display of the entire project.
The general approach is to create simple wrappers for most objects. Objects of the original libdispatch, such as dispatch_group_t
or dispatch_queue_t
, are stored in wrapper objects in the __wrapped
property. Most functions make a single call directly to the functions of the original libdispatch over the __wrapped
properties.
Consider a simple example:
public class DispatchQueue : DispatchObject { // libdispatch internal let __wrapped:dispatch_queue_t ... final internal override func wrapped() -> dispatch_object_t { return unsafeBitCast(__wrapped, to: dispatch_object_t.self) } ... public func sync(execute workItem: ()->()) { // dispatch_sync(self.__wrapped, workItem) } ... }
On the other hand, there are calls that do not consist of one line. They bring up types, counting intermediate values, checking for the system version and calling the appropriate methods. It is also worth mentioning that direct calls to the libdispatch library methods are prohibited in the Private.swift
file. An example is given below. Therefore, you will not be able to write less than a swift
's new code (except, of course, old versions of the swift or the self-contained library libdispatch).
@available(*, unavailable, renamed:"DispatchQueue.async(self:group:qos:flags:execute:)") public func dispatch_group_async(_ group: DispatchGroup, _ queue: DispatchQueue, _ block: @escaping () -> Void) { fatalError() }
Total, the description about the principles of work of libdispatch turned out. Assumptions about its internal structure were confirmed. libdispatch is really built on the POSIX Thread API - as the most minimal API for working with multithreading.
The latest version of libdispatch uses a different library (libpwq), but the essence remains the same.
And now you have a question - why should we understand what is there at a low level? Understanding low-level things is similar to knowing basic concepts. With their help you will not do something quickly, but you will avoid stupid mistakes in the future.
Understanding and knowledge of low-level things will allow to solve non-trivial problems in this area. If you have to write something on pthread for iOS, you will now be prepared.
Source: https://habr.com/ru/post/332026/
All Articles