跳到主要内容
Linux 线程同步与互斥深度解析:从锁机制到生产者消费者模型 | 极客日志
C++ 算法
Linux 线程同步与互斥深度解析:从锁机制到生产者消费者模型 Linux 多线程编程中线程同步与互斥涉及临界资源保护与线程协作机制。文章详细阐述了互斥量(mutex)如何防止数据竞争,条件变量(cond)如何实现有序等待与唤醒。重点介绍了生产者消费者模型的两种实现方案:基于阻塞队列的动态扩容与基于环形队列的固定容量设计,对比了各自的性能与适用场景。此外还涵盖了线程安全与可重入函数的区别,以及死锁产生的条件与预防策略,提供了完整的 C/C++ 代码示例。
战神 发布于 2026/2/20 更新于 2026/9/19 71 浏览一、引言:多线程共享资源的问题
多线程的核心优势是共享进程资源、提高并发效率,但共享资源带来了一个致命问题——竞态条件(Race Condition) :多个线程并发操作共享资源时,由于执行时序不确定,导致最终结果与预期不符。比如多线程售票系统可能卖出负数票,银行转账可能导致余额不一致。
线程同步与互斥的核心目标,就是在保证多线程并发的同时,解决竞态条件,确保数据一致性。
1.1 为什么需要同步与互斥?
举个直观例子:未加保护的售票系统
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100 ;
void * sell_ticket (void * arg) {
char * id = (char *)arg;
while (1 ) {
if (ticket > 0 ) {
usleep(1000 );
printf ("%s sells ticket:%d\n" , id, ticket);
ticket--;
} else {
break ;
}
}
}
int main () {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL , sell_ticket, (void *)"thread 1" );
pthread_create(&t2, NULL , sell_ticket, (void *)"thread 2" );
pthread_create(&t3, NULL , sell_ticket, (void *)"thread 3" );
pthread_create(&t4, , sell_ticket, ( *) );
pthread_join(t1, );
pthread_join(t2, );
pthread_join(t3, );
pthread_join(t4, );
;
}
NULL
void
"thread 4"
NULL
NULL
NULL
NULL
return
0
if (ticket > 0)判断后,线程可能被切换,其他线程继续修改ticket;
ticket--对应三条汇编指令(load→update→store),非原子操作,可能被打断。
解决思路:互斥 保证同一时间只有一个线程访问临界资源,同步 保证线程按合理顺序访问。
1.2 核心概念铺垫
共享资源 :多个线程均可访问的资源(如全局变量、堆内存、文件描述符);
临界资源 :需要被保护的共享资源(如售票系统的ticket);
临界区 :访问临界资源的代码段(如if (ticket > 0)到ticket--的代码);
互斥 :任何时刻仅允许一个线程进入临界区,保证资源独占访问;
同步 :在互斥基础上,保证线程按预期顺序访问资源(如生产者先生产,消费者后消费);
原子操作 :不可被打断的操作(如硬件提供的swap指令),是互斥的底层基础。
二、线程互斥:用互斥量(mutex)守护临界资源 互斥量(mutex)是 Linux 提供的核心互斥工具,本质是一把'锁',通过原子操作实现锁的获取与释放,确保临界区的独占访问。
2.1 互斥的核心:临界资源与临界区
临界区必须是'最小粒度':仅包含访问临界资源的必要代码,避免过度阻塞影响并发效率;
互斥的三大要求:
互斥性:同一时间仅一个线程进入临界区;
空闲让进:临界区空闲时,允许等待线程进入;
有限等待:线程等待锁的时间有限,避免饥饿。
2.2 互斥量接口与实战 POSIX 线程库提供互斥量相关接口,需包含<pthread.h>,链接时加-lpthread。
2.2.1 互斥量核心接口 接口 功能 关键说明 pthread_mutex_init动态初始化互斥量 attr=NULL使用默认属性PTHREAD_MUTEX_INITIALIZER静态初始化互斥量 全局或静态互斥量使用,无需销毁 pthread_mutex_lock加锁 阻塞等待,直到获取锁 pthread_mutex_unlock解锁 必须由加锁线程解锁 pthread_mutex_destroy销毁互斥量 静态初始化的互斥量无需销毁
2.2.2 实战:改进售票系统 #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100 ;
pthread_mutex_t mutex;
void * sell_ticket (void * arg) {
char * id = (char *)arg;
while (1 ) {
int current_ticket = 0 ;
pthread_mutex_lock(&mutex);
if (ticket <= 0 ) {
pthread_mutex_unlock(&mutex);
break ;
}
current_ticket = ticket;
ticket--;
pthread_mutex_unlock(&mutex);
usleep(1000 );
printf ("%s sells ticket:%d\n" , id, current_ticket);
}
return NULL ;
}
int main () {
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL );
pthread_create(&t1, NULL , sell_ticket, (void *)"thread 1" );
pthread_create(&t2, NULL , sell_ticket, (void *)"thread 2" );
pthread_create(&t3, NULL , sell_ticket, (void *)"thread 3" );
pthread_create(&t4, NULL , sell_ticket, (void *)"thread 4" );
pthread_join(t1, NULL );
pthread_join(t2, NULL );
pthread_join(t3, NULL );
pthread_join(t4, NULL );
pthread_mutex_destroy(&mutex);
return 0 ;
}
运行结果 :无负数票,每个线程独占临界区,数据一致。
2.3 RAII 风格锁封装:避免锁泄漏 手动加锁/解锁容易出现'忘记解锁'或'异常时未解锁'的问题,采用 RAII(资源获取即初始化)风格封装锁,利用 C++ 析构函数自动释放锁:
#pragma once
#include <pthread.h>
namespace LockModule {
class Mutex {
public :
Mutex () { pthread_mutex_init (&_mutex, NULL ); }
~Mutex () { pthread_mutex_destroy (&_mutex); }
void Lock () { pthread_mutex_lock (&_mutex); }
void Unlock () { pthread_mutex_unlock (&_mutex); }
pthread_mutex_t * GetRawMutex () { return &_mutex; }
private :
Mutex (const Mutex&) = delete ;
Mutex& operator =(const Mutex&) = delete ;
pthread_mutex_t _mutex;
};
class LockGuard {
public :
LockGuard (Mutex& mutex) : _mutex(mutex) { _mutex.Lock (); }
~LockGuard () { _mutex.Unlock (); }
private :
LockGuard (const LockGuard&) = delete ;
LockGuard& operator =(const LockGuard&) = delete ;
Mutex& _mutex;
};
}
#include "Lock.hpp"
#include <stdio.h>
#include <unistd.h>
using namespace LockModule;
int ticket = 100 ;
Mutex mutex;
void * sell_ticket (void * arg) {
char * id = (char *)arg;
while (1 ) {
int current_ticket = 0 ;
{
LockGuard lock (mutex) ;
if (ticket <= 0 ) break ;
current_ticket = ticket;
ticket--;
}
usleep (1000 );
printf ("%s sells ticket:%d\n" , id, current_ticket);
}
return NULL ;
}
int main () {
pthread_t t1, t2, t3, t4;
pthread_create (&t1, NULL , sell_ticket, (void *)"thread 1" );
pthread_create (&t2, NULL , sell_ticket, (void *)"thread 2" );
pthread_create (&t3, NULL , sell_ticket, (void *)"thread 3" );
pthread_create (&t4, NULL , sell_ticket, (void *)"thread 4" );
pthread_join (t1, NULL );
pthread_join (t2, NULL );
pthread_join (t3, NULL );
pthread_join (t4, NULL );
return 0 ;
}
三、线程同步:条件变量(cond)实现有序协作 互斥量解决了'同一时间只有一个线程访问资源',但无法解决'线程按顺序访问'的问题。例如上面的售票系统输出顺序我们是无法控制的,此时需要条件变量 。
3.1 同步的意义:解决'竞态条件'
竞态条件 :因线程执行时序不确定导致程序异常(如消费者先消费空队列);
同步 :在保证互斥的基础上,让线程按预期顺序执行,避免竞态条件。
3.2 条件变量接口与核心原理 条件变量允许线程在某个条件不满足时主动等待,当条件满足时被其他线程唤醒。它本身不保证互斥,需与互斥量配合使用,核心接口如下:
接口 功能 关键说明 pthread_cond_init动态初始化条件变量 attr=NULL使用默认属性PTHREAD_COND_INITIALIZER静态初始化条件变量 全局或静态条件变量使用 pthread_cond_wait等待条件满足 1. 自动释放互斥量;2. 被唤醒后重新获取互斥量 pthread_cond_signal唤醒一个等待线程 唤醒等待队列中的一个线程 pthread_cond_broadcast唤醒所有等待线程 避免线程饥饿 pthread_cond_destroy销毁条件变量 静态初始化的无需销毁
为什么 pthread_cond_wait 需要互斥量?
条件的判断和修改依赖共享资源(如队列是否为空),需互斥保护;
解锁和等待必须是原子操作:若先解锁再等待,可能错过其他线程的信号,导致永久阻塞。pthread_cond_wait内部自动完成'解锁→等待→唤醒后加锁'的原子流程。
3.3 条件变量使用规范 等待条件 :必须用 while 循环,而非 if,避免伪唤醒 (线程被唤醒但条件未满足):
pthread_mutex_lock (&mutex);
while (条件不满足) {
pthread_cond_wait (&cond, &mutex);
}
pthread_mutex_unlock (&mutex);
发送信号 :修改条件后发送信号,且需在互斥量保护内:
pthread_mutex_lock (&mutex);
pthread_cond_signal (&cond);
pthread_mutex_unlock (&mutex);
3.4 条件变量封装
#pragma once
#include "Lock.hpp"
#include <pthread.h>
namespace CondModule {
using namespace LockModule;
class Cond {
public :
Cond () { pthread_cond_init (&_cond, NULL ); }
~Cond () { pthread_cond_destroy (&_cond); }
void Wait (Mutex& mutex) {
pthread_cond_wait (&_cond, mutex.GetRawMutex ());
}
void Signal () { pthread_cond_signal (&_cond); }
void Broadcast () { pthread_cond_broadcast (&_cond); }
private :
Cond (const Cond&) = delete ;
Cond& operator =(const Cond&) = delete ;
pthread_cond_t _cond;
};
}
#include "Cond.hpp"
#include <stdio.h>
#include <unistd.h>
using namespace LockModule;
using namespace CondModule;
int ticket = 100 ;
Mutex mutex;
Cond cond;
int current_thread_id = 1 ;
void * sell_ticket (void * arg) {
int my_id = *(int *)arg;
char id_str[20 ];
sprintf (id_str, "thread %d" , my_id);
while (1 ) {
{
LockGuard lock (mutex) ;
while (current_thread_id != my_id && ticket > 0 ) {
cond.Wait (mutex);
}
if (ticket <= 0 ) {
current_thread_id = (current_thread_id % 4 ) + 1 ;
cond.Broadcast ();
break ;
}
int current_ticket = ticket;
ticket--;
current_thread_id = (current_thread_id % 4 ) + 1 ;
printf ("%s sells ticket:%d\n" , id_str, current_ticket);
cond.Broadcast ();
}
usleep (1000 );
}
return NULL ;
}
int main () {
pthread_t t1, t2, t3, t4;
int thread_ids[4 ] = {1 , 2 , 3 , 4 };
pthread_create (&t1, NULL , sell_ticket, (void *)&thread_ids[0 ]);
pthread_create (&t2, NULL , sell_ticket, (void *)&thread_ids[1 ]);
pthread_create (&t3, NULL , sell_ticket, (void *)&thread_ids[2 ]);
pthread_create (&t4, NULL , sell_ticket, (void *)&thread_ids[3 ]);
pthread_join (t1, NULL );
pthread_join (t2, NULL );
pthread_join (t3, NULL );
pthread_join (t4, NULL );
return 0 ;
}
四、生产者消费者模型:同步互斥的经典实战 生产者消费者模型是同步互斥的典型应用,核心是'解耦生产者和消费者,平衡处理能力',遵循321 原则 :
3 种关系:生产者与生产者(互斥)、消费者与消费者(互斥)、生产者与消费者(同步 + 互斥);
2 个角色:生产者(生产数据)、消费者(消费数据);
1 个容器:阻塞队列/环形队列(存储数据,解耦角色)。
4.1 模型核心价值
解耦 :生产者和消费者无需知道对方存在,仅通过容器交互;
支持并发 :多个生产者/消费者可同时工作;
平衡负载 :生产者生产快时,容器缓存数据;消费者消费快时,容器空则阻塞等待。
4.2 实现一:基于阻塞队列(动态大小)
队列为空时,消费者阻塞;
队列满时(可选限制),生产者阻塞;
基于 std::queue+ 互斥量 + 条件变量实现。
完整代码(BlockQueue.hpp) #pragma once
#include <queue>
#include "Lock.hpp"
#include "Cond.hpp"
namespace Model {
using namespace LockModule;
using namespace CondModule;
template <typename T>
class BlockQueue {
public :
BlockQueue (int cap = 10 ) : _cap(cap), _stop(false ) {}
~BlockQueue () {
Stop ();
}
void Stop () {
LockGuard lock (_mutex) ;
_stop = true ;
_prod_cond.Broadcast ();
_cons_cond.Broadcast ();
}
bool Enqueue (const T& data) {
LockGuard lock (_mutex) ;
while (_queue.size () >= _cap && !_stop) {
_prod_cond.Wait (_mutex);
}
if (_stop) {
return false ;
}
_queue.push (data);
_cons_cond.Signal ();
return true ;
}
bool Dequeue (T& data) {
LockGuard lock (_mutex) ;
while (_queue.empty () && !_stop) {
_cons_cond.Wait (_mutex);
}
if (_stop && _queue.empty ()) {
return false ;
}
data = _queue.front ();
_queue.pop ();
_prod_cond.Signal ();
return true ;
}
bool IsStopped () const {
LockGuard lock (_mutex) ;
return _stop;
}
size_t Size () {
LockGuard lock (_mutex) ;
return _queue.size ();
}
private :
std::queue<T> _queue;
int _cap;
Mutex _mutex;
Cond _prod_cond;
Cond _cons_cond;
bool _stop;
};
}
测试代码 #include "BlockQueue.hpp"
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
using namespace Model;
using namespace std;
BlockQueue<string> g_task_queue (3 ) ;
void * ProducerThread (void * arg) {
const char * producer_name = static_cast <const char *>(arg);
cout << "[" << producer_name << "] 启动,开始生产任务..." << endl;
for (int i = 0 ; i < 10 ; ++i) {
string task = "任务 -" + to_string (i);
bool ret = g_task_queue.Enqueue (task);
if (!ret) {
cout << "[" << producer_name << "] 队列已停止,停止生产" << endl;
break ;
}
cout << "[" << producer_name << "] 生产任务:" << task << " | 队列当前大小:" << g_task_queue.Size () << endl;
sleep (1 );
}
cout << "[" << producer_name << "] 生产完毕,退出线程" << endl;
return nullptr ;
}
void * ConsumerThread (void * arg) {
const char * consumer_name = static_cast <const char *>(arg);
cout << "[" << consumer_name << "] 启动,开始消费任务..." << endl;
while (true ) {
string task;
bool ret = g_task_queue.Dequeue (task);
if (!ret) {
cout << "[" << consumer_name << "] 队列已停止且无任务,退出消费" << endl;
break ;
}
cout << "[" << consumer_name << "] 消费任务:" << task << " | 队列当前大小:" << g_task_queue.Size () << endl;
sleep (2 );
}
cout << "[" << consumer_name << "] 消费完毕,退出线程" << endl;
return nullptr ;
}
int main () {
pthread_t producer, consumer;
const char * prod_name = "生产者" ;
const char * cons_name = "消费者" ;
pthread_create (&producer, nullptr , ProducerThread, (void *)prod_name);
pthread_create (&consumer, nullptr , ConsumerThread, (void *)cons_name);
cout << "主线程:等待 20 秒后停止队列..." << endl;
sleep (20 );
cout << "主线程:停止队列" << endl;
g_task_queue.Stop ();
pthread_join (producer, nullptr );
pthread_join (consumer, nullptr );
cout << "主线程:所有线程退出,程序结束" << endl;
return 0 ;
}
4.3 实现二:基于环形队列(固定大小) 环形队列是固定大小的数组,用模运算实现'环形',特点:
无需动态扩容,性能更优;
用信号量实现同步,互斥量实现生产者/消费者内部互斥;
适合对性能要求高、数据量可预估的场景。
核心思路
信号量 _empty:表示空闲空间数,初始为队列容量;
信号量 _full:表示已用空间数,初始为 0;
生产者:P(_empty)→生产→V(_full);
消费者:P(_full)→消费→V(_empty);
互斥量 _prod_mutex/_cons_mutex:保证多个生产者/消费者的互斥访问。
完整代码(RingQueue.hpp) #pragma once
#include <vector>
#include <pthread.h>
#include <semaphore.h>
namespace Model {
template <typename T>
class RingQueue {
public :
RingQueue (int cap = 10 ) : _cap(cap), _prod_idx(0 ), _cons_idx(0 ) {
_queue.resize (_cap);
sem_init (&_empty, 0 , _cap);
sem_init (&_full, 0 , 0 );
pthread_mutex_init (&_prod_mutex, NULL );
pthread_mutex_init (&_cons_mutex, NULL );
}
~RingQueue () {
sem_destroy (&_empty);
sem_destroy (&_full);
pthread_mutex_destroy (&_prod_mutex);
pthread_mutex_destroy (&_cons_mutex);
}
void Enqueue (const T& data) {
sem_wait (&_empty);
pthread_mutex_lock (&_prod_mutex);
_queue[_prod_idx] = data;
_prod_idx = (_prod_idx + 1 ) % _cap;
pthread_mutex_unlock (&_prod_mutex);
sem_post (&_full);
}
bool Dequeue (T& data) {
sem_wait (&_full);
pthread_mutex_lock (&_cons_mutex);
data = _queue[_cons_idx];
_cons_idx = (_cons_idx + 1 ) % _cap;
pthread_mutex_unlock (&_cons_mutex);
sem_post (&_empty);
return true ;
}
private :
std::vector<T> _queue;
int _cap;
int _prod_idx;
int _cons_idx;
sem_t _empty;
sem_t _full;
pthread_mutex_t _prod_mutex;
pthread_mutex_t _cons_mutex;
};
}
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <ctime>
#include "RingQueue.hpp"
#include "Lock.hpp"
#include "Cond.hpp"
using namespace std;
using namespace Model;
const int TEST_DATA_COUNT = 20 ;
void * ProducerThread (void * arg) {
RingQueue<int >* rq = static_cast <RingQueue<int >*>(arg);
srand (time (nullptr ));
for (int i = 0 ; i < TEST_DATA_COUNT; ++i) {
int data = rand () % 100 + 1 ;
rq->Enqueue (data);
cout << "[生产者] 写入数据:" << data << endl;
usleep (rand () % 200000 + 100000 );
}
cout << "[生产者] 数据写入完成,退出线程" << endl;
pthread_exit (nullptr );
}
void * ConsumerThread (void * arg) {
RingQueue<int >* rq = static_cast <RingQueue<int >*>(arg);
int data = 0 ;
for (int i = 0 ; i < TEST_DATA_COUNT; ++i) {
bool ret = rq->Dequeue (data);
if (ret) {
cout << "[消费者] 读取数据:" << data << endl;
} else {
cout << "[消费者] 出队失败!" << endl;
}
usleep (rand () % 250000 + 150000 );
}
cout << "[消费者] 数据读取完成,退出线程" << endl;
pthread_exit (nullptr );
}
int main () {
RingQueue<int > ring_queue (5 ) ;
pthread_t prod_tid, cons_tid;
int ret = 0 ;
ret = pthread_create (&prod_tid, nullptr , ProducerThread, &ring_queue);
if (ret != 0 ) {
cerr << "创建生产者线程失败!错误码:" << ret << endl;
return -1 ;
}
ret = pthread_create (&cons_tid, nullptr , ConsumerThread, &ring_queue);
if (ret != 0 ) {
cerr << "创建消费者线程失败!错误码:" << ret << endl;
return -1 ;
}
ret = pthread_join (prod_tid, nullptr );
if (ret != 0 ) {
cerr << "等待生产者线程失败!错误码:" << ret << endl;
return -1 ;
}
ret = pthread_join (cons_tid, nullptr );
if (ret != 0 ) {
cerr << "等待消费者线程失败!错误码:" << ret << endl;
return -1 ;
}
cout << "\n===== 单生产者单消费者测试完成 =====" << endl;
return 0 ;
}
4.4 两种模型对比与选型 特性 阻塞队列(动态) 环形队列(固定) 空间大小 动态扩容,无固定限制 固定大小,需提前预估 实现方式 互斥量 + 条件变量 互斥量 + 信号量 性能 扩容时有开销,中等 无扩容开销,高性能 适用场景 数据量不确定,追求灵活性 数据量稳定,追求高并发
五、线程安全与可重入问题 线程安全和可重入是多线程编程中容易混淆的概念,需明确区分:
5.1 线程安全:多线程访问的一致性保障
定义 :多个线程并发访问同一资源时,结果始终符合预期,无数据竞争;
线程安全的场景 :
仅访问局部变量(每个线程有独立栈空间);
访问共享资源但有互斥保护(如加锁);
仅读取共享资源(无写入操作);
线程不安全的场景 :
无保护地修改全局变量/静态变量;
函数状态随调用变化(如返回静态变量指针);
调用线程不安全的函数(如 strtok)。
5.2 可重入函数:重入场景下的正确性
定义 :同一函数被多个执行流(线程/信号处理函数)重入时,运行结果正确;
可重入的场景 :
不使用全局变量/静态变量;
不调用 malloc/free(依赖全局堆管理);
不调用标准 I/O 库函数(依赖全局缓冲区);
所有数据来自参数或局部变量;
不可重入的场景 :
使用全局变量/静态变量;
调用不可重入函数(如 malloc、printf);
返回静态变量指针(如 char* get_str() { static char buf[1024]; return buf; })。
5.3 核心区别与联系 维度 线程安全 可重入 关注对象 多线程并发访问的资源一致性 函数自身的重入安全性 依赖条件 可能依赖互斥锁 不依赖任何共享资源 关系 可重入函数一定是线程安全的 线程安全函数不一定是可重入的 示例 加锁的全局变量操作 仅使用局部变量的函数
六、死锁 死锁是多线程编程的常见问题,指多个线程互相持有对方需要的锁,且均不释放,导致永久阻塞。
6.1 死锁的四个必要条件
互斥条件 :资源只能被一个线程持有;
请求与保持条件 :线程持有部分资源,同时请求其他线程的资源;
不剥夺条件 :资源不能被强行剥夺,只能由持有者主动释放;
循环等待条件 :线程间形成'线程 A(持 lock1,等 lock2)→ 线程 B(持 lock2,等 lock3)→ 线程 C(持 lock3,等 lock1)→ 线程 A'的循环。
6.2 避免死锁的实战方法
方法 1:破坏循环等待条件(最常用)
给锁编号,所有线程按统一顺序加锁;
示例:线程 A 和 B 都需要锁 1 和锁 2,约定先加锁 1,再加锁 2:
pthread_mutex_lock (&lock1);
pthread_mutex_lock (&lock2);
pthread_mutex_unlock (&lock2);
pthread_mutex_unlock (&lock1);
方法 2:破坏请求与保持条件
一次性申请所有需要的资源,申请失败则释放已申请的资源;
示例:使用 pthread_mutex_trylock 尝试加锁,失败则回滚:
if (pthread_mutex_lock (&lock1)!=0 ){
return -1 ;
}
if (pthread_mutex_trylock (&lock2)!=0 ){
pthread_mutex_unlock (&lock1);
return -1 ;
}
方法 3:设置锁超时
使用 pthread_mutex_timedlock,超时则释放锁并重试;
避免线程永久阻塞。
方法 4:破坏不剥夺条件
允许线程在超时后释放已持有的锁(需配合业务逻辑设计)。
七、总结
互斥 :用互斥量(mutex)保护临界区,RAII 封装避免锁泄漏;
同步 :用条件变量(cond)实现线程有序协作,必须配合互斥量;
生产者消费者模型 :阻塞队列(灵活)和环形队列(高性能),解耦并发角色;
线程池 :预创建线程,避免线程频繁创建销毁,提升高并发性能;
避坑 :区分线程安全与可重入,避免死锁。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online