并发编程 - 锁(@synchronized)

news2024/9/20 20:43:21

引言

在前面的博客中,我们已经讨论了锁在多线程编程中的重要性,主要是为了解决多线程同时访问同一片共享数据时发生的竞争条件(race conditions),导致数据不一致和崩溃问题。

并且介绍了一个在Objective-C中,最常见也最简单的锁机制——属性修饰符atomic,它能够有效地防止资源竞争,保障数据安全。我们深入讨论了它的应用场景以及局限性,原子属性通常作用于单个变量,而对于多个数据或者数组时并不起作用。那么这个时候我们就需要引入另一个在Objective-C中的强大同步工具——@synchronized。

@synchronized简介

作用

@synchronized的主要作用是确保某个代码在同一时刻只被一个线程执行,从而避免多个线程同时访问共享资源时发生数据竞争或不一致问题。

举例来说,如果有多个线程同时读取和修改一个共享变量,可能会导致数据损坏或程序崩溃。通过@synchronized,我们可以将对共享资源的访问限制为一次只有一个线程可以操作。

@synchronized为开发者提供了一种简洁且易用的同步机制。相比于手动管理锁(比如NSLock),@synchronized通过一行代码就可以实现锁定和解锁操作,即使在代码块抛出异常或出现错误时,也会确保锁被正常释放,避免了手动管理的繁琐和容易出错的地方,这种自动管理机制为代码健壮性提供了保障,简化了编程过程,减少了出错的可能性,对于初学者来说非常直观并且方便使用。

使用场景

@synchronized在许多常见的多线程编程场景中都非常有用,尤其是需要保护共享资源或临近区的代码时,比如并发读写共享数据、对数据完整完整性要求较高的应用场景:

  • 多线程访问同一数据库或文件系统。
  • 并发处理网络请求中的共享缓存或配置数据。
  • 管理计数器、日志记录等共享状态。

注意事项

尽管@synchronized可以有效解决线程安全问题,但过度使用或不当使用可能会引发性能瓶颈。同步操作会阻塞其他线程的执行,尤其是在高并发场景下,因此开发者应该谨慎选择使用场景。它在中小规模的应用程序中表现得很好,但在需要高性能和大并发量时,可能需要考虑更轻量的同步机制(如GCD的信号量,NSLock)。

@synchronized使用

基本用法

@synchronized的使用很简单,它的锁是基于对象的,只有在锁定的对象上才能进行同步。使用时将指定对象放到括号内即可。

@synchronized(object) {
    // 临界区代码
}

我们需要注意括号内的对象不能为空,也不能随便创建一个对象就用,因为它的生命周期不确定。通常我们会使用self,self的生命周期比较长,会让这更简单方便。它的存储和释放都在同一个链表内进行操作。

保证数据安全

我们同样使用上一个博客中的案例,这次我们仍然使用nonatomic来修饰address属性,然后通过GCD创建多个并发异步任务来打印这个属性的值,我们已经知道这大概率会导致崩溃,但这次我们让代码在@synchronized同步代码块中执行。

@property (nonatomic, strong) NSString * address;
//MARK: @synchronized
- (void)synchronizedTest {
    for (int i = 0; i < 1000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self) {
                self.address = [NSString stringWithFormat:@"河北省唐山市路北区%d", i];
                NSLog(@"%@", self.address);
            }
        });
    }
}

执行该方法后,我们可以发现即使是使用nonatomic修饰符修饰的属性,也顺利执行完了所有循环。因为@synchronized确保了在代码块中的所有代码在同一时刻只能被一个线程执行。

保持数据一致

在上一篇博客中我们列举了这样一个案例,有一个HPUser实体类,类中有两个属性firstName和lastName,然后服务类可以对其进行数据更新,虽然我们使用属性修饰符atomic保证了数据安全,但是仍然存在一些问题,当有两个不同的线程更新用户时,名称分别为Bob Taylor和Alice Darji,可能会出现Alice Taylor或者Bob Darji的情况。因为atomic并不能保证复合数据的一致性。

这个时候我们使用@synchronized将更新用户信息的方法放在代码块中进行执行,用户所有的相关状态都在同一个事物中批量更新。

@implementation HPUpdaterService

