NSRunLoop Internals

本文深入探讨了NSRunLoop的工作原理,通过伪代码形式解析了其核心方法的实现过程,包括如何处理输入源、定时器及运行模式等关键特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自出处:https://mikeash.com/pyblog/friday-qa-2010-01-01-nsrunloop-internals.html


Friday Q&A 2010-01-01: NSRunLoop Internals
by  Mike Ash  

It's the first Friday of the new year, and that means it's time for the first Friday Q&A of 2010. This week, I'm taking Dave DeLong's suggestion of talking about NSRunLoop internals.

If you want to understand something as thoroughly as possible, you should build one yourself. That's a little too much for a blog post, so rather than build a complete implementation of NSRunLoop, I'm going to take the second-best route and plan out the key features of its internals in pseudocode.

CoreFoundation
On the Mac, NSRunLoop sits on top of its CoreFoundation equivalent, CFRunLoop. Most of the smarts are there, and the Cocoa side of things is mostly a wrapper. For this discussion, I am going to ignore this layering, and examine NSRunLoop as a single, standalone entity. From this perspective, CFRunLoop can be considered to be an implementation detail.

Autorelease Pools
One of the basic things that NSRunLoop does in manage autorelease pools, both for itself and for any code that it calls. Since autorelease pools are fairly straightforward compared to the rest, and will just serve to clutter things up, I will ignore this aspect of NSRunLoop's functionality.

Fundamentals
Most of the mysteriousness in NSRunLoop is in its various run methods. What goes on in there? How does it all work?

The -run method is pretty simple, since the documentation describes it in terms of -runMode:beforeDate::

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.

