随想

谈多线程

原理

线程是什么

线程是操作系统中独立的执行单元,用于执行程序中的代码。每个线程都包含以下内容:

  1. 栈:每个线程都有自己的栈空间,用于存储局部变量、函数调用信息、返回地址等。栈空间是线程私有的,因此线程之间不能直接访问对方的栈空间。
  2. 寄存器:线程在执行代码时会使用寄存器来存储和操作数据。每个线程都有自己的寄存器上下文,用于存储寄存器中的值。
  3. 线程 ID:每个线程都有一个唯一的标识符,称为线程 ID。线程 ID 用于区分不同的线程,并且可以用于在代码中识别当前正在执行的线程。
  4. 线程状态:线程可以处于不同的状态,例如就绪、运行、阻塞等。线程状态取决于操作系统调度器的行为,操作系统会根据调度算法将就绪状态的线程分配给 CPU 运行,阻塞状态的线程等待某些事件发生后再转为就绪状态。
  5. 其他资源:线程可能还包含其他的资源,例如打开的文件、网络连接等。这些资源可能需要在多个线程之间共享或同步访问。

总之,线程是一个独立的执行单元,它包含了执行代码所需的所有信息,例如栈、寄存器、线程 ID、状态和其他资源。线程的创建、调度和销毁都由操作系统负责。

进程是什么

进程是操作系统中的一个概念,它是一个独立的执行环境,包含了正在执行的程序的所有信息。每个进程都有自己的地址空间、代码、数据、堆栈、打开的文件、网络连接等资源。

在操作系统中,进程是并发执行的基本单位。操作系统通过进程间的切换来实现多任务并发执行。当操作系统启动一个程序时,它会为该程序创建一个新的进程,该进程会拥有自己的执行环境,并在其中执行程序代码。

每个进程都有一个唯一的标识符,称为进程 ID,用于区分不同的进程。进程还可以有父进程和子进程之间的关系,子进程通常是由父进程创建的。

进程间可以通过不同的机制进行通信和同步,例如管道、信号量、共享内存等。进程还可以通过操作系统提供的进程管理接口来控制进程的创建、销毁、调度等。

总之,进程是操作系统中的一个概念,它是一个独立的执行环境,包含了正在执行的程序的所有信息,例如地址空间、代码、数据、堆栈、资源等。进程是操作系统中实现多任务并发执行的基本单位,可以通过不同的机制进行通信和同步。

线程进程的区别

线程和进程是操作系统中的两个核心概念,它们有以下几个区别:

  1. 资源分配:进程是操作系统中资源分配的基本单位,每个进程都有自己的地址空间、代码、数据、堆栈、打开的文件、网络连接等资源。而线程则是进程中执行代码的基本单位,多个线程共享进程的地址空间和资源。
  2. 独立性:进程是独立的执行环境,不同的进程之间相互独立,一个进程出现错误不会影响其他进程。而线程是在进程内部执行的,多个线程共享同一进程的地址空间和资源,因此一个线程的错误可能会影响整个进程的稳定性。
  3. 调度和切换:进程是操作系统调度和分配资源的基本单位,进程间的切换需要保存和恢复整个进程的状态。而线程是在进程内部调度和分配资源的基本单位,线程间的切换只需要保存和恢复线程的上下文。
  4. 轻量级和高效性:线程是轻量级的执行单元,线程的创建和销毁比进程快,切换也比进程快。线程共享进程的地址空间和资源,因此线程间的通信和同步比进程间的通信和同步更高效。

总之,进程和线程都是操作系统中的重要概念,它们分别用于描述程序执行环境的不同层次。进程是资源分配的基本单位,线程是执行代码的基本单位;进程是相互独立的执行环境,线程共享进程的资源;进程切换需要保存和恢复整个进程的状态,而线程切换只需要保存和恢复线程的上下文;线程是轻量级的执行单元,切换和通信更高效。

并发(Concurrency)和并行(Parallelism )

Concurrency is when two or more tasks can start, run, and complete in overlapping time periods. It doesn’t necessarily mean they’ll ever both be running at the same instant. For example, multitasking on a single-core machine.

Parallelism is when tasks literally run at the same time, e.g., on a multicore processor.

协程

