iOS多线程总结_iOS 多线程

iOS多线程总结_iOS 多线程Dispatch Source使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。 循环引用:…

类型 简介 实现语言 线程生命周期
pthread posix接口,适合跨平台开发,使用难度较大 c 手动管理
NSThread 面向对象,简单易用,可直接操作线程对象 oc 手动管理
GCD apple封装底层线程技术,充分利用CPU多核 c 自动管理
NSOperation 基于GCD实现的OC接口,比GCD更简单易用 oc 自动管理

pthread

pthread线程为posix接口,适合跨平台开发技术,基于c语言且需要手动管理,见《unix环境高级编程》

相关API如下:

pthread_create(pthread_t *pid, pthread_attr_t *attr, 
               	(void *)(*func)(void *arg), (void *)arg) 创建一个线程
pthread_exit() 终止当前线程
pthread_cancel() 中断另外一个线程的运行
pthread_join() 阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init() 初始化线程的属性
pthread_attr_setdetachstate() 设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate() 获取脱离状态的属性
pthread_attr_destroy() 删除线程的属性
pthread_kill() 向线程发送一个信号

pthread_once() 线程被执行一次,由系统控制,可用于创建线程关联数据pthread_key_t
pthread_key_create() 创建线程关联数据key, NSThread封装保存线程信息就使用了线程关联数据
pthread_setspecific() 设置线程关联数据
pthread_getspecific() 获取线程关联数据

线程关联数据: 用于绑定到特定线程来作为线程私有数据(所有线程共享进程空间,即也可以放到到线程关联数据,但每个线程可指定自己相应的key),如errno错误码就使用了线程关联数;NSThread封装pthread也使用了该结构,用来保存NSThread对象; 线程泄露pthread未设置detach模式,不使用pthread_join等待线程退出获取线程退出状态,就会导致线程泄露;见Thread Leaks

NSThread

NSThreadpthread的封装(见gnustep ./source/NSThread.m),面向对象技术;

  • 基于thread封装,添加面向对象概念,性能较差,偏向底层
  • 相对于GCD和NSOperation来说是较轻量级的线程开发
  • 使用比较简单,但是需要手动管理创建线程的生命周期、同步、异步、加锁等问题
    image.png

相应的API如下:

创建启动

//创建基于target:selector,进入就绪态,默认分离状态,线程退出由系统回收资源
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//基于block
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

//分离状态创建线程,进入就绪态,相当于调用[[NSThread alloc]initWithTarget: selector: object:]
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