Its implementation must therefore look something pretty close to:

    - (void)run
    {
        while([self hasSourcesOrTimers])
            [self runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
    }
The  -runUntilDate:  method is similar:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate: until the specified expiration date.

Its implementation would then be like this:

    - (void)runUntilDate: (NSDate *)limitDate
    {
        while([self hasSourcesOrTimers])
        {
            [self runMode: NSDefaultRunLoopMode beforeDate: limitDate];
            
            // check limitDate at the end of the loop to ensure that
            // the runloop always runs at least once
            if([limitDate timeIntervalSinceNow] < 0)
                break;
        }
    }
That was easy enough. How about  -runMode:beforeDate: , then? Well, that's where all the complication lies.

Input Sources
As described in Apple's Run Loops programming guide, a run loop contains two types of sources: inputs and timers. An input source is basically some kind of external signal from outside the runloop itself.

In Mac OS X, input sources are mach ports. While things like NSFileHandle and CFFileDescriptor may give the appearance of connecting non-mach-port things into the runloop, this is actually fake! They monitor their file descriptor sources on a dedicated thread, then signal back to the runloop over a mach port.

(This may have changed in 10.6, which now has APIs which are capable of monitoring both mach ports and file descriptors at the same time. However, the fundamental fact remains that mach ports are the major input source used on OS X.)

Most people's eyes glaze over when they hear about mach ports. They're not very well known, nor well documented. And personally, I don't know them all that well myself. Because of this, I'm going to explore an alternate NSRunLoop which uses file descriptors as its input sources instead. The fundamentals are the same, and file descriptors are more readily understandable to most people.

Quick refresher: what is a file descriptor, or FD for short? An FD is an object (not in the Objective-C sense, but in the conceptual sense) which you can either read from, write to, or both read and write. An FD can have data available for reading, space available for writing, or neither. This particular state of an FD can change over time. For example, imagine an FD which represents a socket communicating with another application. When that other application writes to the socket, the FD in your application will have data available for reading. If that FD is an input source for a run loop, that run loop will wake up and process that source. Likewise, if the other application reads from the socket, the FD in your application will have space available for writing, and this will also wake up the run loop and process that source. This is one of the fundamental tasks of a run loop.

A run loop needs to monitor multiple input sources at a time. There are several APIs for doing this on OS X, but the one I'm going to use here is select(2).

I won't go into details on how to use select (pseudocode, remember?), but the basics are pretty easy: you give it three sets of FDs, which you want to monitor for reading, writing, and errors. It then returns whenever there's activity, and the three sets contain those FDs which have had that sort of activity on them.

Thus, we can see the first pass at how -runMode:beforeDate: would work. I'm going to simplify things a bit further and ignore the fact that select takes three different sets of FDs, and just use one. The idea is just that we're interested in activity on these input sources.

And remember, I'm doing pseudocode, so don't expect this to look 100% like real Objective-C.

The first thing is to check if there are any sources. According to the documentation, this method immediately returns NO if not:

    - (BOOL)runMode: (NSString *)mode beforeDate: (NSDate *)limitDate
    {
        if(![self hasSourcesOrTimers])
            return NO;
Next, create an empty FD set:
        fd_set fdset;
        FD_ZERO(&fdset);
Then, set each input source's FD within the set. I assume that the input source class has a  -fileDescriptor  method that returns the FD it wants to monitor:
  
        for(inputSource in [self inputSources])
            FD_SET([inputSource fileDescriptor], &fdset);
Now call  select . Remember, to simplify, I'm pretending that it only takes one file descriptor set rather than three. I'm also ignoring all error checking:
        select(fdset, NULL);
Once it returns, check each input source to see if it's ready for processing now. I iterate over a copy of the input sources because the code that the input source executes may modify the set of input sources:
        for(inputSource in [[[self inputSources] copy] autorelease])
            if(FD_ISSET([inputSource fileDescrptor], &fdset))
                [inputSource fileDescriptorIsReady];
The documentation states that this method returns  YES  the runloop was run in any way, so that's the last thing to do here:
        return YES;
    }
Modes
So far so good, but it has a way to go. This method completely ignores its parameters! First, we'll look at the  mode parameter.

Just what is the mode parameter, anyway? A mode is essentially a grouping of input and timer sources. Different sources are active in different modes. NSRunLoop has NSDefaultRunLoopMode, which as the name would expect is where most sources are added. In Cocoa, you also have secondary modes like NSEventTrackingRunLoopMode, which is used when the mouse is held down on a control. By switching to this mode, sources which were only added to the default mode will not fire, which prevents unwanted code from running while the user is in the middle of making a menu selection or moving a slider. Sources which need to fire during event tracking can be added to that mode. Sources which need to fire in both circumstances can be added to both.

You can then imagine NSRunLoop containing an instance variable for input sources like this:

    NSMutableDictionary *_inputSources; // maps modes to NSMutableSets
NSRunLoop 's method to add an input source is called  -addPort:forMode: , and its implementation would then look like this:
    - (void)addPort: (NSPort *)aPort forMode: (NSString *)mode
    {
        NSMutableSet *sourcesSet = [_inputSources objectForKey: mode];
        if(!sourcesSet)
        {
            // this is the first time anything has used this mode
            // so create a new set for it
            sourcesSet = [NSMutableSet set];
            [_inputSources setObject: sourcesSet forKey: mode];
        }
        [sourcesSet addObject: aPort];
    }
Similarly for the removal method:
    - (void)removePort: (NSPort *)aPort forMode: (NSString *)mode
    {
        NSMutableSet *sourcesSet = [_inputSources objectForKey: mode];
        [sourcesSet removeObject: aPort];
        
        // this isn't strictly necessary, but keeps us from leaking
        // sets if the caller uses a lot of one-time "throwaway" modes
        // (which it probably never would)
        if(![sourcesSet count])
            [_inputSources removeObjectForKey: mode];
    }
And then the run method needs to be changed to match:
    - (BOOL)runMode: (NSString *)mode beforeDate: (NSDate *)limitDate
    {
        if(![self hasSourcesOrTimersForMode: mode])
            return NO;
        
        fd_set fdset;
        FD_ZERO(&fdset);
        
        for(inputSource in [_inputSources objectForKey: mode])
            FD_SET([inputSource fileDescriptor], &fdset);
        
        select(fdset, NULL);
        
        for(inputSource in [[[_inputSources objectForKey: mode] copy] autorelease])
            if(FD_ISSET([inputSource fileDescrptor], &fdset))
                [inputSource fileDescriptorIsReady];
        
        return YES;
    }
Timeout
This code still ignores one parameter,  limitDate . The purpose of this parameter is to force the method to return even if no input sources were ready. It functions as a timeout. To make this work, the code simply computes the timeout and passes it as the last parameter to  select  (which in reality requires a more complicated timeout structure, not just an  NSTimeInterval , but remember, pseudocode!):
     - (BOOL)runMode: (NSString *)mode beforeDate: (NSDate *)limitDate
    {
        if(![self hasSourcesOrTimersForMode: mode])
            return NO;
        
        fd_set fdset;
        FD_ZERO(&fdset);
        
        for(inputSource in [_inputSources objectForKey: mode])
            FD_SET([inputSource fileDescriptor], &fdset);
        
        NSTimeInterval timeout = [limitDate timeIntervalSinceNow];
        select(fdset, timeout);
        
        // if the timeout was hit, there may not be
        // any active input sources, but this loop
        // will simply do nothing if that's the case
        for(inputSource in [[[_inputSources objectForKey: mode] copy] autorelease])
            if(FD_ISSET([inputSource fileDescrptor], &fdset))
                [inputSource fileDescriptorIsReady];
        
        return YES;
    }
Timer Sources
This implementation deals with input sources and the timeout parameter well enough, but completely ignores timers.

As with input sources, I'll assume an instance variable which holds timers. And like input sources, timers are grouped into modes:

    NSMutableDictionary *_timerSources; // maps modes to NSMutableSets
I'll skip over the implementation of  -addTimer:forMode: , as it should be pretty obvious and is basically identical to  -addPort:forMode: .

Adding timer support to the above code is relatively straightforward. The list of timers can be consulted to find the one that fires earliest. If that time is earlier than limitDate, then it gets to be the timeout instead of limitDate. After select runs, check the list of timers to see if any of them are ready to fire, and fire the ones that are.

There's one wrinkle, which is that a timer firing does not make -runMode:beforeDate: return. If a timer fires, it should be processed, and then control should return back to select. This continues until an input source fires. If an input source does fire, the method still needs to check the list of timers and fire any that are ready, because otherwise a busy input source could prevent timers from ever running.

Given all of that, here's what the code looks like with timer support:

     - (BOOL)runMode: (NSString *)mode beforeDate: (NSDate *)limitDate
    {
        if(![self hasSourcesOrTimersForMode: mode])
            return NO;
        
        // with timer support, this code has to loop until an input
        // source fires
        BOOL didFireInputSource = NO;
        while(!didFireInputSource)
        {
            fd_set fdset;
            FD_ZERO(&fdset);
            
            for(inputSource in [_inputSources objectForKey: mode])
                FD_SET([inputSource fileDescriptor], &fdset);
            
            // the timeout needs to be set from the limitDate
            // and from the list of timers
            // start with the limitDate
            NSTimeInterval timeout = [limitDate timeIntervalSinceNow];
            
            // now run through the list of timers and set the
            // timeout to the smallest one found in them and
            // in the limitDate
            for(timer in [_timerSources objectForKey: mode])
                timeout = MIN(timeout, [[timer fireDate] timeIntervalSinceNow]);
            
            // now run select
            select(fdset, timeout);
            
            // process input sources first (this choice is arbitrary)
            for(inputSource in [[[_inputSources objectForKey: mode] copy] autorelease])
                if(FD_ISSET([inputSource fileDescrptor], &fdset))
                {
                    didFireInputSource = YES;
                    [inputSource fileDescriptorIsReady];
                }
            
            // now process timers
            // responsibility for updating fireDate for repeating timers
            // and for removing the timer from the runloop for non-repeating timers
            // rests in the timer class, not in the runloop
            for(timer in [[[_timerSources objectForKey: mode] copy] autorelease])
                if([[timer fireDate] timeIntervalSinceNow] <= 0)
                    [timer fire];
            
            // see if we timed out, if so, abort!
            // this is checked at the end to ensure that timers and inputs are
            // always processed at least once before returning
            if([limitDate timeIntervalSinceNow] < 0)
                break;
        }
        return YES;
    }
And that covers all of the necessary functionality. The final code is pretty straightforward and understandable.

Conclusion
What does this exercise tell us? We have to be careful not to take too much away from this pseudocode, as there's no guarantee that it matches Apple's. In fact, I know of one case that recently bit me where it does not: Apple's implementation will only fire one pending timer for each pass through the run loop, even if multiple timers are ready to fire, whereas this code will fire all pending timers once before returning. And of course there's the major difference that Apple's code uses mach ports, not file descriptors, although their semantics are similar.

Despite this problem, a lot can be learned from this sort of exercise. For example, run loop modes are a common point of confusion among Cocoa programmers, and writing all of this stuff out helps to make it clear just what a mode is and how it works.

It can also inform speculation about the implementation of other parts of Cocoa. For example, we can deduce how the -performSelector:withObject:afterDelay: method works on the inside. Since a run loop only handles sources and timers, it must use one of those two. Since it activates after a delay, it must use a timer. Watching how it behaves in the debugger will confirm this to be correct. As another example, we can conclude that -performSelectorOnMainThread:withObject:waitUntilDone: must use a mach port, since it can't manipulate a timer on the main thread from a secondary thread. (NSRunLoop is not thread safe.)

All in all, this kind of technique is really useful in general. I don't usually take it as far as writing out detailed pseudocode like this, but thinking about how some Apple code might be implemented can really help further understanding of how it works and what the documentation says, as well as what it implies but does not say directly. You have to be careful to ensure that your conclusions are ultimately based on documentation and real-world constraints, and not the peculiarities of your particular idea of how it might work, but that just takes a bit of care.

It's also helpful just to demystify a class. It's easy to get into magical thinking, where you see a class as being incomprehensible and elevated above the mortal plane. The fact is, while the particular implementations of some of these classes can be pretty sophisticated (the CFRunLoop source will make your eyes bleed), the basics of what they do and how they do it are usually very straightforward. For 99.9% of the APIs in Cocoa, they aren't there because they're doing something amazing that you could never achieve, but rather they simply exist to save you the time and trouble of having to write it all yourself.

That's it for this week. Check back in seven days for another exciting edition. Until then, keep sending in your ideas for topics. Friday Q&A is reader-driven, and the more topic ideas I get, the better this series will become.

Did you enjoy this article? I'm selling a whole book full of them. It's available for iBooks and Kindle, plus a direct download in PDF and ePub format. It's also available in paper for the old-fashioned.  Click here for more information.



内容概要:本文从关键概念、核心技巧、应用场景、代码案例分析及未来发展趋势五个维度探讨了Python编程语言的进阶之路。关键概念涵盖装饰器、生成器、上下文管理器、元类和异步编程,这些概念有助于开发者突破基础认知的核心壁垒。核心技巧方面,介绍了内存优化、性能加速、代码复用和异步处理的方法,例如使用生成器处理大数据流、numba库加速计算密集型任务等。应用场景展示了Python在大数据处理、Web开发、人工智能和自动化运维等多个领域的广泛运用,特别是在FastAPI框架中构建异步API服务的实战案例,详细分析了装饰器日志记录、异步数据库查询和性能优化技巧。最后展望了Python的未来发展趋势,包括异步编程的普及、类型提示的强化、AI框架的深度整合以及多语言协同。 适合人群:已经掌握Python基础语法,希望进一步提升编程技能的开发者,特别是有意向从事数据科学、Web开发或AI相关工作的技术人员。 使用场景及目标:①掌握Python进阶概念和技术,如装饰器、生成器、异步编程等,提升代码质量和效率;②学习如何在实际项目中应用这些技术,如通过FastAPI构建高效的异步API服务;③了解Python在未来编程领域的潜在发展方向,为职业规划提供参考。 阅读建议:本文不仅提供了理论知识,还包含了丰富的实战案例,建议读者在学习过程中结合实际项目进行练习,特别是尝试构建自己的异步API服务,并通过调试代码加深理解。同时关注Python社区的发展动态,及时掌握最新的技术和工具。
内容概要:本文档《Rust系统编程实战》详细介绍了Rust在系统编程领域的应用,强调了其内存安全、零成本抽象和高性能的特点。文档分为三个主要部分:核心实战方向、典型项目案例和技术关键点。在核心实战方向中,重点讲解了unsafe编程、FFI(外部函数接口)和底层API调用,涉及操作系统组件开发、网络编程、设备驱动开发、系统工具开发和嵌入式开发等多个领域,并列出了每个方向所需的技术栈和前置知识。典型项目案例部分以Linux字符设备驱动为例,详细描述了从环境搭建到核心代码实现的具体步骤,包括使用bindgen生成Linux内核API的Rust绑定,定义设备结构体,以及实现驱动核心函数。 适合人群:对系统编程有兴趣并有一定编程基础的开发者,尤其是那些希望深入了解操作系统底层机制、网络协议栈或嵌入式系统的工程师。 使用场景及目标:①掌握Rust在不同系统编程场景下的应用,如操作系统组件开发、网络编程、设备驱动开发等;②通过实际项目(如Linux字符设备驱动)的学习,理解Rust与操作系统内核的交互逻辑;③提高对unsafe编程、FFI和底层API调用的理解和运用能力。 阅读建议:由于文档内容较为深入且涉及多个复杂概念,建议读者在学习过程中结合实际操作进行练习,特别是在尝试实现Linux字符设备驱动时,务必按照文档提供的步骤逐步进行,并多加调试和测试。
内容概要:本文针对现有配电网灵活性评估方法对网络传输能力考虑不足的问题,提出了一种新的评估方法。该方法首先建立了配电网灵活性供需模型,分析了4种供需匹配情况,接着提出3类灵活性评估指标,构建了以运行成本最低为目标的优化调度模型。通过改进的IEEE33节点配电网仿真验证了方法的有效性。重点解决了高比例分布式电源接入带来的波动性问题,为配电网灵活性评估提供了新思路。文中还详细介绍了MATLAB代码实现,涵盖参数初始化、灵活性需求和供给计算、评估指标计算、优化调度模型及可视化结果等方面。此外,对灵活性供需匹配的4种情况进行深入分析,并扩展实现了完整的灵活性评估系统,增加了动态时间尺度、增强可视化和实用扩展等功能,提升了系统的可扩展性和实用性。; 适合人群:从事电力系统研究、配电网规划与运营的专业人士,特别是关注分布式电源接入和电网灵活性评估的研究人员和技术人员。; 使用场景及目标:①评估含高比例分布式电源的配电网灵活性,解决DG接入带来的波动性问题;②通过优化调度模型最小化运行成本,提高配电网的运行效率;③利用扩展实现的系统进行多时间尺度仿真和不同场景下的对比分析,支持实际工程应用。; 其他说明:此资源不仅提供了详细的理论分析和MATLAB代码实现,还通过模块化设计增强了代码的可扩展性和实用性。建议读者结合具体配电网参数调整设备容量约束,根据当地电价政策优化成本系数,并采用历史数据训练更精确的场景生成模型。同时,可以通过并行计算加速仿真过程,采用交叉验证和蒙特卡洛仿真验证结果的稳定性和鲁棒性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值