- (void)updateUser:(HPUser *)user properties:(NSDictionary *)properties{
    @synchronized(user){//取得针对user对象的锁。一切相关的修改都会被一同处理,而不会发生竞争状态
         NSString * fn = [properties objectForKey:@“firstName”];
        if(fn != nil){
            user.firstName = fn;
        }
        NSString * ln = [properties objectForKey:@“lastName”];
        if(fn != nil){
            user.lastName = ln;
        }
    }
}

@end

这样不管有多少条线程来更新用户信息,在同一个时刻都有且只有一条线程能够进入代码块执行更新代码。这就保证了用户信息的一致性。

另外在本例中,我们通过user对象来获取锁,保证了当user对象不同时,该方法仍然能够高并发地执行,这既实现了高并发,又设置了警戒以防止数据冲突。

可以看到获取锁的对象是良好定义临界区的关键。作为经验法则,可以选择状态会被访问和修改的对象作为信号量的引用。

可变数组的操作

在多线程环境中操作可变数组NSMutableArray,多线程会同时往数组中添加或删除元素。如果不进行同步操作,可能会导致数组状态不一致甚至是崩溃,即使是使用atomic修饰的数组也保证不了在多线程中添加和删除元素的安全。

未使用@synchronized的多线程操作数组

我们首先来创建一个共享数组的管理类,并添加两个方法用于添加元素和移除元素。

#import "PHArrayManager.h"

@interface PHArrayManager ()

/// 共享数组
@property(nonatomic,strong)NSMutableArray * sharedArray;

@end

@implementation PHArrayManager

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.sharedArray = [NSMutableArray array];
    }
    return self;
}

//MARK: 添加元素
- (void)addObject:(id)object {
    [self.sharedArray addObject:object];
    NSLog(@"添加元素成功:%@",object);
}

//MARK: 删除元素
- (void)removeObject:(id)object {
    [self.sharedArray removeObject:object];
    NSLog(@"删除元素成功:%@",object);
}

@end

然后在多线程环境下使用它来添加元素,我们仍然采用了GCD的方式获取一个全局并发队列来创建一个多线程的场景。

//MARK: 操作共享数组
- (void)arrayTest {
    PHArrayManager * arrayManager = [[PHArrayManager alloc] init];
    for (int i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [arrayManager addObject:[NSString stringWithFormat:@"河北省唐山市路北区%d", i]];
        });
    }
}

执行后会发现,大概率会发生程序崩溃。

另外如果多线程读取的话,还可能会发生数组越界的情况。

使用@synchronized的多线程操作数组

接下来我们使用@synchronized将对共享数组的操作都放到代码块中来执行。

//MARK: 添加元素
- (void)addObject:(id)object {
    @synchronized (self.sharedArray) {
        [self.sharedArray addObject:object];
        NSLog(@"添加元素成功:%@",object);
    }
}

//MARK: 删除元素
- (void)removeObject:(id)object {
    @synchronized (self.sharedArray) {
        [self.sharedArray removeObject:object];
        NSLog(@"删除元素成功:%@",object);
    }
}

在执行同样的操作,我们发现所有的代码都可以顺利执行。

@synchronized源码

我们执行clang之后会发现其实@synchronized就两句代码:

objc_sync_enter(_sync_obj);

 objc_sync_exit(_sync_obj);

objc_sync_enter方法的源码如下:

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }
    return result;
}

从源码中我们可以看得出锁对象是不能传递nil的。

objc_sync_exit()方法源码如下:

// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
     
    return result;
}

其中有一个SyncData,它的结构如下:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

可以看到SyncData是一个结构体,并且是一个单向链表。

recursive_mutex_t事实上是一把递归锁,但是只能在单线程中使用。

但是由于有threadCount的存在,synchronized不仅是一把递归锁,还可以在多线程中使用。

结语

在多线程编程中,确保数据的完整性和线程安全始终是一个重要的挑战。通过本篇博客我们可以看到如果不采取适当的同步措施,多个线程同时访问和修改共享资源可能导致崩溃和数据不一致。而@synchronized为我们提供了一种简洁而有效的解决方案,自动处理锁定和解锁,避免了手动管理锁的繁琐。