//线程启动,将线程放入可调度线程池,具体启动时机由cpu调度
- (void)start API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//取消线程,只是内部标记线程处于取消状态,gnu实现中未使用pthread_cancel
- (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//线程退出,包装了pthread_exit,为类方法
+ (void)exit;

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

使用如下:

NSThread *thread = [[NSThread alloc]initWithBlock:^{
		NSLog(@"thread start");
}];
[thread start];
//或者使用detachNewThreadWithBlock
[NSThread detachNewThreadWithBlock:^{ NSLog(@"thread start"); }];

线程属性

@property (readonly, retain) NSMutableDictionary *threadDictionary;//线程字典
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses //线程堆栈返回地址
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols //线程堆栈

@property (nullable, copy) NSString *name;线程名称
@property NSUInteger stackSize ;//线程使用栈区大小,默认是512K,可设置堆栈大小
@property (readonly, getter=isExecuting) BOOL executing;//线程正在执行
@property (readonly, getter=isFinished) BOOL finished;//线程执行结束
@property (readonly, getter=isCancelled) BOOL cancelled;//线程是否可以取消
@property double threadPriority ; //优先级,封装pthread_getschedparam
@property NSQualityOfService qualityOfService ; // 线程优先级,read-only after the thread is started
          NSQualityOfServiceUserInteractive// 最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
          NSQualityOfServiceUserInitiated// 次高优先级,主要用于执行需要立即返回的任务
          NSQualityOfServiceDefault// 默认优先级,当没有设置优先级的时候,线程默认优先级
          NSQualityOfServiceUtility// 普通优先级,主要用于不需要立即返回的任务
          NSQualityOfServiceBackground// 后台优先级,用于完全不紧急的任务
@property (readonly) BOOL isMainThread 
@property (class, readonly) BOOL isMainThread // reports whether current thread is main
            
//获取/设定优先级(类方法)
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

通知

NSNotificationName const NSWillBecomeMultiThreadedNotification;
NSNotificationName const NSDidBecomeSingleThreadedNotification;
NSNotificationName const NSThreadWillExitNotification;

线程间通信

//主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;// equivalent to the first method with kCFRunLoopCommonModes
//指定线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array 
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait // equivalent to the first method with kCFRunLoopCommonModes
//后台线程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg

Example:子线程进行耗时操作,操作结束后再回到主线程去刷新 UI; NSThread

GCD

GCD(Grand Cental Dispatch)为apple多核处理下多线程编程技术,多线程编程更为简洁,且为系统级实现,由系统统一管理(不需要手动管理其生命周期),相比其他线程编程技术效率更高;

Dispatch queue

GCD指定了两种dispatch queue分发队列:串行队列(serial dispathc queue)并行队列(concurrent dispatch queue)

对于串行队列,顾名思义添加到队列中的任务会串行执行,且在一个线程,若使用dispatch_async则会新创建一个线程(除主队列外);若使用dispatch_sync则会使用调用线程;

对于并行队列,会多线程并发执行,且系统会根据队列任务数、处理器核心数、处理器负荷等当前系统的状态来决定并行处理的处理数;但若使用dispatch_sync同步执行,则会使用当前调用线程同步执行;

『主线程』中,『不同队列』+**『不同任务』**简单组合的区别:

image.png

『不同队列』+『不同任务』 组合,以及
『队列中嵌套队列』 使用的区别:

image.png

注意避免死锁的情况,如主线程中,同步执行主队列任务;

//主线程中调用
1.
dispatch_sync(dispatch_get_main_queue(), ^{
	NSLog(@"main queue, sync task done");
});
2.
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(main_queue, ^{
  dispatch_sync(main_queue, ^{
    NSLog(@"main queue, sync task done");
  });
});

image.png

dispatch_sync源码实现流程在主线程中执行并添加到主队列同步执行,会阻塞执行到如下:

//关键代码如下:
_dispatch_queue_push(dq, (void *)&dbss);
dispatch_semaphore_wait(dbss2.dbss2_sema, DISPATCH_TIME_FOREVER);
_dispatch_put_thread_semaphore(dbss2.dbss2_sema);

会阻塞等待信号量,但主线程后续队列无法执行,因此无法释放信号量导致一直阻塞,进而引发“死锁”;见dispatch_sync死锁问题研究

dispatch_queue_create

//label,队列名称,推荐使用appid
//attr, 分为DISPATH_QUEUE_SERIAL(该值即为NULL)串行队列或者DISPATCH_QUEUE_CONCURRENT并行队列
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

//DISPATCH_SWIFT_UNAVAILABLE("Can't be used with ARC")
//ARC模式下不能使用该函数释放,及ARC模式下为自动释放
void dispatch_release(dispatch_object_t object);

对于dispatch_release释放队列函数,苹果官方文档已说明:ARC模式下且macos10.8+ ios6.0+无需手动释放,且不能释放主队列及全局队列;对于需要手动释放的,则无需关注仍在队列中未完成的任务,因为block任务会dispatch_retain自动持有该队列(即使调用了dispatch_release),也存在引用计数的概念;

If your app is built with a deployment target of macOS 10.8 and later or iOS v6.0 and later, dispatch queues are typically managed by ARC, so you do not need to retain or release the dispatch queues.

Your application does not need to retain or release the global (main and concurrent) dispatch queues; calling this function on global dispatch queues has no effect.

默认存在main dispatch queue主队列(主线程执行的队列,因主线程只有一个,该队列为串行队列)和global dispatch queue全局队列,且全局队列存在四个不同的等级:

image.png

dispatch_set_target_queue

*dispatch_queue_create创建的队列默认等级为默认优先级的全局队列等级一样;*因此需要修改队列的等级使用dispatch_set_target_queue

//object, 指定修改的队列
//queue, 指定目标队列
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t _Nullable queue);

Example:

dispatch_queue_t queue = dispatch_queue_create("myqueue", NULL);
dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));

不可指定主队列及全局队列的优先级!

若多个串行队列指定为同一个目标队列,则原先并行执行的串行队列就会串行执行,且在同一个线程;

dispatch_after

dispatch_after只是负责指定时间后添加任务到队列中,具体的任务执行由系统去调度;

//when类型为dispatch_time_t,可通过dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)获取
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

dispatch_group

对于并发队列并行执行无法有效获取何时结束,dispatch_group可对于同一group下的队列所有任务完成后,再将指定的任务添加到指定队列(包括group下的队列),可汇合所有任务完成节点;