协程(Coroutine)是一种用户级线程,也称为轻量级线程。协程可以看作是一种特殊的函数,可以在其内部使用 yield 操作将控制权交还给调用者,并在之后的某个时间点继续执行。与传统线程不同,协程的切换由程序员手动控制,因此可以实现更加灵活高效的任务切换和调度。

协程的本质是一种状态机,用于保存函数执行的上下文和状态,并支持在不同的时间点进行状态的保存和恢复。在协程中,yield 操作用于将当前协程的执行权交还给调用者,并保存当前协程的上下文和状态;而 resume 操作用于恢复之前保存的协程的状态和上下文,并将其执行权交还给操作系统。

协程的使用可以帮助程序员实现高效的任务调度和并发执行,同时减少线程切换的开销。在一些需要高并发和高吞吐量的场景中,如网络编程、图形处理、游戏开发等领域,协程已经成为一种非常流行的编程模型。在一些编程语言中,如 Go、Lua、Python、C# 等中都提供了协程的原生支持,以方便程序员使用。

应用

Java

Java 中的线程是基于 Thread 类实现的。除了 Thread 类外,Java 还提供了一些其他的线程相关类和接口,包括 Runnable、Callable、ThreadGroup、Executor、ExecutorService 等。

  1. Thread 类:Thread 类是 Java 中表示线程的基本类,可以通过继承 Thread 类或实现 Runnable 接口来创建新的线程。Thread 类提供了一些方法来控制线程的状态、优先级、中断等,例如 start()、sleep()、interrupt() 等。
  2. Runnable 接口:Runnable 接口定义了一个 run() 方法,用于描述线程的执行代码。可以通过实现 Runnable 接口来创建线程,通常需要将实现了 Runnable 接口的类传递给 Thread 类的构造函数。
  3. Callable 接口:Callable 接口类似于 Runnable 接口,但它可以返回执行结果,并可以抛出异常。Callable 接口定义了一个 call() 方法,用于描述线程的执行代码。可以通过实现 Callable 接口和使用 FutureTask 类来创建线程。
  4. ThreadGroup 类:ThreadGroup 类用于管理线程组,可以将多个线程放入一个线程组中,方便对线程进行统一的管理和控制。
  5. Executor 接口:Executor 接口定义了一个 execute() 方法,用于提交一个任务给执行器。Executor 接口的实现可以是线程池,也可以是直接执行提交的任务。
  6. ExecutorService 接口:ExecutorService 接口是 Executor 接口的子接口,定义了一些方法来控制执行器的状态和任务执行情况,例如 shutdown()、submit()、invokeAll() 等。

Java 中的线程池是基于 Executor 框架实现的。Java 中提供了多种类型的线程池,例如 FixedThreadPool、CachedThreadPool、ScheduledThreadPool 等。线程池可以提高程序的执行效率,减少线程创建和销毁的开销,并提供更好的线程管理和任务调度机制。

除了 Java 中的线程和线程池外,还有一些其他的重要方法和机制可以用于实现线程同步和控制,包括 synchronized、wait/notify、Lock、Semaphore、CountDownLatch 等。

  1. synchronized:synchronized 是 Java 中的关键字,可以用于实现线程同步。synchronized 可以修饰方法或代码块,保证同一时刻只有一个线程可以访问被 synchronized 修饰的方法或代码块。
  2. wait/notify:wait/notify 是 Java 中的一个机制,用于线程之间的通信和同步。wait/notify 需要在 synchronized 块中使用,wait 方法可以使当前线程等待,直到其他线程调用 notify 或 notifyAll 方法来唤醒该线程。
  3. Lock:Lock 是 Java 中的一个接口,提供了比 synchronized 更为灵活的线程同步机制。Lock 接口的实现类可以实现公平锁、非公平锁等不同类型的锁。
  4. Semaphore:Semaphore 是 Java 中的一个类,用于实现线程间的信号量机制。Semaphore 可以用来限制同时访问某个共享资源的线程数量,以实现线程的同步。
  5. CountDownLatch:CountDownLatch 是 Java 中的一个类,用于实现线程间的等待机制。CountDownLatch 可以使一个或多个线程等待,直到其他线程执行完毕后才继续执行。

总之,Java 中还有一些其他重要的方法和机制可以用于实现线程同步和控制,包括 synchronized、wait/notify、Lock、Semaphore、CountDownLatch 等。这些方法和机制都可以帮助我们实现多线程编程中的同步、通信、等待等操作,从而更好地管理和控制多线程程序的执行。