虽然@synchronized不是性能最高的同步工具,但在代码简洁性和易用性方面,它无疑是初学者和中小型项目中的练好选择。在并发编程中,了解并选择适当的同步机制能够极大提高应用程序的可靠性和稳定性。希望本文能够帮助你更好地理解@synchronized及其在多线程环境中的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2146160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql笔记7(单表查询)

文章目录 1. select① 从伪表里查数据(可以结合第3点dual理解数据来源)select 文字 ;做计算&#xff1a;select 算式;select 文字( 或算式) as 别名;(as 用于起别名&#xff0c;可省略) ② 从真实表里查数据select * from 表名;select 字段名&#xff0c;字段名 from 表名; 2. …

centos远程桌面连接windows

CentOS是一款广泛使用的Linux发行版&#xff0c;特别是在服务器领域。很多企业和个人用户会选择远程连接到CentOS进行操作和维护。虽然CentOS自带了一些远程桌面解决方案&#xff0c;但它们在使用上存在一些局限性。接下来&#xff0c;我将介绍如何实现CentOS的远程桌面连接&am…

关于wordPress中的用户登录注册等问题

前言 大家在做类似的功能的时候&#xff0c;有没有相关的疑问。那就是我都已经选择好了相应的主题和模版&#xff0c;但是为什么都没有用户注册和用户登录的页面存在呢&#xff1f; WordPress默认情况下不提供用户注册和登录功能的原因是它最初是作为一个博客平台开发的&…

OCR两篇革命之作

DocOwl2 参考 阿里8B模型拿下多页文档理解新SOTA&#xff0c;324个视觉token表示一页&#xff0c;缩减80% mPLUG-DocOwl 2聚焦多页文档理解&#xff0c;兼顾效果和效率&#xff0c;在大幅缩减单页视觉token的前提下实现了多页文档理解的SOTA效果。 仅用324个token表示文档图…

离散制造 vs 流程制造:锚定精准制造未来,从装配线到化学反应,实时数据集成在制造业案例中的多维应用

使用 TapData&#xff0c;化繁为简&#xff0c;摆脱手动搭建、维护数据管道的诸多烦扰&#xff0c;轻量替代 OGG, Kettle 等同步工具&#xff0c;以及基于 Kafka 的 ETL 解决方案&#xff0c;「CDC 流处理 数据集成」组合拳&#xff0c;加速仓内数据流转&#xff0c;帮助企业…

使用雷达速度因子进行越野导航的鲁棒高速状态估计

使用雷达速度因子进行越野导航的鲁棒高速状态估计 Morten Nissov 1 , 2 ^{1,2} 1,2, Jeffrey A. Edlund 1 ^{1} 1, Patrick Spieler 1 ^{1} 1, Curtis Padgett 1 ^{1} 1, Kostas Alexis 2 ^{2} 2 和 Shehryar Khattak 1 ^{1} 1 摘要 在复杂环境中实现机器人自主性以用于关键…

【限流算法】

文章目录 介绍算法原理适用场景令牌通算法实现限流算法 介绍 令牌桶算法是网络流量整形&#xff08;Traffic Shaping&#xff09;和速率限制&#xff08;Rate Limiting&#xff09;中最常使用的一种算法。典型情况下&#xff0c;令牌桶算法用来控制发送到网络上的数据的数目&a…

第6天:趋势轮动策略开发(年化18.8%,大小盘轮动加择时)

原创内容第655篇&#xff0c;专注量化投资、个人成长与财富自由。 轮动策略是一种投资策略&#xff0c;它涉及在不同的资产类别、行业或市场之间进行切换&#xff0c;以捕捉市场机会并优化投资组合的表现。 这种策略的核心在于识别并利用不同资产或市场的相对强弱&#xff0c…

[数据集][目标检测]智慧养殖场肉鸡目标检测数据集VOC+YOLO格式3548张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;3548 标注数量(xml文件个数)&#xff1a;3548 标注数量(txt文件个数)&#xff1a;3548 标注…

医学数据分析实训 项目九 糖尿病风险预测

文章目录 综合实践二 糖尿病遗传风险预测一、分析目标二、实现步骤三、数据准备四、特征工程五、模型构建六、性能度量七、提交要求 综合实践任务二 糖尿病遗传风险预测代码&#xff08;一&#xff09;数据准备&#xff08;二&#xff09;特征工程&#xff08;三&#xff09;模…

