[技术八卦]放毒记

博客记录了一场Delphi版聚会上的技术交流,讨论了多种技术如FreeBSD/Linux、PostgreSQL、Python/Ruby等的优势,对比了*nix与Windows在服务器领域的表现,并分享了使用动态语言进行开发的心得。

这是关于上周末Delphi版聚的记录。

放毒一词源于摄影器材论坛:旦凡有人在坛里大肆鼓吹某些高级器材——比如Leica机、Zeiss头一类的(此即为放毒),往往导致观者心痒难耐(此为中毒),最终忍不住大出血,也去败了回来(毒发)。

当然这回我放的毒都是技术毒。

算 下来我们这一帮人,大多已经不怎么拿DELPHI作为主要兵器了,于是便扯的都是别的技术。549谈的是MS的测试技术,大风则是谈JAVA,我当然谈开 源,主打便是FreeBSD/Linux、PostgreSQL、Python/Ruby等,顺道批判了一下RAD/IDE。

FreeBSD/Linux 的安全性那是地球人都知道——当然有人说,Windows配置得好也很安全,但是要把Windows配置到安全那么好,所需要的技术含量并不比 FreeBSD/Linux要少。更何况绝大多数人都是默认安装的,而Windows的默认安装有多不安全那是勿庸置疑的。而且我认为,相对来说,基于源 码安装应用程序的FreeBSD比基于二进制安装的Linux更加安全(见《安全意味着麻烦》)。

大风问我:*nux相对于Windows,除了安全性以外,还有什么好处?

我 开始还真愣了一下,安全就是最大的好处了嘛。不过想了想,还是想到一些,比如性能。我是在虚拟机上装FreeBSD的,因为是纯粹的服务器,没有装 Xwindow,性能还是相当不错的。而Windows即使是做服务器用,它的GUI部分也是集成在内核里的,白白消耗资源,一点都不环保。

再 有就是软件安装的方便性。比如在Ubuntu下,一个包管理器(其实还是更喜欢用apt-get install)可以安装几乎所有应用软件,哪像在Windows下,还要自己到处去找,说不定下载来的软件里还有流氓插件。至于FreeBSD就更好 了,Ports里都是源码安装的,自己更改编译选项,可以保证你不需要的东西一点都不会出现在你的电脑里。

考虑到人力成本的因素,用*nix做服务器的TCO可能的确会比Windows高,但是得到的结果绝对要好得多——远远超过那一点点TCO的差别。

至于PostgreSQL,我在《选择什么数据库》里也说过了,在管理的易用性和开发的方便性方面,它并不比MS SQL差多少,而功能和性能也差不了很多——更何况它还是跨平台的。而安全性则无疑要比MS SQL要好一些——特别是在非Windows平台下。

还有动态语言。现在已经习惯了,在自己用的每台电脑上装上Python,手边也写了一堆的小工具脚本——大多都是几行代码的东东,要是换成原生开发工具,还不知道要多麻烦,更别提跨平台了。

前 一段玩matplotlib感觉很爽,后来又因为Django/TurboGears的关系,又玩了一段SQLAlchemy(一个Python的ORM)。 ORM是个好东东,但是在原生开发中(包括基于虚拟机JAVA/.net里),ORM的实现都太丑陋,又难用,剩下的唯一好处大概就是性能了。但是在动态 语言中,ORM是如此的自然,仿佛数据持久化已经不存在一般,真正做到了专心处理逻辑而不必再为持久化所困扰。为了这个好处,我愿意牺牲那一点点性能的损 失——实际上,对于大多数数据库应用来说,数据库的性能往往才是整个应用的瓶颈。

再加上框架。RoR自不必说,Python下的 Django和TurboGears也都是好东东。也许用惯了IDE的人很难理解,这样的框架居然可以比IDE有更好的易用性。但事实就是如此。IDE看 上去简单,拖拖控件而已,但是还是有很多事情需要写代码,而且代码与界面搞在一起,简直惨不忍睹。而RoR类的框架下,要做的事情只是写几行代码。是拖几 个控件简单,还是写几行代码简单?后者只要一个最简单的文本编辑器就可以做到,而前者离开了IDE则一事无成。

说到编辑器,不免又谈到IDE里的代码编辑器。不过现在对我来说,最常用的编辑器已经是VIM了。^O^