java线程池

Java 中的线程池是一种常用的并发编程机制,用于管理和调度线程的创建和执行。Java 中的线程池是由 ThreadPoolExecutor 类实现的,并提供了一系列参数来控制线程池的大小、任务队列、线程的优先级等。以下是一些 Java 中线程池的常用参数:

  1. corePoolSize:线程池的核心线程数,即在池中保持的线程数,即使它们处于空闲状态。如果在执行任务时提交的任务数大于核心线程数,线程池会创建新的线程来处理任务。
  2. maximumPoolSize:线程池的最大线程数,即线程池中最多允许的线程数。如果在执行任务时提交的任务数大于最大线程数,线程池会将任务放入任务队列中等待执行。
  3. keepAliveTime:线程池中空闲线程的存活时间。如果一个线程处于空闲状态超过了 keepAliveTime 指定的时间,则该线程会被回收,直到线程池中的线程数小于核心线程数。
  4. workQueue:线程池的任务队列,用于保存等待执行的任务。Java 中提供了多种队列实现,如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
  5. threadFactory:线程工厂,用于创建线程池中的线程。可以使用线程工厂自定义线程的名称、优先级、守护进程等属性。
  6. RejectedExecutionHandler:任务拒绝策略,用于控制当线程池中的线程数达到最大值并且任务队列已满时的处理方式。Java 中提供了四种默认的拒绝策略,如 ThreadPoolExecutor.AbortPolicy、ThreadPoolExecutor.CallerRunsPolicy、ThreadPoolExecutor.DiscardOldestPolicy、ThreadPoolExecutor.DiscardPolicy 等。

Java 线程池的执行顺序如下:

  1. 当提交一个任务时,线程池会根据当前线程池中的线程数和任务队列中等待执行的任务数来判断是直接创建新线程来执行任务,还是将任务放入任务队列中等待执行。
  2. 如果当前线程池中的线程数小于核心线程数,线程池会创建新的线程来执行任务。
  3. 如果当前线程池中的线程数等于核心线程数,且任务队列未满,线程池会将任务放入任务队列中等待执行。
  4. 如果当前线程池中的线程数等于核心线程数,且任务队列已满,但线程池中的线程数未达到最大线程数,线程池会创建新的线程来执行任务。
  5. 如果当前线程池中的线程数等于最大线程数,且任务队列已满,线程池会
  6. 执行指定的拒绝策略,如抛出异常、丢弃任务、使用调用线程执行任务、丢弃队列中最老的任务等。
  7. 当任务执行完毕后,线程池会继续从任务队列中获取等待执行的任务,并将其分配给线程池中的线程执行。

总之,Java 中的线程池是一种非常实用的并发编程机制,可以帮助程序员控制线程的创建和执行,提高程序的性能和效率。了解线程池的参数和执行顺序,可以帮助程序员更好地使用线程池,避免出现线程阻塞、死锁等问题,提高程序的稳定性和可维护性。

C++

edge 浏览器 进程 和 线程

在 C++11 标准中,C++ 引入了 std::thread 类来支持多线程编程。std::thread 类可以用于创建新的线程,并可以指定线程的执行函数、传递参数、控制线程的启动和终止等。

以下是一些 std::thread 类的常用方法:

  1. 构造函数:std::thread 类的构造函数用于创建新的线程。构造函数需要传递一个函数对象或函数指针,作为线程的执行函数。
  2. join() 方法:join() 方法用于等待线程执行结束,并回收线程资源。调用 join() 方法后,当前线程会阻塞,直到被 join 的线程执行结束。
  3. detach() 方法:detach() 方法用于分离线程,将线程的控制权转移到系统中,由系统负责回收线程资源。调用 detach() 方法后,当前线程不会等待被分离的线程执行结束,会立即返回。
  4. get_id() 方法:get_id() 方法用于获取线程的 ID。
  5. 线程局部存储(Thread-local storage,TLS):C++11 中引入了 thread_local 关键字,用于定义线程局部变量,每个线程都有自己的副本,互不干扰。

在 C++ 中,还可以使用一些其他的多线程库来实现线程,例如 Boost.Thread、OpenMP 等。

