精华内容
下载资源
问答
  • 多线程实验原理
    千次阅读
    2021-03-20 08:32:15

    精品文档

    Java实验程序设计实验报告

    实验名称:多线程

    一:实验目的

    1、掌握线程和多线程的概念。

    2、掌握创建线程的两种方法及其区别。

    3、了解线程的启动、终止、同步、互斥和优先级等概念。

    二:实验内容

    1、编写一个程序,其功能是运行之后,其中有一个线程可以输出20次你的学号,另一个线程会输出20次你的姓名。

    2、编写一个图形界面程序,运行之后,让其中有一个线程能在界面上“实时”显示系统当前时间(精确到秒获取时间可查询java.util.Calendar类,它包含了多个用于获得系统时间的函数)。另让一个线程可以在界面上提示当前系统时间下用户该做什么工作(例如当程序判断出系统时间现在是8:00到9:00,则提示用户该上课;现在是23:00到8:00,则提示用户该休息。 具体测试时可以将时间段限制到秒级,以便及时查看到程序运行中提示信息的变化)。

    三:实验设计

    四:实验测试及运行结果

    实验一;20次输出姓名,学号实验二:一个界面的左边显示当时时间,显示的时间会随时间的改变而改变,右边显示某个时间段该干什么,比如该睡觉,该上课,该自习。

    五:问题与总结

    通过这次实验学会了怎么使用多线程。

    六:附录

    package shiyan6_1;

    class MyThread implements Runnable {

    public MyThread() {

    // 构造函数的代码,根据需要来写

    }

    public void run() {

    for (int i = 1; i <= 20; i++) {

    System.out.println("第" + i + "次执行线程"

    + Thread.currentThread().getName());

    try {

    Thread.currentThread().sleep(500);// 睡眠500ms

    } catch (InterruptedException e) {

    }

    }

    }

    public static void main(String args[]) {

    Thread t1 = new Thread(new MyThread(), "学号"); // 创建线程1的对象,并

    // 通过第二个参数将其命名为thread 1

    Thread t2 = new Thread(new MyThread(), "姓名"); // 创建线程2的对象,并

    // 通过第二个参数将其命名为thread 2

    t1.start(); // 启动两个线程运行

    t2.start(); // 虽然t2的启动表面上好像在后面,实际上两个线程的执行并无先后之分,

    }

    }

    实验二:

    package shiyan6_2;

    import java.awt.FlowLayout;

    import java.text.SimpleDateFormat;

    import java.util.Calendar;

    import javax.swing.JFrame;

    import javax.swing.JTextArea;

    public class MyThread {

    JFrame jf = new JFrame("线程都往界面上显示内容的例子");

    static JTextArea jta1, jta2;

    Thread trda = new thread1(); // 线程trda

    Thread trdb = new thread2(); // 线程trdb

    public MyThread() // 构造函数,生成图形界面

    {

    // setBounds(100,100,500,200);

    jf.setLayout(new FlowLayout());

    jta1 = new JTextArea(15, 30);

    jta2 = new JTextArea(15, 30);

    jf.add(jta1);

    jf.add(jta2); // 将2个组件添加到界面上

    jf.setLocation(100, 150);

    jf.setVisible(true);

    jf.pack();

    trda.start(); // 两个线程都启动

    trdb.start();

    }

    public static void main(String args[]) {

    MyThread frm = new MyThread();

    }

    }

    class thread1 extends Thread // 线程类thread1

    {

    public void run() {

    int y, m, d, h, mi, s;

    while(true){

    Calendar cal = Calendar.getInstance(); // 获取一个Calendar类的实例对象

    y = cal.get(Calendar.YEAR); // 获取年份

    m = cal.get(Calendar.MONTH)+1; // 获取月份,获取的月份是从0到11表示一到十二月

    d = cal.get(Calendar.DATE); // 获取日期

    h = cal.get(Calendar.HOUR_OF_DAY); // 获取小时

    mi = cal.get(Calendar.MINUTE); // 获取分钟

    s = cal.get(Calendar.SECOND); // 获取秒钟

    String s1=Integer.toString(y);

    String s2=Integer.toString(m);

    String s3=Integer.toString(d);

    String s4=Integer.toString(h);

    String s5=Integer.toString(mi);

    String s6=Integer.toString(s);

    MyThread.jta1.setText(s1+"年"+s2+"月"+s3+"日"+s4+"时"+s5+"分"+s6+"秒");

    }}

    }

    class thread2 extends Thread // 线程类thread2

    {

    public void run() {

    Calendar cal = Calendar.getInstance();

    int hour = cal.get(Calendar.HOUR_OF_DAY); // 获取小时

    int minute = cal.get(Calendar.MINUTE); // 获取分钟

    if (hour>23||hour<7){

    MyThread.jta2.append(" 睡觉时间");

    }

    else if(hour>7&&hour<17){

    MyThread.jta2.append(" 上课时间");

    }

    else if(hour>17&&hour<23){

    MyThread.jta2.append(" 自习时间");}

    }

    }

    .

    展开阅读全文

    更多相关内容
  • 实验的主要任务,是实现一个多线程的搜索应用,利用处理器的多线程提高运行效能。 请首先建lcc项目,SearchRandom,选择优化,运行.
  • 成绩 面向对象原理与Java实践课程实验报告 实验5多线程 姓 名 _ _ _ 班 级 _ 学 号 _ 实验地点 _ 实验时间 _ 指导教师 _ _ _ 一实验目的 了解线程调度机制 理解线程同步机制 掌握线程设计方法 二实验要求 掌握线程...
  • 多线程实验

    2018-12-25 12:08:21
    多线程的运用,以及!
  • 用ASP开多线程

    2021-01-20 07:44:57
    1.原理实验 原理当然都一样,利用web服务器支持多线程,在同一页面里向服务器发多个http请求来完成我们的工作。还是先实验一下,在一个页面里同时写2个txt文件,比较写入时间的差异。代码如下: <% startime=...
  • 对于一个进程中的线程来说,线程共享进程的内存块,当有新的线程产生的时候,操作系统不分配新的内存,而是让新线程共享原有的进程块的内存。因此,线程间的通信很容易,速度也很快。不同的进程因为处于不同...
  • Java多线程编程总结

    2021-01-31 07:16:15
    要认识多线程就要从操作系统的原理说起。以前古老的DOS操作系统(V6.22)是单任务的,还没有线程的概念,系统在每次只能做一件事情。比如你在copy东西的时候不能rename文件名。为了提高系统的利用效率,采用批处理来...
  • Linux多线程实验.ppt

    2021-05-14 02:40:12
    Linux多线程实验.ppt (10页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦!9.9 积分Linux多线程实验实验目的掌握Linux多线程程序编写。 Date1实验设备硬件PC机(带网卡) 1...

    62e50291a81fc54b507f33cb80033297.gif Linux多线程实验.ppt

    (10页)

    fc5bf3e7f1cba3a4a4f12e498016888f.gif

    本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦!

    9.9 积分

    Linux多线程实验实验目的掌握Linux多线程程序编写。 Date1实验设备硬件PC机(带网卡) 1台MagicARM270教学实验开发平台 1套软件RedHat Linux 9.0操作系统(完全安装)arm-linux-gcc-3.4.3交叉编译器Linux多线程实验Date2实验内容创建两个线程,各自统计发生次数,主程序监测两个线程的返回值。Linux多线程实验Date3实验预习要求预习Linux线程相关知识和Linux系统调用。 Linux多线程实验Date4实验原理线程是一种标准化模型,它用于把一个程序分成一组能够同时执行的任务。线程是一种特殊的进程,常称之为轻量级进程(light-weight process)。一个进程的所有线程有独立的执行线索和堆栈,但共享数据。 Linux多线程实验Date5实验原理线程的属性joinable:具有joinable属性的线程在执行完毕后并不会立即被Linux清除,如果函数有返回值,其返回值可通过pthread_join()函数调用得到。Detatched:具有detatched属性的线程,执行完毕立即被Linux清除,无法通过pthread_join()函数调用获得其返回值。建立线程的的时候如果没有指定属性,默认为joinable属性。Linux多线程实验Date6实验原理创建线程:创建线程使用pthread_create()函数实现。函数原型: int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) 挂起线程:挂起线程使用pthread_join()函数实现。函数原型: int pthread_join(pthread_t th, void **thread_return) Linux多线程实验Date7实验步骤(1)在/pxa270/nfs/prog/目录下,为本实验建立工作目录multi-thread,并进入该目录。 $ mkdir /pxa270/nfs/prog/multi-thread $ cd /pxa270/nfs/prog/multi-thread(2)在multi-thread目录下建立文件multi-thread.c,并编写程序代码。 $ vi multi-thread.c Linux多线程实验Date8实验步骤(3)编写Makefile,使程序最终在ARM上运行。编译,得到multi-thread文件 $ vi Makefile $ make注意,本程序的Makefile必须为链接选项加上-lpthread参数。 LDFLAGS += -lpthread Linux多线程实验Date9实验步骤(4)启动MagicARM 270,进行NFS连接,并进入multi-thread目录,执行multi-thread程序,观察程序运行结果,注意两线程的运行顺序。多运行几次,比较各次运行结果。 [root@MagicARM270 ~]# mount –t nfs 192.168.0.94:/pxa270/nfs /mnt –o nolock [root@MagicARM270 ~]# cd /mnr/prog/multi-thread [root@MagicARM270 multi-thread]# ./multi-threadLinux多线程实验Date10 关 键 词: 实验 linux 多线程

    524d6daf746efaa52c3c71bbfe7ba172.gif  天天文库所有资源均是用户自行上传分享,仅供网友学习交流,未经上传用户书面授权,请勿作他用。

    展开全文
  • 其中aeApiPoll的具体的原理请查看:计算机网络——select、poll、epoll原理_庄小焱的博客-CSDN博客 二、redis多线程模型原理 Redis 在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些...

    摘要

    Redis 作为缓存系统的事实标准,它的底层原理值得开发者去深入学习,Redis 自 2009 年发布第一版之后,其单线程网络模型的选择在社区中从未停止过讨论。如今到网络上搜索redis线程模型原理文章。都存在很多的误导大家的学习,同时没有深入对redis的线程模型的原理和redis到底使用的是单线程还是使用的多线的原因解释不清楚,今天博文博文将详细的介绍redis3.5的单线模型和redis6.0的多线程模型的原理。

    一、redis单线程模型原理

    1.1 单线程的优点

    • Redis 服务中运行的绝大多数操作的性能瓶颈都不是 CPU。redis核心考虑的因素
    • 使用单线程模型能带来更好的可维护性,方便开发和调试;
    • 使用单线程模型也能并发的处理客户端的请求;

    1.1.1 性能瓶颈

    最后要介绍的其实就是 Redis 选择单线程模型的决定性原因 —— 多线程技术的能够帮助我们充分利用 CPU 的计算资源来并发的执行不同的任务,但是 CPU 资源往往都不是 Redis 服务器的性能瓶颈。哪怕我们在一个普通的 Linux 服务器上启动 Redis 服务,它也能在 1s 的时间内处理 1,000,000 个用户请求。如果这种吞吐量不能满足我们的需求,更推荐的做法是使用分片的方式将不同的请求交给不同的 Redis 服务器来处理,而不是在同一个 Redis 服务中引入大量的多线程操作。

    Redis 并不是 CPU 密集型的服务,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度是非常快的;整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O,所以使用多线程模型处理全部的外部请求可能不是一个好的方案。

    AOF 是 Redis 的一种持久化机制,它会在每次收到来自客户端的写请求时,将其记录到日志中,Redis 服务器启动时都会重放 AOF 日志构建原始的数据集,保证数据的持久性。

    多线程虽然会帮助我们更充分地利用 CPU 资源,但是操作系统上线程的切换也不是免费的,线程切换其实会带来额外的开销,其中包括:

    1. 保存线程 1 的执行上下文;
    2. 加载线程 2 的执行上下文;

    频繁的对线程的上下文进行切换可能还会导致性能地急剧下降,这可能会导致我们不仅没有提升请求处理的平均速度,反而进行了负优化,所以这也是为什么 Redis 对于使用多线程技术非常谨慎。

    1.1.2 并发处理

    使用单线程模型也并不意味着程序不能并发的处理任务,Redis 虽然使用单线程模型处理用户的请求,但是它却使用 I/O 多路复用机制并发处理来自客户端的多个连接,同时等待多个连接发送的请求。在 I/O 多路复用模型中,最重要的函数调用就是 select 以及类似函数,该方法的能够同时监控多个文件描述符(也就是客户端的连接)的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。使用 I/O 多路复用技术能够极大地减少系统的开销,系统不再需要额外创建和维护进程和线程来监听来自客户端的大量连接,减少了服务器的开发成本和维护成本。

    1.1.3 可维护性

    可维护性对于一个项目来说非常重要,如果代码难以调试和测试,问题也经常难以复现,这对于任何一个项目来说都会严重地影响项目的可维护性。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,代码的执行过程不再是串行的,多个线程同时访问的变量如果没有谨慎处理就会带来诡异的问题。如果计算机中的两个进程(线程同理)同时尝试修改一个共享内存的内容,在没有并发控制的情况下,最终的结果依赖于两个进程的执行顺序和时机,如果发生了并发访问冲突,最后的结果就会是不正确的。引入了多线程,我们就必须要同时引入并发控制来保证在多个线程同时访问数据时程序行为的正确性,这就需要工程师额外维护并发控制的相关代码,例如,我们会需要在可能被并发读写的变量上增加互斥锁:

    var (
        mu Mutex // cost
        data int
    )
    
    // thread 1
    func() {
        mu.Lock()
        data += 1
        mu.Unlock()
    }
    
    // thread 2
    func() {
        mu.Lock()
        data -= 1
        mu.Unlock()
    }
    

    在访问这些变量或者内存之前也需要先对获取互斥锁,一旦忘记获取锁或者忘记释放锁就可能会导致各种诡异的问题,管理相关的并发控制机制也需要付出额外的研发成本和负担。

    1.2 redis单线程模型

    从 Redis 的 v1.0 到 v6.0 版本之前,Redis 的核心网络模型一直是一个典型的单 Reactor 模型:利用 epoll/select/kqueue 等多路复用技术,在单线程的事件循环中不断去处理事件(客户端请求),最后回写响应数据到客户端:

    •  client:客户端对象,Redis 是典型的 CS 架构(Client <---> Server),客户端通过 socket 与服务端建立网络通道然后发送请求命令,服务端执行请求的命令并回复。Redis 使用结构体 client 存储客户端的所有相关信息,包括但不限于封装的套接字连接 -- *conn当前选择的数据库指针 -- *db读入缓冲区 -- querybuf写出缓冲区 -- buf写出数据链表 -- reply等。
    • aeApiPoll:I/O 多路复用 API,是基于 epoll_wait/select/kevent 等系统调用的封装,监听等待读写事件触发,然后处理,它是事件循环(Event Loop)中的核心函数,是事件驱动得以运行的基础。
    • acceptTcpHandler:连接应答处理器,底层使用系统调用 accept 接受来自客户端的新连接,并为新连接注册绑定命令读取处理器,以备后续处理新的客户端 TCP 连接;除了这个处理器,还有对应的 acceptUnixHandler 负责处理 Unix Domain Socket 以及 acceptTLSHandler 负责处理 TLS 加密连接。
    • readQueryFromClient:命令读取处理器,解析并执行客户端的请求命令。
    • beforeSleep:事件循环中进入 aeApiPoll 等待事件到来之前会执行的函数,其中包含一些日常的任务,比如把 client->buf 或者 client->reply (后面会解释为什么这里需要两个缓冲区)中的响应写回到客户端,持久化 AOF 缓冲区的数据到磁盘等,相对应的还有一个 afterSleep 函数,在 aeApiPoll 之后执行。
    • sendReplyToClient:命令回复处理器,当一次事件循环之后写出缓冲区中还有数据残留,则这个处理器会被注册绑定到相应的连接上,等连接触发写就绪事件时,它会将写出缓冲区剩余的数据回写到客户端。

    Redis 内部实现了一个高性能的事件库 --- AE,基于 epoll/select/kqueue/evport 四种事件驱动技术,实现 Linux/MacOS/FreeBSD/Solaris 多平台的高性能事件循环模型。Redis 的核心网络模型正式构筑在 AE 之上,包括 I/O 多路复用、各类处理器的注册绑定,都是基于此才得以运行。

    Redis 发起请求命令的工作原理

    1. Redis 服务器启动,开启主线程事件循环(Event Loop),注册 acceptTcpHandler 连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接到来;
    2. 客户端和服务端建立网络连接;
    3. acceptTcpHandler 被调用,主线程使用 AE 的 API 将 readQueryFromClient 命令读取处理器绑定到新连接对应的文件描述符上,并初始化一个 client 绑定这个客户端连接;
    4. 客户端发送请求命令,触发读就绪事件,主线程调用 readQueryFromClient 通过 socket 读取客户端发送过来的命令存入 client->querybuf 读入缓冲区;
    5. 接着调用 processInputBuffer,在其中使用 processInlineBuffer 或者 processMultibulkBuffer 根据 Redis 协议解析命令,最后调用 processCommand 执行命令;
    6. 根据请求命令的类型(SET, GET, DEL, EXEC 等),分配相应的命令执行器去执行,最后调用 addReply 函数族的一系列函数将响应数据写入到对应 client 的写出缓冲区:client->buf 或者 client->replyclient->buf 是首选的写出缓冲区,固定大小 16KB,一般来说可以缓冲足够多的响应数据,但是如果客户端在时间窗口内需要响应的数据非常大,那么则会自动切换到 client->reply 链表上去,使用链表理论上能够保存无限大的数据(受限于机器的物理内存),最后把 client 添加进一个 LIFO 队列 clients_pending_write
    7. 在事件循环(Event Loop)中,主线程执行 beforeSleep --> handleClientsWithPendingWrites,遍历 clients_pending_write 队列,调用 writeToClientclient 的写出缓冲区里的数据回写到客户端,如果写出缓冲区还有数据遗留,则注册 sendReplyToClient 命令回复处理器到该连接的写就绪事件,等待客户端可写时在事件循环中再继续回写残余的响应数据。

    对于那些想利用多核优势提升性能的用户来说,Redis 官方给出的解决方案也非常简单粗暴:在同一个机器上多跑几个 Redis 实例。事实上,为了保证高可用,线上业务一般不太可能会是单机模式,更加常见的是利用 Redis 分布式集群多节点和数据分片负载均衡来提升性能和保证高可用。

    其中aeApiPoll的具体的原理请查看:计算机网络——select、poll、epoll原理_庄小焱的博客-CSDN博客

    二、redis多线程模型原理

    Redis 在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环。Redis 的 DEL 命令是用来删除掉一个或多个 key 储存的值,它是一个阻塞的命令,大多数情况下你要删除的 key 里存的值不会特别多,最多也就几十上百个对象,所以可以很快执行完,但是如果你要删的是一个超大的键值对,里面有几百万个对象,那么这条命令可能会阻塞至少好几秒,又因为事件循环是单线程的,所以会阻塞后面的其他事件,导致吞吐量下降。

    2.1 redis多线程原因

    Redis 的作者 antirez 为了解决这个问题进行了很多思考,一开始他想的办法是一种渐进式的方案:利用定时器和数据游标,每次只删除一小部分的数据,比如 1000 个对象,最终清除掉所有的数据,但是这种方案有个致命的缺陷,如果同时还有其他客户端往某个正在被渐进式删除的 key 里继续写入数据,而且删除的速度跟不上写入的数据,那么将会无止境地消耗内存,虽然后来通过一个巧妙的办法解决了,但是这种实现使 Redis 变得更加复杂,而多线程看起来似乎是一个水到渠成的解决方案:简单、易理解。于是,最终 antirez 选择引入多线程来实现这一类非阻塞的命令。于是,在 Redis v4.0 之后增加了一些的非阻塞命令如 UNLINKFLUSHALL ASYNCFLUSHDB ASYNC

    UNLINK 命令其实就是 DEL 的异步版本,它不会同步删除数据,而只是把 key 从 keyspace 中暂时移除掉,然后将任务添加到一个异步队列,最后由后台线程去删除,不过这里需要考虑一种情况是如果用 UNLINK 去删除一个很小的 key,用异步的方式去做反而开销更大,所以它会先计算一个开销的阀值,只有当这个值大于 64 才会使用异步的方式去删除 key,对于基本的数据类型如 List、Set、Hash 这些,阀值就是其中存储的对象数量。

    随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis 的单线程模式会导致系统消耗很多CPU 时间在网络 I/O 上从而降低吞吐量,提升 Redis 的性能有两个方向:

    • 优化网络 I/O 模块(零拷贝技术或者 DPDK 技术或者利用多核优势)
    • 提高机器内存读写的速度(依赖于硬件的发展,暂时无解。所以只能从前者下手)

    6.0 版本之后,Redis 正式在核心网络模型中引入了多线程,也就是所谓的 I/O threading,至此 Redis 真正拥有了多线程模型。Redis 在 6.0 版本之前的单线程事件循环模型,实际上就是一个非常经典的 Reactor 模型:

    Redis 的核心网络模型在 6.0 版本之前,一直是单 Reactor 模式:所有事件的处理都在单个线程内完成,虽然在 4.0 版本中引入了多线程,但是那个更像是针对特定场景(删除超大 key 值等)而打的补丁,并不能被视作核心网络模型的多线程。通常来说,单 Reactor 模式,引入多线程之后会进化为 Multi-Reactors 模式,基本工作模式如下:

    区别于单 Reactor 模式,这种模式不再是单线程的事件循环,而是有多个线程(Sub Reactors)各自维护一个独立的事件循环,由 Main Reactor 负责接收新连接并分发给 Sub Reactors 去独立处理,最后 Sub Reactors 回写响应给客户端。Multiple Reactors 模式通常也可以等同于 Master-Workers 模式,比如 Nginx 和 Memcached 等就是采用这种多线程模型,虽然不同的项目实现细节略有区别,但总体来说模式是一致的。

    2.2 redis多线程流程

    Redis 虽然也实现了多线程,但是却不是标准的 Multi-Reactors/Master-Workers 模式。

    1. Redis 服务器启动,开启主线程事件循环(Event Loop),注册 acceptTcpHandler 连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接到来;
    2. 客户端和服务端建立网络连接;
    3. acceptTcpHandler 被调用,主线程使用 AE 的 API 将 readQueryFromClient 命令读取处理器绑定到新连接对应的文件描述符上,并初始化一个 client 绑定这个客户端连接;
    4. 客户端发送请求命令,触发读就绪事件,服务端主线程不会通过 socket 去读取客户端的请求命令,而是先将 client 放入一个 LIFO 队列 clients_pending_read
    5. 在事件循环(Event Loop)中,主线程执行 beforeSleep -->handleClientsWithPendingReadsUsingThreads,利用 Round-Robin 轮询负载均衡策略,把 clients_pending_read队列中的连接均匀地分配给 I/O 线程各自的本地 FIFO 任务队列 io_threads_list[id] 和主线程自己,I/O 线程通过 socket 读取客户端的请求命令,存入 client->querybuf 并解析第一个命令,但不执行命令,主线程忙轮询,等待所有 I/O 线程完成读取任务;
    6. 主线程和所有 I/O 线程都完成了读取任务,主线程结束忙轮询,遍历 clients_pending_read 队列,执行所有客户端连接的请求命令,先调用 processCommandAndResetClient 执行第一条已经解析好的命令,然后调用 processInputBuffer 解析并执行客户端连接的所有命令,在其中使用 processInlineBuffer 或者 processMultibulkBuffer 根据 Redis 协议解析命令,最后调用 processCommand 执行命令;
    7. 根据请求命令的类型(SET, GET, DEL, EXEC 等),分配相应的命令执行器去执行,最后调用 addReply 函数族的一系列函数将响应数据写入到对应 client 的写出缓冲区:client->buf 或者 client->replyclient->buf 是首选的写出缓冲区,固定大小 16KB,一般来说可以缓冲足够多的响应数据,但是如果客户端在时间窗口内需要响应的数据非常大,那么则会自动切换到 client->reply 链表上去,使用链表理论上能够保存无限大的数据(受限于机器的物理内存),最后把 client 添加进一个 LIFO 队列 clients_pending_write
    8. 在事件循环(Event Loop)中,主线程执行 beforeSleep --> handleClientsWithPendingWritesUsingThreads,利用 Round-Robin 轮询负载均衡策略,把 clients_pending_write 队列中的连接均匀地分配给 I/O 线程各自的本地 FIFO 任务队列 io_threads_list[id] 和主线程自己,I/O 线程通过调用 writeToClientclient 的写出缓冲区里的数据回写到客户端,主线程忙轮询,等待所有 I/O 线程完成写出任务;
    9. 主线程和所有 I/O 线程都完成了写出任务, 主线程结束忙轮询,遍历 clients_pending_write 队列,如果 client 的写出缓冲区还有数据遗留,则注册 sendReplyToClient 到该连接的写就绪事件,等待客户端可写时在事件循环中再继续回写残余的响应数据。

    这里大部分逻辑和之前的单线程模型是一致的,变动的地方仅仅是把读取客户端请求命令和回写响应数据的逻辑异步化了,交给 I/O 线程去完成,这里需要特别注意的一点是:I/O 线程仅仅是读取和解析客户端命令而不会真正去执行命令,客户端命令的执行最终还是要在主线程上完成

    三、redis多线程源码分析

    3.1 多线程初始化

    void initThreadedIO(void) {
        server.io_threads_active = 0; /* We start with threads not active. */
    
        // 如果用户只配置了一个 I/O 线程,则不会创建新线程(效率低),直接在主线程里处理 I/O。
        if (server.io_threads_num == 1) return;
    
        if (server.io_threads_num > IO_THREADS_MAX_NUM) {
            serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
                                 "The maximum number is %d.", IO_THREADS_MAX_NUM);
            exit(1);
        }
    
        // 根据用户配置的 I/O 线程数,启动线程。
        for (int i = 0; i < server.io_threads_num; i++) {
            // 初始化 I/O 线程的本地任务队列。
            io_threads_list[i] = listCreate();
            if (i == 0) continue; // 线程 0 是主线程。
    
            // 初始化 I/O 线程并启动。
            pthread_t tid;
            // 每个 I/O 线程会分配一个本地锁,用来休眠和唤醒线程。
            pthread_mutex_init(&io_threads_mutex[i],NULL);
            // 每个 I/O 线程分配一个原子计数器,用来记录当前遗留的任务数量。
            io_threads_pending[i] = 0;
            // 主线程在启动 I/O 线程的时候会默认先锁住它,直到有 I/O 任务才唤醒它。
            pthread_mutex_lock(&io_threads_mutex[i]);
            // 启动线程,进入 I/O 线程的主逻辑函数 IOThreadMain。
            if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
                serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
                exit(1);
            }
            io_threads[i] = tid;
        }
    }

    initThreadedIO 会在 Redis 服务器启动时的初始化工作的末尾被调用,初始化 I/O 多线程并启动。Redis 的多线程模式默认是关闭的,需要用户在 redis.conf 配置文件中开启:

    io-threads 4
    io-threads-do-reads yes

    3.2 读取请求

    当客户端发送请求命令之后,会触发 Redis 主线程的事件循环,命令处理器 readQueryFromClient 被回调,在以前的单线程模型下,这个方法会直接读取解析客户端命令并执行,但是多线程模式下,则会把 client 加入到 clients_pending_read 任务队列中去,后面主线程再分配到 I/O 线程去读取客户端请求命令:

    void readQueryFromClient(connection *conn) {
        client *c = connGetPrivateData(conn);
        int nread, readlen;
        size_t qblen;
    
        // 检查是否开启了多线程,如果是则把 client 加入异步队列之后返回。
        if (postponeClientRead(c)) return;
    
        // 省略代码,下面的代码逻辑和单线程版本几乎是一样的。
        ...
    }
    
    int postponeClientRead(client *c) {
        // 当多线程 I/O 模式开启、主线程没有在处理阻塞任务时,将 client 加入异步队列。
        if (server.io_threads_active &&
            server.io_threads_do_reads &&
            !ProcessingEventsWhileBlocked &&
            !(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
        {
            // 给 client 打上 CLIENT_PENDING_READ 标识,表示该 client 需要被多线程处理,
            // 后续在 I/O 线程中会在读取和解析完客户端命令之后判断该标识并放弃执行命令,让主线程去执行。
            c->flags |= CLIENT_PENDING_READ;
            listAddNodeHead(server.clients_pending_read,c);
            return 1;
        } else {
            return 0;
        }
    }

    接着主线程会在事件循环的 beforeSleep() 方法中,调用 handleClientsWithPendingReadsUsingThreads

    int handleClientsWithPendingReadsUsingThreads(void) {
        if (!server.io_threads_active || !server.io_threads_do_reads) return 0;
        int processed = listLength(server.clients_pending_read);
        if (processed == 0) return 0;
    
        if (tio_debug) printf("%d TOTAL READ pending clients\n", processed);
    
        // 遍历待读取的 client 队列 clients_pending_read,
        // 通过 RR 轮询均匀地分配给 I/O 线程和主线程自己(编号 0)。
        listIter li;
        listNode *ln;
        listRewind(server.clients_pending_read,&li);
        int item_id = 0;
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            int target_id = item_id % server.io_threads_num;
            listAddNodeTail(io_threads_list[target_id],c);
            item_id++;
        }
    
        // 设置当前 I/O 操作为读取操作,给每个 I/O 线程的计数器设置分配的任务数量,
        // 让 I/O 线程可以开始工作:只读取和解析命令,不执行。
        io_threads_op = IO_THREADS_OP_READ;
        for (int j = 1; j < server.io_threads_num; j++) {
            int count = listLength(io_threads_list[j]);
            io_threads_pending[j] = count;
        }
    
        // 主线程自己也会去执行读取客户端请求命令的任务,以达到最大限度利用 CPU。
        listRewind(io_threads_list[0],&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            readQueryFromClient(c->conn);
        }
        listEmpty(io_threads_list[0]);
    
        // 忙轮询,累加所有 I/O 线程的原子任务计数器,直到所有计数器的遗留任务数量都是 0,
        // 表示所有任务都已经执行完成,结束轮询。
        while(1) {
            unsigned long pending = 0;
            for (int j = 1; j < server.io_threads_num; j++)
                pending += io_threads_pending[j];
            if (pending == 0) break;
        }
        if (tio_debug) printf("I/O READ All threads finshed\n");
    
        // 遍历待读取的 client 队列,清除 CLIENT_PENDING_READ 和 CLIENT_PENDING_COMMAND 标记,
        // 然后解析并执行所有 client 的命令。
        while(listLength(server.clients_pending_read)) {
            ln = listFirst(server.clients_pending_read);
            client *c = listNodeValue(ln);
            c->flags &= ~CLIENT_PENDING_READ;
            listDelNode(server.clients_pending_read,ln);
    
            if (c->flags & CLIENT_PENDING_COMMAND) {
                c->flags &= ~CLIENT_PENDING_COMMAND;
                // client 的第一条命令已经被解析好了,直接尝试执行。
                if (processCommandAndResetClient(c) == C_ERR) {
                    /* If the client is no longer valid, we avoid
                     * processing the client later. So we just go
                     * to the next. */
                    continue;
                }
            }
            processInputBuffer(c); // 继续解析并执行 client 命令。
    
            // 命令执行完成之后,如果 client 中有响应数据需要回写到客户端,则将 client 加入到待写出队列 clients_pending_write
            if (!(c->flags & CLIENT_PENDING_WRITE) && clientHasPendingReplies(c))
                clientInstallWriteHandler(c);
        }
    
        /* Update processed count on server */
        server.stat_io_reads_processed += processed;
    
        return processed;
    }

    这里的核心工作是:

    • 遍历待读取的 client 队列 clients_pending_read,通过 RR 策略把所有任务分配给 I/O 线程和主线程去读取和解析客户端命令。
    • 忙轮询等待所有 I/O 线程完成任务。
    • 最后再遍历 clients_pending_read,执行所有 client 的命令。

    3.3 写回响应

    完成命令的读取、解析以及执行之后,客户端命令的响应数据已经存入 client->buf 或者 client->reply 中了,接下来就需要把响应数据回写到客户端了,还是在 beforeSleep 中, 主线程调用 handleClientsWithPendingWritesUsingThreads

    int handleClientsWithPendingWritesUsingThreads(void) {
        int processed = listLength(server.clients_pending_write);
        if (processed == 0) return 0; /* Return ASAP if there are no clients. */
    
        // 如果用户设置的 I/O 线程数等于 1 或者当前 clients_pending_write 队列中待写出的 client
        // 数量不足 I/O 线程数的两倍,则不用多线程的逻辑,让所有 I/O 线程进入休眠,
        // 直接在主线程把所有 client 的相应数据回写到客户端。
        if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
            return handleClientsWithPendingWrites();
        }
    
        // 唤醒正在休眠的 I/O 线程(如果有的话)。
        if (!server.io_threads_active) startThreadedIO();
    
        if (tio_debug) printf("%d TOTAL WRITE pending clients\n", processed);
    
        // 遍历待写出的 client 队列 clients_pending_write,
        // 通过 RR 轮询均匀地分配给 I/O 线程和主线程自己(编号 0)。
        listIter li;
        listNode *ln;
        listRewind(server.clients_pending_write,&li);
        int item_id = 0;
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            c->flags &= ~CLIENT_PENDING_WRITE;
    
            /* Remove clients from the list of pending writes since
             * they are going to be closed ASAP. */
            if (c->flags & CLIENT_CLOSE_ASAP) {
                listDelNode(server.clients_pending_write, ln);
                continue;
            }
    
            int target_id = item_id % server.io_threads_num;
            listAddNodeTail(io_threads_list[target_id],c);
            item_id++;
        }
    
        // 设置当前 I/O 操作为写出操作,给每个 I/O 线程的计数器设置分配的任务数量,
        // 让 I/O 线程可以开始工作,把写出缓冲区(client->buf 或 c->reply)中的响应数据回写到客户端。
        io_threads_op = IO_THREADS_OP_WRITE;
        for (int j = 1; j < server.io_threads_num; j++) {
            int count = listLength(io_threads_list[j]);
            io_threads_pending[j] = count;
        }
    
        // 主线程自己也会去执行读取客户端请求命令的任务,以达到最大限度利用 CPU。
        listRewind(io_threads_list[0],&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            writeToClient(c,0);
        }
        listEmpty(io_threads_list[0]);
    
        // 忙轮询,累加所有 I/O 线程的原子任务计数器,直到所有计数器的遗留任务数量都是 0。
        // 表示所有任务都已经执行完成,结束轮询。
        while(1) {
            unsigned long pending = 0;
            for (int j = 1; j < server.io_threads_num; j++)
                pending += io_threads_pending[j];
            if (pending == 0) break;
        }
        if (tio_debug) printf("I/O WRITE All threads finshed\n");
    
        // 最后再遍历一次 clients_pending_write 队列,检查是否还有 client 的写出缓冲区中有残留数据,
        // 如果有,那就为 client 注册一个命令回复器 sendReplyToClient,等待客户端写就绪再继续把数据回写。
        listRewind(server.clients_pending_write,&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
    
            // 检查 client 的写出缓冲区是否还有遗留数据。
            if (clientHasPendingReplies(c) &&
                    connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
            {
                freeClientAsync(c);
            }
        }
        listEmpty(server.clients_pending_write);
    
        /* Update processed count on server */
        server.stat_io_writes_processed += processed;
    
        return processed;
    }

    这里的核心工作是:

    • 检查当前任务负载,如果当前的任务数量不足以用多线程模式处理的话,则休眠 I/O 线程并且直接同步将响应数据回写到客户端。
    • 唤醒正在休眠的 I/O 线程(如果有的话)。
    • 遍历待写出的 client 队列 clients_pending_write,通过 RR 策略把所有任务分配给 I/O 线程和主线程去将响应数据写回到客户端。
    • 忙轮询等待所有 I/O 线程完成任务。
    • 最后再遍历 clients_pending_write,为那些还残留有响应数据的 client 注册命令回复处理器 sendReplyToClient,等待客户端可写之后在事件循环中继续回写残余的响应数据。

    3.4 I/O 线程主逻辑

    void *IOThreadMain(void *myid) {
        /* The ID is the thread number (from 0 to server.iothreads_num-1), and is
         * used by the thread to just manipulate a single sub-array of clients. */
        long id = (unsigned long)myid;
        char thdname[16];
    
        snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);
        redis_set_thread_title(thdname);
        // 设置 I/O 线程的 CPU 亲和性,尽可能将 I/O 线程(以及主线程,不在这里设置)绑定到用户配置的
        // CPU 列表上。
        redisSetCpuAffinity(server.server_cpulist);
        makeThreadKillable();
    
        while(1) {
            // 忙轮询,100w 次循环,等待主线程分配 I/O 任务。
            for (int j = 0; j < 1000000; j++) {
                if (io_threads_pending[id] != 0) break;
            }
    
            // 如果 100w 次忙轮询之后如果还是没有任务分配给它,则通过尝试加锁进入休眠,
            // 等待主线程分配任务之后调用 startThreadedIO 解锁,唤醒 I/O 线程去执行。
            if (io_threads_pending[id] == 0) {
                pthread_mutex_lock(&io_threads_mutex[id]);
                pthread_mutex_unlock(&io_threads_mutex[id]);
                continue;
            }
    
            serverAssert(io_threads_pending[id] != 0);
    
            if (tio_debug) printf("[%ld] %d to handle\n", id, (int)listLength(io_threads_list[id]));
    
    
            // 注意:主线程分配任务给 I/O 线程之时,
            // 会把任务加入每个线程的本地任务队列 io_threads_list[id],
            // 但是当 I/O 线程开始执行任务之后,主线程就不会再去访问这些任务队列,避免数据竞争。
            listIter li;
            listNode *ln;
            listRewind(io_threads_list[id],&li);
            while((ln = listNext(&li))) {
                client *c = listNodeValue(ln);
                // 如果当前是写出操作,则把 client 的写出缓冲区中的数据回写到客户端。
                if (io_threads_op == IO_THREADS_OP_WRITE) {
                    writeToClient(c,0);
                  // 如果当前是读取操作,则socket 读取客户端的请求命令并解析第一条命令。
                } else if (io_threads_op == IO_THREADS_OP_READ) {
                    readQueryFromClient(c->conn);
                } else {
                    serverPanic("io_threads_op value is unknown");
                }
            }
            listEmpty(io_threads_list[id]);
            // 所有任务执行完之后把自己的计数器置 0,主线程通过累加所有 I/O 线程的计数器
            // 判断是否所有 I/O 线程都已经完成工作。
            io_threads_pending[id] = 0;
    
            if (tio_debug) printf("[%ld] Done\n", id);
        }
    }

    I/O 线程启动之后,会先进入忙轮询,判断原子计数器中的任务数量,如果是非 0 则表示主线程已经给它分配了任务,开始执行任务,否则就一直忙轮询一百万次等待,忙轮询结束之后再查看计数器,如果还是 0,则尝试加本地锁,因为主线程在启动 I/O 线程之时就已经提前锁住了所有 I/O 线程的本地锁,因此 I/O 线程会进行休眠,等待主线程唤醒。

    主线程会在每次事件循环中尝试调用 startThreadedIO 唤醒 I/O 线程去执行任务,如果接收到客户端请求命令,则 I/O 线程会被唤醒开始工作,根据主线程设置的 io_threads_op 标识去执行命令读取和解析或者回写响应数据的任务,I/O 线程在收到主线程通知之后,会遍历自己的本地任务队列 io_threads_list[id],取出一个个 client 执行任务:

    • 如果当前是写出操作,则调用 writeToClient,通过 socket 把 client->buf 或者 client->reply 里的响应数据回写到客户端。
    • 如果当前是读取操作,则调用 readQueryFromClient,通过 socket 读取客户端命令,存入 client->querybuf,然后调用 processInputBuffer 去解析命令,这里最终只会解析到第一条命令,然后就结束,不会去执行命令。
    • 在全部任务执行完之后把 自己的原子计数器置 0,以告知主线程自己已经完成了工作。
    void processInputBuffer(client *c) {
    // 省略代码
    ...
    
        while(c->qb_pos < sdslen(c->querybuf)) {
            /* Return if clients are paused. */
            if (!(c->flags & CLIENT_SLAVE) && clientsArePaused()) break;
    
            /* Immediately abort if the client is in the middle of something. */
            if (c->flags & CLIENT_BLOCKED) break;
    
            /* Don't process more buffers from clients that have already pending
             * commands to execute in c->argv. */
            if (c->flags & CLIENT_PENDING_COMMAND) break;
            /* Multibulk processing could see a <= 0 length. */
            if (c->argc == 0) {
                resetClient(c);
            } else {
                // 判断 client 是否具有 CLIENT_PENDING_READ 标识,如果是处于多线程 I/O 的模式下,
                // 那么此前已经在 readQueryFromClient -> postponeClientRead 中为 client 打上该标识,
                // 则立刻跳出循环结束,此时第一条命令已经解析完成,但是不执行命令。
                if (c->flags & CLIENT_PENDING_READ) {
                    c->flags |= CLIENT_PENDING_COMMAND;
                    break;
                }
    
                // 执行客户端命令
                if (processCommandAndResetClient(c) == C_ERR) {
                    /* If the client is no longer valid, we avoid exiting this
                     * loop and trimming the client buffer later. So we return
                     * ASAP in that case. */
                    return;
                }
            }
        }
    
    ...
    }

     3.5 redis的多线程试验

    io-threads 4
    io-threads-do-reads yes
    # 开启redis多线程
    
    strace -ff -o log redis服务path conf-path
    
    # redis 压测命令
    
    redis-benchmark -t set -c 50

    四、redis线程模型总结

    测试数据表明,如果在4核的时候,redis6.0的数据的QPS将变成20W。同时在8个I/O的thread的时候能够弥补内核和app之间的系统调用的损耗的。。Redis 在使用多线程模式之后性能大幅提升,达到了一倍。更详细的性能压测数据。

    模型缺陷

    首先第一个就是我前面提到过的,Redis 的多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers 模型,和其他主流的开源网络服务器的模式有所区别,最大的不同就是在标准的 Multi-Reactors/Master-Workers 模式下,Sub Reactors/Workers 会完成 网络读 -> 数据解析 -> 命令执行 -> 网络写 整套流程,Main Reactor/Master 只负责分派任务,而在 Redis 的多线程方案中,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令,所有客户端命令最后还需要回到主线程去执行,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。

    Redis 之所以如此设计它的多线程网络模型,我认为主要的原因是为了保持兼容性,因为以前 Redis 是单线程的,所有的客户端命令都是在单线程的事件循环里执行的,也因此 Redis 里所有的数据结构都是非线程安全的,现在引入多线程,如果按照标准的 Multi-Reactors/Master-Workers 模式来实现,则所有内置的数据结构都必须重构成线程安全的,这个工作量无疑是巨大且麻烦的。

    Redis 多线程网络模型的设计方案:

    • 使用 I/O 线程实现网络 I/O 多线程化,I/O 线程只负责网络 I/O 和命令解析,不执行客户端命令。
    • 利用原子操作+交错访问实现无锁的多线程模型。
    • 通过设置 CPU 亲和性,隔离主进程和其他子进程,让多线程网络模型能发挥最大的性能。

    博文参考

    Benchmarking the experimental Redis Multi-Threaded I/O

    为什么 Redis 选择单线程模型 · Why's THE Design?(001) - 知乎

    Redis 多线程网络模型全面揭秘 - 知乎

    Redis v5.0.10

    Redis v6.0.10

    Lazy Redis is better Redis

    An update about Redis developments in 2019

    How fast is Redis?

    Go netpoller 原生网络模型之源码全面揭秘

    Linux I/O 原理和 Zero-copy 技术全面揭秘

    Benchmarking the experimental Redis Multi-Threaded I/O

    NUMA DEEP DIVE PART 1: FROM UMA TO NUMA

    展开全文
  • 实验目的:通过实验掌握下列知识:1、在Linux环境下,掌握pthread库中的mutex对象,pthread_mutex_lock()函数,pthread_mutex_init和pthread_self()的用法。2、在Linux环境下,掌握pthread_create()函数,pthread_join...

    实验目的:

    通过实验掌握下列知识:

    1、在Linux环境下,掌握pthread库中的mutex对象,pthread_mutex_lock()函数,pthread_mutex_init和pthread_self()的用法。

    2、在Linux环境下,掌握pthread_create()函数,pthread_join()函数的用法。

    3、在Linux环境下,掌握pthread_cancel(pthread_t

    thread),pthread_kill()的用法。

    一、多线程和互斥锁

    (1) 使用头文件,pthread_mutex_lock()函数锁住由mutex指定的mutex

    对象。如果mutex已经被锁住,调用pthread_mutex_lock()函数的线程阻塞直到mutex可用为止。pthread_self()可以获得线程自身的ID。pthread_mutex_init用于互斥锁的初始化。

    #include

    #include

    #include

    int data=0;

    //共享数据

    pthread_mutex_t mutex;

    //互斥锁

    void *thread1(void

    *argu)

    //子线程执行函数

    {

    int

    i;

    if(pthread_mutex_lock(&mutex)!=0)

    {

    printf("lock error!\n");

    return NULL;

    }

    for(i=0;i<6;i++)

    {

    printf("thread2: %d =

    %d\n",pthread_self(),data++);

    sleep(1);

    }

    if(pthread_mutex_unlock(&mutex)!=0)

    {

    printf("unlock error!\n");

    return NULL;

    }

    }

    int main()

    {

    pthread_t id;

    int

    i;

    if(pthread_mutex_init(&mutex,NULL)!=0)

    //在创建线程之前初始化锁

    {

    printf("init error!\n");

    return 1;

    }

    int

    ret=pthread_create(&id,NULL,thread1,NULL);

    //创建子线程

    if(ret!=0)

    {

    printf("creata error!\n");

    return 1;

    }

    if(pthread_mutex_lock(&mutex)!=0)

    //对共享数据加锁

    {

    printf("lock error!\n");

    return 1;

    }

    for(i=0;i<6;i++)

    {

    printf("thread1: %d %d ",pthread_self(),data++);

    sleep(1);

    }

    if(pthread_mutex_unlock(&mutex)!=0)

    //对共享数据解锁

    {

    printf("unlock error!\n");

    return 1;

    }

    pthread_join(id,NULL);

    return 0;

    }

    完成程序的编写,输入以下命令,对pthread1.c程序进行编译,在编译时,需要添加pthread的库文件。

    $ gcc pthread1.c -o pthread1

    –lpthread

    二、多线程的创建

    (2)线程的创建,利用线程调用函数。

    #include

    #include

    #include

    #include

    #include

    pthread_t

    ntid;

    void printids(const

    char *s)

    {

    pid_t pid;

    pthread_t

    tid;

    pid =

    getpid();//返回当前的进程号

    tid =

    pthread_self();

    //返回当前线程的线程ID号

    printf("%s pid %u tid %u

    (0x%x)\n", s, (unsigned int)pid, (unsigned int)tid, (unsigned

    int)tid);

    }

    void *thr_fn(void

    *arg)

    {

    printids(arg); return

    NULL;

    }

    int

    main(void)

    {

    int err;

    err =

    pthread_create(&ntid, NULL, thr_fn, "new thread:

    "); //创建进程,并传递”new

    thread”参数

    if (err !=

    0)

    {

    fprintf(stderr, "can't create

    thread: %s\n", strerror(err));

    exit(1);

    }

    printids("main

    thread:");

    //再次调用printids函数

    sleep(1);

    return 0;

    }

    完成程序的编写,输入以下命令,对thread2.c程序进行编译,在编译时,需要添加pthread的库文件。

    $ gcc thread2.c -o thread2

    -lpthread

    ./thread2

    main thread: pid 7398 tid

    3084450496 (0xb7d8fac0)

    new thread: pid 7398 tid

    3084446608 (0xb7d8eb90)

    (3)定理三个函数线程,利用pthread_create ()中四个参数分别是:第一个参数为指向线程标识符的指针;第二个参数用来设置线程属性;第三个参数是线程运行函数的起始地址;

    最后一个参数是运行函数的参数。pthread_join等待线程结束,第一个参数thread: 线程标识符,即线程ID,标识唯一线程;第二个参数用来设置用户定义的指针,用来存储被等待线程的返回值。retval返回值:0代表成功。失败,返回的则是错误号。

    #include

    #include

    #include

    #include

    void *thr_fn1(void

    *arg)

    {

    printf("thread 1

    returning\n");

    return (void

    *)1;

    }

    void *thr_fn2(void

    *arg)

    {

    printf("thread 2

    exiting\n");

    pthread_exit((void

    *)2);

    //调用pthread_exit函数终止执行

    }

    void *thr_fn3(void

    *arg)

    {

    while(1) {

    printf("thread 3

    writing\n");

    sleep(1);

    }

    }

    int

    main(void)

    {

    pthread_t

    tid;

    void *tret;

    pthread_create(&tid, NULL,

    thr_fn1, NULL);

    //创建线程tid,调用thr_fn1函数

    pthread_join(tid,

    &tret);

    //等待线程tid结束

    printf("thread 1 exit code

    %d\n", (int)tret);

    //打印线程tid的返回值

    pthread_create(&tid, NULL,

    thr_fn2, NULL);

    //创建线程tid,调用thr_fn2函数

    pthread_join(tid,

    &tret);

    //等待线程tid结束

    printf("thread 2 exit code

    %d\n", (int)tret);

    //打印线程tid的返回值

    pthread_create(&tid, NULL,

    thr_fn3, NULL);

    //创建线程tid,调用thr_fn3函数

    sleep(3);

    //休眠3秒

    pthread_cancel(tid);

    //发送终止信号给thread线程

    pthread_join(tid,

    &tret);

    //等待线程tid结束

    printf("thread 3 exit code

    %d\n", (int)tret);

    return 0;

    }

    完成程序的编写,输入以下命令,对thread3.c程序进行编译,在编译时,需要添加pthread的库文件。

    $ gcc thread3.c -o thread3

    -lpthread

    ./thread3

    展开全文
  • 1. 学习第18章“多线程服务器端的实现”,掌握线程创建、线程同步的原理和实现方法。 2. 在Linux操作系统上编写基于多线程的聊天室程序。
  • 文章目录 线程创建和退出 原理 代码 结果 线程属性修改 原理 代码 结果 互斥锁 原理 代码 结果 信号量线程控制原理 使用信号量线程互斥 代码 结果 使用信号量线程同步 代码 结果 小结分析 “生产者消费者”实验 问题...
  • 对结果进行性能评价 2 实验原理 使用积分方法即 1 4 2 dx 4 1 0 1 x i 0.5 2 N 0 i N 1 N 1/14 3 程序流程图 图 主线程流程图 4 实现方法 方法简述 本程序使用多线程方法 首先启动主进程输入基数和线
  • 【java】--多线程原理

    2021-03-06 15:37:02
    自考在《操作系统中》学到了关于进程和线程的概念,结合Demo更好的理解了多线程原理,这篇博客主要讲一下多线程的创建和我对这个创建过程的理解。首先展现一张思维导图: 进程:是一个正在执行中的程序。每一个进程...
  • Java并发编程技术官笔记 由于篇幅原因,我只能在文章...对Java里的线程-点点认识 线程间的共享和协作 面试题 CPU核心数和线程数的关系 澄清并行和并发 2,线程的并发工具类 Fork-Join CountDownLatch CyclicB
  • Java并发编程技术官笔记 由于篇幅原因,我只能在文章...对Java里的线程-点点认识 线程间的共享和协作 面试题 CPU核心数和线程数的关系 澄清并行和并发 2,线程的并发工具类 Fork-Join CountDownLatch CyclicB
  • JAVA多线程总结

    2022-03-20 22:59:24
    首先我们要知道进程是系统进行资源分配和调度的基本单位,而线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的线程共享进程的资源。 我们之前启动程序执行后,main方法的执行其实就是一条单独的...
  • 多进程与多线程的实现与原理

    千次阅读 2018-02-12 01:34:34
    多进程 ...使用多线程实现tcp并发 开启线程的俩种方式 进程和线程的俩种区别 区别一:启动数据快 区别二:线程间资源共享,进程间资源独立 守护线程的使用 线程的互斥锁 线程的GIL锁(解释器锁) pa...
  • Linux下基于多线程的服务器程序设计研究.pdf
  • 3. 熟悉Makefile工作原理,掌握编写Makefile的编写方法。 二、实验基本要求 1. 掌握熟悉线程的定义及操作方法。 2. 利用信号量的PV操作完成完成以下单个生产者和单个消费者模型的代码。 3. 编写在Ubuntu中编译执行的...
  • 一、实验目的:了解线程的概念、线程的生命周期,掌握多线程的编程。掌握异常的概念以及如何定义、抛出和捕捉处理异常。 二、实验环境:Java SE 三、实验内容:(一)Thread子类的方法实现多线程 运行截图如下: ...
  • Linux线程实现原理

    2021-11-07 14:16:57
    一,说在前面 ...4.当我们用pthread_create创建一个或个属于原进程的线程时,也会创建新的独一无二的task_struct,含有独一无二的pid值,但是他们隶属于一个线程组,这个线程组的leader thread是原始进程,
  • 多线程 之 Lock 锁的实现原理

    千次阅读 2017-04-05 16:44:52
    1. Lock 的简介及使用 Lock完全用Java写成,在java这个层面是无关JVM实现的,Lock ...//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 void lockInterruptibly() throws InterruptedExc
  • Windows多线程编程

    2021-01-22 16:53:52
    熟练掌握Windows下的多线程编程,能够让我们编写出更规范多线程代码,避免不要的异常。Windows下的多线程编程非常复杂,但是了解一些常用的特性,已经能够满足我们普通多线程对性能及其他要求。 进程与线程 1. 进程...
  • 实验一 Linux多线程编程一》由会员分享,可在线阅读,更多相关《实验一 Linux多线程编程一(6页珍藏版)》请在人人文库网上搜索。1、实验一 Linux多线程编程I一、 实验目的熟悉GNU GCC编译器,能够用Pthreads线程库...
  • 实验二.多线程应用程序设计
  • 《操作系统实验(进程)多线程实现矩阵乘法》由会员分享,可在线阅读,更多相关《操作系统实验(进程)多线程实现矩阵乘法(6页珍藏版)》请在人人文库网上搜索。1、多线程编程实现矩阵乘法一、实验目的通过实验,熟悉基于...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,041
精华内容 18,816
关键字:

多线程实验原理