放毒的目的就是要让人中毒。我放的这些可都是好毒,既不用花一分钱,还有种种好处,花些精力去学一学用一用是很值得的。

 

Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=1450270


<think> 我们面对的是一个多线程任务场景,需要处理以下几个子任务: 1. 使用自定义线程池处理多个数据表的冷热数据识别、冷数据转储及数据校验(可以看作一个批量任务) 2. 用一个单独的线程录在冷数据转移过程中热库数据发生的修改操作(日志录线程) 3. 主线程需要等待所有子线程(包括线程池中的任务和那个单独的日志录线程)完成任务后再继续。 解决方案: 1. 创建自定义线程池(如通过`ThreadPoolExecutor`)来处理多个数据表的任务。 2. 创建一个单独的线程(可以使用`Thread`)来录热库数据修改操作。 3. 主线程等待所有任务完成,包括线程池中的任务和那个单独的日志线程。 具体步骤: a. 提交所有数据表处理任务到线程池。 b. 启动日志录线程。 c. 主线程需要等待两个部分:线程池中的所有任务完成,以及日志录线程完成。 注意:由于日志录线程可能会一直运行(直到数据转移完成),我们需要在适当的时候通知它结束。 实现等待的方法: - 对于线程池,我们可以调用`shutdown()`然后使用`awaitTermination`等待,或者使用`CountDownLatch`/`CyclicBarrier`等同步工具。 - 对于日志录线程,我们可以使用`thread.join()`来等待。 但是,由于日志录线程需要在所有数据表处理任务完成之后才能结束(因为转移过程中可能还有修改操作),所以我们需要先等待线程池任务完成,然后通知日志录线程停止,最后等待日志录线程结束。 步骤细化: 1. 创建一个线程池(假设大小为N)和一个用于停止日志录的标志(例如一个volatile变量)或一个阻塞队列(用于传递停止信号)。 2. 提交所有数据表任务到线程池。 3. 启动日志录线程,该线程循环检查停止标志或从队列中取消息,当收到停止信号时退出循环。 4. 主线程等待线程池任务完成: - 调用线程池的`shutdown()`方法,然后调用`awaitTermination(long timeout, TimeUnit unit)`等待所有任务完成。 5. 主线程在确认线程池任务已经完成后,设置停止标志(或向队列发送停止信号),通知日志录线程停止。 6. 调用日志录线程的`join()`方法,等待其结束。 另外,由于日志录线程可能还需要处理在停止信号发出之前已经进入队列的修改操作,所以停止信号发出后,日志录线程会处理完已进入队列的操作再停止。 代码结构示例(Java): ```java // 创建线程池 ExecutorService dataProcessPool = Executors.newFixedThreadPool(nThreads); // 用于停止日志线程的标 volatile boolean stopLogging = false; // 创建一个队列,用于传递修改操作日志 BlockingQueue<ModificationLog> logQueue = new LinkedBlockingQueue<>(); // 提交数据表处理任务 for (DataTable table : tables) { dataProcessPool.submit(() -> processTable(table, logQueue)); // 在任务中,如果发生热库数据修改,就将修改操作放入logQueue } // 创建并启动日志录线程 Thread loggingThread = new Thread(() -> { while (!stopLogging || !logQueue.isEmpty()) { // 当未收到停止信号或队列不为空时继续 try { ModificationLog log = logQueue.poll(100, TimeUnit.MILLISECONDS); // 设置超时避免永久阻塞 if (log != null) { // 录日志 recordLog(log); } } catch (InterruptedException e) { // 处理中断 Thread.currentThread().interrupt(); break; } } }); loggingThread.start(); // 主线程等待线程池任务完成 dataProcessPool.shutdown(); // 不再接受新任务 try { // 等待线程池中的任务完成,设置合理的超时时间 if (!dataProcessPool.awaitTermination(1, TimeUnit.HOURS)) { // 如果超时,可以取消未完成的任务 dataProcessPool.shutdownNow(); } } catch (InterruptedException e) { dataProcessPool.shutdownNow(); Thread.currentThread().interrupt(); } // 设置停止标志,通知日志线程停止 stopLogging = true; // 等待日志线程结束 try { loggingThread.join(); } catch (InterruptedException e) { // 处理中断 Thread.currentThread().interrupt(); } ``` 注意事项: - 在数据表处理任务中,如果对热库进行了修改操作,需要将修改操作作为日志对象放入`logQueue`。 - 日志录线程在退出循环条件中,我们检查`stopLogging`为true并且队列为空才退出,这样可以保证队列中的剩余日志被处理完。 - 使用`poll`方法并设置超时是为了避免无限期阻塞,以便定期检查停止条件。 另一种更优雅的方式是使用一个特殊的“毒丸”对象(Poison Pill)来通知日志线程停止。当消费者从队列中取到毒丸对象时,就停止运行。这种方式的好处是我们可以发送多个毒丸(如果多个生产者)或者一个毒丸(如果只有一个消费者),且不需要额外的停止标志。 使用毒丸的示例: ```java // 在日志队列中定义一个特殊的毒丸对象 ModificationLog POISON_PILL = new ModificationLog(); // 可以设计一个特殊对象 // 提交任务后,在需要停止时向队列放入毒丸 // 日志录线程: Thread loggingThread = new Thread(() -> { while (true) { ModificationLog log = logQueue.take(); // 阻塞取 if (log == POISON_PILL) { break; } recordLog(log); } }); // ... 主线程在等待线程池任务完成后: logQueue.put(POISON_PILL); // 放入毒丸 loggingThread.join(); ``` 但是注意:如果有多个生产者(多个数据表处理任务)向队列放入日志,那么每个生产者都应该在任务结束时放入一个毒丸?这样消费者就需要收到多个毒丸才停止,或者我们只放一个毒丸,但这样就需要保证只有一个生产者会放毒丸。这里只有一个地方会放毒丸(主线程),所以使用一个毒丸即可。 然而,在数据表处理任务中,我们并不放入毒丸,而是由主线程在确认所有数据表任务完成后放入一个毒丸。日志线程一旦取到毒丸就停止。 因此,采用毒丸的方式可以避免使用停止标志,代码更清晰。 综上,推荐使用毒丸的方式。 修改后的代码示例: ```java ExecutorService dataProcessPool = Executors.newFixedThreadPool(nThreads); BlockingQueue<ModificationLog> logQueue = new LinkedBlockingQueue<>(); final ModificationLog POISON_PILL = new ModificationLog(); // 特殊对象,用于停止日志线程 // 提交任务 for (DataTable table : tables) { dataProcessPool.submit(() -> { // 处理表,过程中可能产生日志 // 如果有修改,就向logQueue放入日志 // 注意:不要放入POISON_PILL }); } // 日志录线程 Thread loggingThread = new Thread(() -> { try { while (true) { ModificationLog log = logQueue.take(); if (log == POISON_PILL) { break; } recordLog(log); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); loggingThread.start(); // 主线程等待线程池任务完成 dataProcessPool.shutdown(); try { dataProcessPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); // 或者设置超时 } catch (InterruptedException e) { // 处理中断 dataProcessPool.shutdownNow(); Thread.currentThread().interrupt(); } // 放入毒丸 logQueue.put(POISON_PILL); // 等待日志线程结束 loggingThread.join(); ``` 注意:这里放入一个毒丸,日志线程取到毒丸就会退出。但是,有可能在放入毒丸之前,日志线程已经在阻塞等待,所以毒丸会被立即取到。 然而,如果队列中还有大量的日志,毒丸会在所有日志之后被处理,因此确保所有日志都被处理后才停止。 这种方法可以确保日志线程处理完队列中所有日志(在毒丸之前的所有日志)然后退出。 总结: 使用线程池处理数据表任务,使用单独的日志录线程,主线程等待线程池任务完成,然后通过毒丸通知日志线程停止,最后等待日志线程结束。 根据具体的编程语言和线程库,实现方式可能有所不同,但思路相同。 在Python中,我们可以使用`concurrent.futures`模块的线程池,以及`queue.Queue`,同样使用毒丸的方式。 由于问题中没有指定语言,这里提供了Java的示例,但思路适用于其他语言。 </think> ### 主线程等待子线程完成任务的实现方案 在自定义线程池处理多数据表冷热数据识别、转储、校验,并单独使用线程录热库修改操作的场景下,主线程等待子线程完成的推荐实现方式如下: #### 核心实现步骤 1. **线程池任务提交** ```java // 创建自定义线程池(根据CPU核心数设置大小) ExecutorService dataTransferPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // 创建单线程录器 ExecutorService logExecutor = Executors.newSingleThreadExecutor(); // 提交数据表处理任务 List<Future<?>> dataTransferFutures = new ArrayList<>(); for (DataTable table : tables) { dataTransferFutures.add(dataTransferPool.submit(() -> { identifyColdData(table); // 冷热识别 transferColdData(table); // 冷数据转储 validateDataConsistency(table); // 数据校验 })); } // 提交热库修改录任务 Future<?> logFuture = logExecutor.submit(() -> { while (!Thread.currentThread().isInterrupted()) { recordHotDataModifications(); // 持续录热库修改 } }); ``` 2. **主线程等待机制** ```java // 等待数据转移任务完成 for (Future<?> future : dataTransferFutures) { try { future.get(); // 阻塞直到单任务完成 } catch (InterruptedException | ExecutionException e) { Thread.currentThread().interrupt(); // 异常处理逻辑 } } // 关闭数据转移线程池 dataTransferPool.shutdown(); try { dataTransferPool.awaitTermination(30, TimeUnit.MINUTES); // 设置超时 } catch (InterruptedException e) { // 处理中断 } // 停止日志录线程 logExecutor.shutdownNow(); // 发送中断信号 try { if (!logExecutor.awaitTermination(5, TimeUnit.SECONDS)) { // 强制终止未能响应的线程 } logFuture.get(); // 确保日志线程完全退出 } catch (InterruptedException | ExecutionException e) { // 异常处理 } ``` #### 关键同步组件解释 1. **`Future` 机制** - 通过 `future.get()` 实现阻塞等待 - 可捕获子线程执行异常 - 支持批量任务状态跟踪 2. **线程池关闭流程** - `shutdown()`: 停止接收新任务 - `awaitTermination()`: 等待现有任务完成 - `shutdownNow()`: 强制中断运行中任务(发送Interrupt信号) 3. **日志线程安全停止** - 使用 `isInterrupted()` 检查中断标志 - 在日志录循环中增加中断检查点: ```java while (!Thread.currentThread().isInterrupted()) { // 录操作... } ``` #### 优化建议 1. **增加超时控制** ```java future.get(30, TimeUnit.MINUTES); // 单任务超时设置 ``` 2. **使用 `CountDownLatch` 精确同步** ```java CountDownLatch latch = new CountDownLatch(tables.size()); for (DataTable table : tables) { pool.submit(() -> { try { // 任务逻辑... } finally { latch.countDown(); // 确保计数减少 } }); } latch.await(); // 主线程阻塞等待 ``` 3. **异常聚合处理** ```java List<Exception> errors = Collections.synchronizedList(new ArrayList<>()); // 任务内捕获异常添加到errors if (!errors.isEmpty()) { // 统一处理异常 } ``` ### 架构示意图 ``` 主线程 │ ├─ 启动数据转移线程池 → [表1处理] [表2处理] ... [表N处理] │ │ │ │ │ │ │ └─ 冷热识别 → 冷数据转储 → 数据校验 │ │ │ │ │ └─ Future.get() 等待完成 │ │ │ └─ CountDownLatch 同步点 │ ├─ 启动日志线程 → [持续录热库修改] │ │ │ └─ 中断机制停止 │ └─ 等待所有Future完成 → 关闭线程池 → 结束 ``` ### 注意事项 1. 使用 `ThreadPoolExecutor` 替代 `Executors` 以获得更精细控制 2. 日志录线程需设计成可中断模式 3. 数据库连接需使用线程安全的数据源 4. 冷数据转移过程需加分布式锁避免数据竞争 5. 建议添加监控指标: ```java // 监控任务队列积压 ((ThreadPoolExecutor)pool).getQueue().size() // 监控活跃线程数 ((ThreadPoolExecutor)pool).getActiveCount() ``` 此方案确保主线程在所有子任务(包括线程池任务和独立日志线程)完成后才继续执行,并通过组合使用Future、CountDownLatch和线程池生命周期管理实现安全关闭[^1]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值