除了 std::thread 类外,C++ 还提供了一些其他的线程相关类和接口,例如 std::mutex、std::condition_variable、std::atomic 等,用于实现线程同步、通信等操作。其中 std::mutex 类可以用于实现互斥锁,std::condition_variable 类可以用于实现线程间的条件变量等。

浏览器

浏览器中的线程是浏览器用于处理用户请求、渲染页面等功能的执行单元,不同的浏览器可能有不同的线程结构和分配方式。以下是 Chrome 和 Edge 浏览器中的线程及其作用:

  1. 主线程(Main thread):主线程是浏览器的核心线程,用于处理用户界面交互、网络请求、渲染页面等主要任务。主线程是单线程的,所有的 JavaScript 代码和用户事件处理都在主线程中执行。主线程的阻塞会导致浏览器的卡顿和无响应现象。
  2. 渲染线程(Rendering thread):渲染线程用于处理页面的渲染,包括布局、绘制等操作。渲染线程是多线程的,通常会有多个渲染线程来处理不同的页面。渲染线程会将渲染结果传递给主线程,由主线程将其呈现到用户界面上。
  3. JavaScript 引擎线程(JavaScript engine thread):JavaScript 引擎线程用于处理 JavaScript 代码的执行,包括解析、编译、执行等操作。JavaScript 引擎线程通常是单线程的,但一些浏览器引擎(如 Chrome 的 V8 引擎)采用了多线程解析和编译 JavaScript 代码,以提高执行效率。
  4. GPU 线程(GPU thread):GPU 线程用于处理图形和动画效果,通常是通过 OpenGL、WebGL 等图形库实现的。GPU 线程是单独的线程,通常与渲染线程一起工作,将渲染结果传递给渲染线程。
  5. 事件线程(Event thread):事件线程用于处理用户事件,例如鼠标点击、键盘输入等。事件线程通常是单独的线程,将事件传递给主线程或 JavaScript 引擎线程进行处理。
  6. 数据库线程(Database thread):数据库线程用于处理浏览器中的本地存储和数据库操作,例如 IndexedDB、Web SQL 等。数据库线程通常是单独的线程,负责处理数据库的读写操作,与主线程、JavaScript 引擎线程等进行通信。

总之,浏览器中的线程结构和分配方式因浏览器而异,但通常包括主线程、渲染线程、JavaScript 引擎线程、GPU 线程、事件线程、数据库线程等。这些线程负责处理不同的任务,共同协作完成浏览器的功能。了解浏览器中的线程结构和分配方式对于理解浏览器的工作原理

在 C++ 和 Chromium 中,有一些线程和任务管理相关的类和机制,包括 Task、base::Thread、Sequence Task Runner 和 Thread Pool 等,这些类和机制可以帮助我们实现多线程编程中的任务调度、线程管理等操作。

  1. Task:Task 类代表一个可执行的任务,通常是一个函数对象或函数指针。Task 可以通过 PostTask() 方法提交到线程池或 Sequence Task Runner 中执行。
  2. base::Thread:base::Thread 类是一个线程类,可以用于创建新的线程,并可以指定线程的执行函数、传递参数、控制线程的启动和终止等。base::Thread 类是基于 Chromium 的线程库实现的,支持在 Windows、Mac OS X、Linux 等多个平台上运行。
  3. Sequence Task Runner:Sequence Task Runner 是一个任务队列,用于按照一定的顺序执行任务。Sequence Task Runner 支持按照优先级、延迟等方式执行任务,可以用于实现多线程编程中的任务调度等操作。
  4. Thread Pool:Thread Pool 是一个线程池,用于管理多个线程并发执行任务。Thread Pool 可以控制线程的数量、优先级、任务调度等,可以用于实现多线程编程中的任务并发执行等操作。

这些类和机制可以组合使用,以实现多种不同的功能。例如,可以使用 base::Thread 类创建新的线程,将任务提交到 Sequence Task Runner 或 Thread Pool 中执行;也可以使用 Sequence Task Runner 来实现任务调度和优先级控制;还可以使用 Thread Pool 来实现任务的并发执行等操作。