Selenium通过ActionBuilder模拟鼠标操作直接移动到指定坐标的注意事项

在目前&#xff08;2024-09-18&#xff09;得Selenium官方手册中&#xff0c;模拟鼠标操作基本上都是通过ActionChains完成的&#xff0c;唯独有一动作&#xff0c;是通过ActionBuilder完成的。 而前者ActionChains&#xff0c;主要是通过offset&#xff0c;也就是坐标偏移量来…

【Prometheus】jmx_prometheus_javaagent监控java应用

目录 一、概述 1.1 promethues简介 1.2 JMX Exporter简介 二、监控SparkHistoryServer实现 一、概述 1.1 promethues简介 promethues采集数据的方法很多&#xff0c;常用的是通过各种exporter去主机采集&#xff0c;然后有些程序是没有相关的exporter,所以有些时候会通过脚…

信息学奥赛报考指南

近年来&#xff0c;信息学奥林匹克竞赛&#xff08;NOI&#xff09;越来越受到家长和学生的重视。这项竞赛不仅能培养孩子的编程与算法思维&#xff0c;还为优秀的选手提供了进入国内顶尖大学的保送资格&#xff0c;并有机会参加国际级赛事。因此&#xff0c;许多家长都希望了解…

设计图纸加密方法知多少?小编给你讲清楚

一、对称加密 使用对称加密算法&#xff0c;对设计图纸进行加密。对称加密使用相同的密钥进行加密和解密&#xff0c;确保只有持有正确密钥的人能够解密文件。 二、非对称加密 使用非对称加密算法&#xff0c;进行设计图纸的加密。非对称加密使用公钥加密、私钥解密的方式&a…

机械设备产品资料方案介绍小程序系统开发制作

设备产品资料介绍小程序系统&#xff0c;是一家工业机械设备生产厂家为了更好的服务客户而定制开发的一套小程序系统&#xff0c;让用户通过小程序就可以了解公司产品介绍的详细参数、售后服务和产品操作手持等。 该小程序系统里面主要开发的功能模块有&#xff1a; 1、产品目…

如何在算家云搭建DynamiCrafter(图生视频)

一、模型简介 DynamiCrafter 是一种&#xff08;文本-&#xff09;图像到视频/图像动画的方法&#xff0c;旨在从条件图像和文本提示中生成短视频片段&#xff08;约 2 秒&#xff09;&#xff0c;可以将静止图像转换为动画。它使用了一种称为视频扩散先验的技术&#xff0c;可…

探索Facebook的黑暗面:数字化社交的双面剑

Facebook作为全球最大的社交平台&#xff0c;改变了我们的沟通和互动方式。虽然它带来了便利&#xff0c;但也存在不少隐忧。本文将探讨Facebook的负面影响&#xff0c;包括隐私问题、信息操控、心理健康危机及社交表面化等。 一、隐私问题&#xff1a;数据收集的隐忧 Facebo…

优思学院|如何从零开始自己学习六西格玛?

优思学院为学习六西格玛管理的学员&#xff0c;精心推荐了几本由浅入深、系统全面的书籍&#xff0c;帮助大家从入门到精通&#xff0c;逐步掌握六西格玛这一强大的管理工具。无论你是刚接触六西格玛的初学者&#xff0c;还是想在专业领域提升的高级学员&#xff0c;这几本书都…

硬件(驱动开发概念)

驱动程序开发 裸机驱动&#xff08;无操作系统&#xff09; Linux驱动 以计算机技术为基础&#xff0c;在软件和硬件层间可以被剪裁的专业硬件计算机系统 SOC&#xff1a;片上系统 Kernel&#xff1a;内核 x86 &#xff08;CISC:complex instruction set computer 复杂指令…

IEEE Electronic Library(IEL)数据库文献检索下载介绍及个人获取IEEE文献途径

一、数据库介绍 IEEE&#xff08;The Institute of Electrical and Electronics Engineers&#xff0c;电气电子工程师学会&#xff09;是目前全球最大的非营利性专业技术学会&#xff0c;在全球160多个国家拥有超过45万名会员。IEEE在电气电子、计算机、半导体、通讯、电力能…