I've been working at this problem for a few days and none of my solutions have been adequate. I'm lacking the theoretical knowledge to make this happen, I think, and would love some advice (does not have to be iOS specific--I can translate C, pseudocode, whatever, into what I need).
我研究这个问题已经有几天了,我的解决方案中没有一个是足够的。我认为,我缺乏实现这一目标的理论知识,我希望得到一些建议(不一定非要是针对iOS的——我可以将C、伪代码等转换为我所需要的)。
Basically, I have two iPhones. Either one can trigger a repeating action when the user presses a button. It then needs to notify the other iPhone (via the MultiPeer framework) to trigger the same action...but they both need to start at the same instant and stay in step. I really need to get 1/100sec accuracy, which I think is achievable on this platform.
基本上,我有两个iphone。当用户按下按钮时,任何一个都可以触发重复操作。然后它需要通知另一个iPhone(通过多端框架)触发相同的动作……但他们都需要在同一时刻开始,并保持步调一致。我真的需要1/100秒的准确度,我认为在这个平台上是可以做到的。
As a semi-rough gauge of how well in synch I am, I use AudioServices to play a "tick" sound on each device...you can very easily tell by ear how well in synch they are (ideally you would not be able to discern multiple sound sources).
作为一个半粗略的标准,我使用AudioServices在每个设备上播放“滴答”声……你可以很容易地通过耳朵判断它们的同步程度(理想情况下,你不能识别多个声源)。
Of course, I have to account for the MultiPeer latency somehow...and it's highly variable, anywhere from .1 sec to .8 sec in my testing.
当然,我必须考虑到多端延迟……它是高度可变的,从0。1秒到0。8秒。
Having found that the system clock is totally unreliable for my purposes, I found an iOS implementation of NTP and am using that. So I'm reasonably confident that the two phones have an accurate common reference for time (though I haven't figured out a way to test this assumption short of continuously displaying NTP time on both devices, which I do, and it seems nicely in synch to my eye).
在发现系统时钟完全不可靠后,我找到了NTP的iOS实现,并正在使用它。所以我有理由相信这两款手机有一个准确的时间参考(虽然我还没有找到一种方法来测试这两种设备上的NTP时间,但我做了,而且它似乎很好地与我的眼睛同步)。
What I was trying before was sending the "start time" with the P2P message, then (on the recipient end) subtracting that latency from a 1.5sec constant, and performing the action after that delay. On the sender end, I would simply wait for that constant to elapse and then perform the action. This didn't work at all. I was way off.
我之前尝试的是用P2P消息发送“开始时间”,然后(在接收端)从1.5秒常量中减去延迟,并在延迟之后执行操作。在发送端,我只需等待该常量过期,然后执行操作。这根本不起作用。我是路要走。
My next attempt was to wait, on both ends, for a whole second divisible by three, Since latency always seems to be <1sec, I thought this would work. I use the "delay" method to simply block the thread. It's a cudgel, I know, but I just want to get the timing working period before I worry about a more elegant solution. So, my "sender" (the device where the button is pressed) does this:
我的下一个尝试是,在两端,等待整整一秒,可以被3整除,因为延迟总是小于1秒,我认为这是可行的。我使用“delay”方法直接阻塞线程。我知道这是一种工具,但我只是想在考虑一个更优雅的解决方案之前得到工作时间。因此,我的“发送者”(按下按钮的设备)会这样做:
-(void)startActionAsSender
{
[self notifyPeerToStartAction];
[self delay];
[self startAction];
}
And the recipient does this, in response to a delegate call:
接受者这样做,作为对委托调用的回应:
-(void)peerDidStartAction
{
[self delay];
[self startAction];
}
My "delay" method looks like this:
我的“延迟”方法如下:
-(void)delay
{
NSDate *NTPTimeNow = [[NetworkClock sharedInstance] networkTime];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [calendar components:NSSecondCalendarUnit
fromDate:NTPTimeNow];
NSInteger seconds = [components second];
// If this method gets called on a second divisible by three, wait a second...
if (seconds % 3 == 0) {
sleep(1);
}
// Spinlock
while (![self secondsDivideByThree]) {}
}
-(BOOL)secondsDivideByThree
{
NSDate *NTPTime = [[NetworkClock sharedInstance] networkTime];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSInteger seconds = [[calendar components:NSSecondCalendarUnit fromDate:NTPTime]
second];
return (seconds % 3 == 0);
}
1 个解决方案
#1
5
This is old, so I hope you were able to get something working. I faced a very similar problem. In my case, I found that the inconsistency was almost entirely due to timer coalescing, which causes timers to be wrong by up to 10% on iOS devices in order to save battery usage.
这是旧的,所以我希望你能做点什么。我遇到了一个非常相似的问题。在我的例子中,我发现这种不一致几乎完全是由于计时器合并造成的,这导致iOS设备上的计时器错误高达10%,以节省电池的使用。
For reference, here's a solution that I've been using in my own app. First, I use a simple custom protocol that's essentially a rudimentary NTP equivalent to synchronize a monotonically increasing clock between the two devices over the local network. I call this synchronized time "DTime" in the code below. With this code I'm able to tell all peers "perform action X at time Y", and it happens in sync.
作为参考,我在自己的应用程序中使用了一个解决方案。首先,我使用了一个简单的自定义协议,它本质上相当于一个基本的NTP协议,在本地网络上同步两个设备之间单调递增的时钟。在下面的代码中,我将这个同步时间称为“DTime”。有了这段代码,我就可以告诉所有的同伴“在Y时刻执行X操作”,并且它是同步的。
+ (DTimeVal)getCurrentDTime
{
DTimeVal baseTime = mach_absolute_time();
// Convert from ticks to nanoseconds:
static mach_timebase_info_data_t s_timebase_info;
if (s_timebase_info.denom == 0) {
mach_timebase_info(&s_timebase_info);
}
DTimeVal timeNanoSeconds = (baseTime * s_timebase_info.numer) / s_timebase_info.denom;
return timeNanoSeconds + localDTimeOffset;
}
+ (void)atExactDTime:(DTimeVal)val runBlock:(dispatch_block_t)block
{
// Use the most accurate timing possible to trigger an event at the specified DTime.
// This is much more accurate than dispatch_after(...), which has a 10% "leeway" by default.
// However, this method will use battery faster as it avoids most timer coalescing.
// Use as little as necessary.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, dispatch_get_main_queue());
dispatch_source_set_event_handler(timer, ^{
dispatch_source_cancel(timer); // one shot timer
while (val - [self getCurrentDTime] > 1000) {
// It is at least 1 microsecond too early...
[NSThread sleepForTimeInterval:0.000001]; // Change this to zero for even better accuracy
}
block();
});
// Now, we employ a dirty trick:
// Since even with DISPATCH_TIMER_STRICT there can be about 1ms of inaccuracy, we set the timer to
// fire 1.3ms too early, then we use an until(time) { sleep(); } loop to delay until the exact time
// that we wanted. This takes us from an accuracy of ~1ms to an accuracy of ~0.01ms, i.e. two orders
// of magnitude improvement. However, of course the downside is that this will block the main thread
// for 1.3ms.
dispatch_time_t at_time = dispatch_time(DISPATCH_TIME_NOW, val - [self getCurrentDTime] - 1300000);
dispatch_source_set_timer(timer, at_time, DISPATCH_TIME_FOREVER /*one shot*/, 0 /* minimal leeway */);
dispatch_resume(timer);
}
#1
5
This is old, so I hope you were able to get something working. I faced a very similar problem. In my case, I found that the inconsistency was almost entirely due to timer coalescing, which causes timers to be wrong by up to 10% on iOS devices in order to save battery usage.
这是旧的,所以我希望你能做点什么。我遇到了一个非常相似的问题。在我的例子中,我发现这种不一致几乎完全是由于计时器合并造成的,这导致iOS设备上的计时器错误高达10%,以节省电池的使用。
For reference, here's a solution that I've been using in my own app. First, I use a simple custom protocol that's essentially a rudimentary NTP equivalent to synchronize a monotonically increasing clock between the two devices over the local network. I call this synchronized time "DTime" in the code below. With this code I'm able to tell all peers "perform action X at time Y", and it happens in sync.
作为参考,我在自己的应用程序中使用了一个解决方案。首先,我使用了一个简单的自定义协议,它本质上相当于一个基本的NTP协议,在本地网络上同步两个设备之间单调递增的时钟。在下面的代码中,我将这个同步时间称为“DTime”。有了这段代码,我就可以告诉所有的同伴“在Y时刻执行X操作”,并且它是同步的。
+ (DTimeVal)getCurrentDTime
{
DTimeVal baseTime = mach_absolute_time();
// Convert from ticks to nanoseconds:
static mach_timebase_info_data_t s_timebase_info;
if (s_timebase_info.denom == 0) {
mach_timebase_info(&s_timebase_info);
}
DTimeVal timeNanoSeconds = (baseTime * s_timebase_info.numer) / s_timebase_info.denom;
return timeNanoSeconds + localDTimeOffset;
}
+ (void)atExactDTime:(DTimeVal)val runBlock:(dispatch_block_t)block
{
// Use the most accurate timing possible to trigger an event at the specified DTime.
// This is much more accurate than dispatch_after(...), which has a 10% "leeway" by default.
// However, this method will use battery faster as it avoids most timer coalescing.
// Use as little as necessary.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, dispatch_get_main_queue());
dispatch_source_set_event_handler(timer, ^{
dispatch_source_cancel(timer); // one shot timer
while (val - [self getCurrentDTime] > 1000) {
// It is at least 1 microsecond too early...
[NSThread sleepForTimeInterval:0.000001]; // Change this to zero for even better accuracy
}
block();
});
// Now, we employ a dirty trick:
// Since even with DISPATCH_TIMER_STRICT there can be about 1ms of inaccuracy, we set the timer to
// fire 1.3ms too early, then we use an until(time) { sleep(); } loop to delay until the exact time
// that we wanted. This takes us from an accuracy of ~1ms to an accuracy of ~0.01ms, i.e. two orders
// of magnitude improvement. However, of course the downside is that this will block the main thread
// for 1.3ms.
dispatch_time_t at_time = dispatch_time(DISPATCH_TIME_NOW, val - [self getCurrentDTime] - 1300000);
dispatch_source_set_timer(timer, at_time, DISPATCH_TIME_FOREVER /*one shot*/, 0 /* minimal leeway */);
dispatch_resume(timer);
}