总之,Task、base::Thread、Sequence Task Runner 和 Thread Pool 等是 C++ 和 Chromium 中常用的线程和任务管理相关的类和机制,可以帮助我们实现多线程编程中的任务调度、线程管理等操作。这些类和机制在 C++ 和 Chromium 的很多项目中都有广泛的应用,如浏览器内核、媒体框架、网络协议栈等。

Go

在 Go 语言中,协程(Goroutine)是一种轻量级的线程实现,由 Go 语言的运行时系统管理。协程可以在单个线程上执行,可以实现高并发和高效率的程序设计。

以下是一些 Go 语言中的协程相关的特性和用法:

  1. 协程的创建:在 Go 语言中,可以通过 go 关键字创建一个新的协程,并将其与一个函数绑定。例如,可以使用 go foo() 来创建一个新的协程,并让其执行 foo 函数。
  2. 协程的调度:在 Go 语言中,协程的调度由运行时系统自动管理,程序员不需要手动调度。运行时系统会根据调度算法将协程分配到可用的 CPU 核心上运行,并在需要时自动抢占和恢复协程。
  3. 协程的通信:在 Go 语言中,协程之间可以通过通道(Channel)进行通信。通道是一种带有类型的管道,可以用于协程之间传递数据。通道可以实现协程之间的同步和数据交换等操作。
  4. 协程的同步:在 Go 语言中,可以使用 sync 包提供的锁和条件变量等机制来实现协程之间的同步。sync 包提供了多种锁和条件变量的实现,包括互斥锁(Mutex)、读写锁(RWMutex)、条件变量(Cond)等。
  5. 协程的并发:在 Go 语言中,可以使用并发的方式来实现任务的并发执行。可以通过创建多个协程,并将任务分配到不同的协程上执行,以实现任务的并发执行。

总之,Go 语言中的协程是一种轻量级的线程实现,可以实现高并发和高效率的程序设计。协程的创建、调度、通信、同步和并发等操作都非常方便,使得 Go 语言成为了一个非常适合编写并发程序的语言。

GMP模型

GMP 模型是一种常见的并发编程模型,它是由 Goroutine、Channel 和 Select 三个关键元素组成的。

  1. Goroutine:Goroutine 是 Go 语言提供的一种轻量级的并发机制,它可以在一个或多个操作系统线程上运行,并使用调度器来实现任务的调度和协作。Goroutine 的创建和销毁的开销非常小,可以很容易地创建数百万个 Goroutine,并发执行大量的任务。
  2. Channel:Channel 是一种带有类型的管道,可以用于协程之间传递数据。通道可以实现协程之间的同步和数据交换等操作。Channel 可以被用来实现多种不同的并发模型,如生产者消费者模型、消息传递模型等。
  3. Select:Select 是 Go 语言提供的一种多路复用机制,可以同时监听多个 Channel 上的数据流,并在其中任意一个 Channel 准备好数据时进行操作。Select 可以用于实现高效的并发控制和任务调度,尤其适用于协程之间的通信和同步等操作。

GMP 模型将这三个关键元素结合起来,形成了一种高效、灵活的并发编程模型。在 GMP 模型中,程序员可以通过创建 Goroutine,将任务分配到不同的协程上执行;通过 Channel,实现协程之间的数据交换和同步;通过 Select,实现高效的并发控制和任务调度。

总之,GMP 模型是一种常见的并发编程模型,它是由 Goroutine、Channel 和 Select 三个关键元素组成的。GMP 模型的设计和实现使得 Go 语言成为了一个非常适合编写高并发和高效率程序的语言,也为其他语言提供了一种值得借鉴的并发编程模型。

C#

在 C# 中,线程(Thread)是一种基本的并发编程机制,可以用于创建和管理线程,实现多线程并发执行任务。以下是一些 C# 中的线程相关的特性和用法:

  1. 线程的创建:在 C# 中,可以使用 Thread 类的构造函数创建新的线程。例如,可以使用 new Thread(new ThreadStart(DoWork)) 来创建一个新的线程,并将其与 DoWork 方法绑定。
  2. 线程的启动:在 C# 中,可以使用 Thread 类的 Start() 方法来启动一个线程,并让其开始执行指定的函数。例如,可以使用 thread.Start() 方法来启动之前创建的线程。
  3. 线程的同步:在 C# 中,可以使用 Monitor 类或 lock 语句来实现线程之间的同步。这些机制可以帮助程序员实现线程之间的互斥访问和共享资源的同步访问等操作。
  4. 线程池:在 C# 中,可以使用 ThreadPool 类来创建和管理线程池,实现多线程并发执行任务。线程池可以控制线程的数量、优先级、任务调度等,可以用于实现多线程编程中的任务并发执行等操作。
  5. 异步编程:在 C# 中,可以使用异步编程模型(Asynchronous Programming Model,简称 APM)和异步任务模型(Task-based Asynchronous Pattern,简称 TAP)等机制,实现高效的异步编程。这些机制可以帮助程序员实现任务的并发执行和非阻塞的程序设计等操作。