dispatch_group_t dispatch_group_create(void);//创建group
//异步执行添加指定队列的任务
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
//异步等待(不会阻塞当前线程)指定队列添加的任务执行完成(与执行该函数顺序无关,即dispatch_group_async可在该函数后面添加任务)后,添加任务到指定任务
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
//阻塞当前线程,一直等待或者等待指定timeout时间所有任务完成
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
//dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
//dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
//只有任务数为0时,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务
//使用下面函数,可不需要使用dispatch_group_async,使用dispatch_async
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);

dispatch_barrier_async

dispatch_group不同,dispatch_barrier_async会等待调用此函数前所有并行队列添加的任务完成后,执行该函数添加的任务,然后恢复并行队列的正常行为;

image.png

dispatch_apply

dispatch_apply是按照指定次数添加任务到指定并发队列中,并阻塞等待所有任务完成,类似dispatch_sync或者dispatch_group_wait

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));//传入iterations序号

dispatch_once

dispatch_once保证添加的任务只会被执行一次,为线程安全,常用在单例或者整个程序只执行一次的代码;

- (void)shared {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行 1 次的代码(这里面默认是线程安全的)
    });
}

dispatch_supend dispatch_resume

void dispatch_suspend(dispatch_object_t object);
void dispatch_resume(dispatch_object_t object);

dispatch_suspend可以挂起正在执行的队列,但已添加到队列并且执行的任务不受影响,挂起后,队列中尚未执行的任务就会停止执行,需要调用dispatch_resume恢复才可以,也采用计数概念,dispatch_supend会计数+1, dispatch_resume会计数-1,只有计数为0时,才会完全恢复队列中的任务;

dispatch_semphore信号量

见《ios锁》

dispatch source

dispatch source封装了kqueue用来监听内核事件,如下:

image.png

kqueue事件继承自FreeBSD,用于监听内核事件,与
epoll类似,通过
epoll_wait
kevent系统调动阻塞等待事件,但不像
select需要轮训(也可以一直阻塞),并且不需要每次
select调用时从用户空间拷贝文件描述符至内核空间,还有不会线性扫描文件描述符数组,而是通过在内核注册事件回调来监听事件发生,因此在文件描述符较多时优势明显;

Select、poll、Epoll、KQueue区别 epoll内核源码分析 OSX/iOS中多路I/O复用总结 具体API如下:

dispatch_source_t dispatch_source_create(dispatch_source_type_t type,//事件类型 uintptr_t handle,//内核监听的句柄,如套接字、文件描述符 unsigned long mask,// dispatch_queue_t _Nullable queue);
void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);
void dispatch_source_set_timer(dispatch_source_t source,//间隔定时器 dispatch_time_t start,//定时器起始时间dispatch_time()或者使用dispatch_wall_time uint64_t interval,//重复间隔时间,可以使用DISPATCH_TIME_FOREVER不重复 uint64_t leeway);//延迟时间

//source默认是暂停状态,需要启动或者挂起
void dispatch_resume(dispatch_object_t object);

Dispatch Source使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。

Dispatch Source定时器使用时也有一些需要注意的地方,不然很可能会引起crash

  1. 循环引用:因为dispatch_source_set_event_handler回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel取消timer。

  2. dispatch_resumedispatch_suspend调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH(“Over-resume of an object”)导致崩溃。

  3. source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)后再重新创建;

  4. 当我们使用dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时;

  5. 对于后台线程(不是主线程,前台线程),dispatch timer不受RunLoop影响,但NSTimer 是始终需要 Runloop 支持的;见iOS定时器,你真的会使用吗?

  6. dispatch_supen为挂起Timer,需要和dispatch_resume平衡使用;而dispatch_source_cancel取消定时器;

  7. dispatch_source在ARC模式下超过作用域会自动释放,会导致计时器不生效,需要强持有,如在dispatch_source_set_event_handler里面持有timer

Dispatch Source Timer 的使用以及注意事项 深入浅出 GCD 之 dispatch_source iOS多线程:『GCD』详尽总结

NSOperation NSOperationQueue

NSOperation NSOperationQueueGCD的高级封装的面向对象的技术,可实现添加依赖关系、设定执行的优先级、取消执行操作,比GCD更简单易用、代码可读性更高,使用KVO观察对操作执行状态的更改,如isExcuting、isFinished、isCancelled;

NSoperation

NSOperation任务(或者操作)类似GCD中的block执行路径代码,该类为抽象类,子类分别为NSInvocationOperationNSBlockOperation和自定义NSOpeartion子类;

//NSInvocationOperation
//操作会在主线程执行,若操作中添加其他线程操作,则在其他线程执行
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
- (void)start;