总之,C# 中的线程是一种基本的并发编程机制,可以用于创建和管理线程,实现多线程并发执行任务。线程的同步、线程池、异步编程等机制可以帮助程序员实现多线程编程中的任务调度、线程管理等操作。C# 中的多线程编程已经广泛应用于各种领域,如游戏开发、网络编程、大数据处理等等。

async & await

在 C# 中,async 和 await 是一种异步编程模型,用于实现非阻塞的程序设计和任务并发执行。async 和 await 的本质是一种语法糖,可以帮助程序员简化异步编程的代码,实现更加简洁、可读、可维护的程序设计。

async 和 await 的原理是基于 C# 5.0 引入的 Task 和 Task<TResult> 类型。使用 async 和 await 可以实现以下几个步骤:

  1. 定义异步方法:在 C# 中,可以使用 async 关键字来定义异步方法。异步方法可以使用 await 关键字等待另一个异步方法完成,同时不会阻塞当前线程,从而实现非阻塞的程序设计。
  2. 返回 Task 或 Task<TResult> 类型:异步方法可以返回 Task 或 Task<TResult> 类型的对象,用于表示异步操作的状态和结果。异步方法可以使用 await 关键字等待异步操作完成,同时可以使用 Task 类型的方法和属性来操作异步操作的状态和结果。
  3. 使用 Task.Run 方法:在异步方法中,可以使用 Task.Run 方法来启动另一个异步方法。Task.Run 方法可以将异步方法分配到线程池中的线程上执行,从而实现多线程并发执行任务。

async 和 await 在 C# 中的应用非常广泛,可以用于实现各种异步编程操作,如网络编程、数据库访问、文件操作、图形处理等。使用 async 和 await 可以帮助程序员实现非阻塞的程序设计,避免了线程阻塞和死锁等问题,提高了程序的响应速度和性能。

Task

在 C# 中,Task 是一种轻量级的并发编程机制,用于实现多线程并发执行任务。Task 类型是 .NET Framework 4.0 引入的,并在 .NET Framework 4.5 中得到了进一步的扩展和优化。以下是一些 C# 中的 Task 相关的特性和用法:

  1. Task 的创建:在 C# 中,可以使用 Task 类的静态方法或构造函数来创建新的 Task 对象。例如,可以使用 Task.Run(Action) 方法来创建一个新的 Task 对象,并将其与指定的 Action 绑定。
  2. Task 的启动和等待:在 C# 中,可以使用 Task 类的 Start() 方法或 Task.Run 方法来启动一个 Task 对象,并让其开始执行指定的函数。可以使用 Task.Wait 方法或 await 关键字等待 Task 对象的完成。
  3. Task 的控制和取消:在 C# 中,可以使用 Task 类的一些方法和属性来控制 Task 对象的状态、优先级、取消等操作。例如,可以使用 Task.IsCompleted 属性判断 Task 对象是否完成,使用 Task.Result 属性获取 Task 的执行结果,使用 Task.ContinueWith 方法创建一个新的 Task 对象,并在原有的 Task 对象完成时执行指定的函数。
  4. Task 的并发执行:在 C# 中,可以使用 Task.Factory.StartNew 方法或 Parallel 类来创建和管理多个 Task 对象,实现多线程并发执行任务。Task 对象可以通过 Task.WaitAll 或 Task.WaitAny 方法来实现任务的同步和控制。

总之,Task 是一种轻量级的并发编程机制,在 C# 中被广泛应用于各种领域,如网络编程、数据库访问、大数据处理等。Task 的设计和实现使得 C# 成为了一个非常适合编写高并发和高效率程序的语言。

最后更新于 2023年4月14日 by qlili

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x