//NSBlockOperation
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
//添加额外操作,blockOperationWithBlock操作完成后执行,可添加多个;
//操作执行线程是否开启新的线程,由操作个数及系统来决定
- (void)addExecutionBlock:(void (^)(void))block;
- (void)start;

//自定义子类
- (void)main;//需要重写该方法
- (void)start;//启动

NSOperationQueu

NSOperationQueue操作队列,类似GCD队列,分为主队列和自定义队列;

  • 主队列:

    添加到主队列的操作,都会在主线程执行,除非操作中新开启线程;

    NSOperationQueue mainQueue = [NSOprationQueu mainQueue];
    
    //添加操作
    - (void)addOperation:(NSOperation *)op;
    - (void)addOperationWithBlock:(void (^)(void))block
    
  • 自定义队列

    添加的操作会自动开启子线程执行;同时包含了串行和并行执行;

    NSOperationQueue queue = [NSOprationQueue alloc]init];
    

    串行还是并行的关键属性:maxConcurrentOperationCount(最大并行操作数),最大并行操作最大数为一个队列同时并发执行操作的最大数,而且操作并非只在一个线程执行(如最大并行数为1,则队列串行执行,但多个操作不一定都在同一个线程执行,但保证只在一个线程执行);

    • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。

    • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。

    • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。

NSOperation操作依赖

NSOperation NSOperationQuque最大的吸引点就是添加操作依赖,可以很方便的控制操作的执行顺序,具体的接口如下:

- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
@property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。

注意:需要addDependency后再addOperation,否则无法添加操作依赖关系!

NSOperation优先级

NSOperatio提供了queuePriority优先级属性,NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。

// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

操作执行顺序:首先保证依赖被执行,其次再根据优先级决定执行顺序;

NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性);

那么,什么样的操作才是进入就绪状态的操作呢?

  • 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行。

  • 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。
  • 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。

iOS多线程:『NSOperation、NSOperationQueue』详尽总结

Demo

github.com/FengyunSky/…

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13705.html

(0)

相关推荐

  • 分享一个Linq和Expression的通用查询设计思想[通俗易懂]

    分享一个Linq和Expression的通用查询设计思想[通俗易懂]一般来说所有的系统都离不开查询,系统的查询无非都是通过实体的属性作为条件进行查询,那我们有什么方法可以拼装成类似sql中的where条件呢?在.

    2023-06-27
    132
  • oracle检查表是否存在 然后删除_存储过程和自定义函数的区别

    oracle检查表是否存在 然后删除_存储过程和自定义函数的区别oracle删除表,如果表不存在,就报错,在跑大型脚本(脚本长且耗时的时候)比较麻烦,一般希望的是点开始然后脚本运行到结束,不可能一直盯着屏幕等弹出提示手工点掉,mysql就很好有drop table

    2022-12-17
    138
  • python学习(python学习路线图)

    python学习(python学习路线图)零基础情况下,想学一门语言。其实python非常适合初学者入门。相比较其他不少主流编程语言,有更好的可读性,因此上手相对容易。那么如何快速掌握python?

    2023-11-30
    121
  • python中lightgbm(Python 中)

    python中lightgbm(Python 中)Numpy是Python的一个科学计算的库,提供了矩阵运算的功能,一般与Scipy、matplotlib一起使用。导入numpy的范例如下:import numpy as npprint np.version.version1.6.2

    2023-10-25
    138
  • Python实现Kill进程的方法

    Python实现Kill进程的方法在计算机中,进程是程序在执行时的实例。一个进程可以包含多个线程,在不同的线程中执行不同的代码。在某些情况下,我们需要终止进程,以确保系统资源不被浪费。在Linux和Unix等系统中,通过使用Kill命令可以终止运行的进程。Kill命令可以向特定进程发送一个信号,请求其终止运行。

    2024-08-31
    21
  • SQL Server将多行数据合并为一行「终于解决」

    SQL Server将多行数据合并为一行「终于解决」一、引言 需求描述:现实工作中,有一些很特别的需求:在一个汇总表中,需要显示明细数据。因为是在汇总表中,所以明细数据只能显示在某一列中,这个列,就是多行数据合并为一行之后的结果。 案例描述:比如,在物

    2023-05-15
    136
  • Python 函数参数

    Python 函数参数当定义一个函数时,可以给函数的参数设置默认值。当调用该函数时,如果没有给该参数传入值,则默认使用设置的默认值。

    2024-03-06
    94
  • 蜀山禁洞(禁洞)

    蜀山禁洞(禁洞)

    2023-10-02
    142

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注