嵌入式linux同步原理详解与实现思路

考虑一个停车场管理系统,其中有一个信号量用于表示停车场的空位数量。当一辆车进入停车场时,它会尝试获取一个空位(即减少信号量的值)。如果信号量的值为零(表示没有空位),则车辆需要等待。当一辆车离开停车场时,它会释放一个空位(即增加信号量的值),从而允许等待的车辆进入停车场。通过这种方式,信号量用于协调车辆进入和离开停车场的操作。这就是同步在生活中的一个简单的应用,同样在linux下,也提供了相关的机制去实现资源的同步。

同步原理的详细介绍

互斥锁(Mutexes)

互斥锁是最简单的同步机制之一,用于确保在任何时候只有一个线程可以访问某个特定的资源或代码段。当一个线程需要访问受保护的资源时,它会尝试获取互斥锁。如果锁已经被其他线程持有,则该线程会被阻塞,直到锁被释放。这种机制可以防止多个线程同时修改共享资源,从而避免数据不一致的问题。

/*
在这个例子中,我们有两个线程:一个用于增加共享资源,另一个用于减少共享资源。共享资源是一个整数shared_resource,初始值为0。我们使用pthread_mutex_t类型的变量lock作为互斥锁来保护对shared_resource的访问。

每个线程在修改shared_resource之前都会尝试获取互斥锁。如果锁已经被另一个线程持有,则该线程会阻塞,直到锁被释放。一旦线程获取到锁,它就可以安全地修改共享资源,并在完成后释放锁,从而允许其他线程获取锁并访问共享资源。

主函数main初始化互斥锁,创建两个线程,并等待它们完成。最后,它销毁互斥锁并输出最终的共享资源值。由于每个线程都进行了相同数量的增加和减少操作,因此最终的共享资源值应该接近0(考虑到线程调度和同步操作可能引入的一些微小差异)。
*/
#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
  
// 共享资源  
int shared_resource = 0;  
  
// 互斥锁  
pthread_mutex_t lock;  
  
// 线程函数,用于增加共享资源  
void *increment_resource(void *arg) {  
    for (int i = 0; i < 100000; ++i) {  
        // 获取互斥锁  
        pthread_mutex_lock(&lock);  
          
        // 访问和修改共享资源  
        shared_resource++;  
          
        // 释放互斥锁  
        pthread_mutex_unlock(&lock);  
    }  
    return NULL;  
}  
  
// 线程函数,用于减少共享资源  
void *decrement_resource(void *arg) {  
    for (int i = 0; i < 100000; ++i) {  
        // 获取互斥锁  
        pthread_mutex_lock(&lock);  
          
        // 访问和修改共享资源  
        shared_resource--;  
          
        // 释放互斥锁  
        pthread_mutex_unlock(&lock);  
    }  
    return NULL;  
}  
  
int main() {  
    // 初始化互斥锁  
    if (pthread_mutex_init(&lock, NULL) != 0) {  
        printf("Mutex initialization failed\n");  
        return 1;  
    }  
      
    // 创建两个线程  
    pthread_t increment_thread, decrement_thread;  
    pthread_create(&increment_thread, NULL, increment_resource, NULL);  
    pthread_create(&decrement_thread, NULL, decrement_resource, NULL);  
      
    // 等待两个线程完成  
    pthread_join(increment_thread, NULL);  
    pthread_join(decrement_thread, NULL);  
      
    // 销毁互斥锁  
    pthread_mutex_destroy(&lock);  
      
    // 输出最终的共享资源值,它应该接近0(考虑到可能的调度和同步开销)  
    printf("Final value of shared resource: %d\n", shared_resource);  
      
    return 0;  
}

读写锁(Read-Write Locks)

读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。这对于读操作远多于写操作的场景特别有效,因为读操作不会互相阻塞。读写锁分为共享锁(读锁)和排他锁(写锁)。当一个线程持有写锁时,其他线程既不能读也不能写;当多个线程只是读取资源时,它们可以同时持有读锁。


/*
在这个例子中,我们有一个共享资源shared_data,初始值为0。我们使用pthread_rwlock_t类型的变量rwlock作为读写锁。

我们定义了两个线程函数:read_resource和write_resource。read_resource函数用于读取共享资源,而write_resource函数用于写入共享资源。

在read_resource函数中,线程首先获取读锁,然后读取共享资源,并输出其值。之后,它释放读锁以允许其他线程获取锁。

在write_resource函数中,线程获取写锁,修改共享资源,并输出新的值。由于写锁是独占的,因此在写操作期间,其他线程(无论是读线程还是写线程)都无法获取锁。

在main函数中,我们初始化读写锁,创建三个读线程和一个写线程,然后等待所有线程完成。最后,我们销毁读写锁。


*/
#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
  
// 共享资源  
int shared_data = 0;  
  
// 读写锁  
pthread_rwlock_t rwlock;  
  
// 线程函数,用于读取共享资源  
void *read_resource(void *arg) {  
    for (int i = 0; i < 5; ++i) {  
        // 获取读锁  
        pthread_rwlock_rdlock(&rwlock);  
          
        // 读取共享资源  
        printf("Thread %ld is reading shared_data: %d\n", (long)arg, shared_data);  
          
        // 释放读锁  
        pthread_rwlock_unlock(&rwlock);  
          
        // 模拟一些工作  
        usleep(1000);  
    }  
    return NULL;  
}  
  
// 线程函数,用于写入共享资源  
void *write_resource(void *arg) {  
    for (int i = 0; i < 5; ++i) {  
        // 获取写锁  
        pthread_rwlock_wrlock(&rwlock);  
          
        // 修改共享资源  
        shared_data += 10;  
        printf("Thread %ld is writing to shared_data: %d\n", (long)arg, shared_data);  
          
        // 释放写锁  
        pthread_rwlock_unlock(&rwlock);  
          
        // 模拟一些工作  
        usleep(1000);  
    }  
    return NULL;  
}  
  
int main() {  
    // 初始化读写锁  
    if (pthread_rwlock_init(&rwlock, NULL) != 0) {  
        printf("Read-write lock initialization failed\n");  
        return 1;  
    }  
      
    // 创建多个读线程和写线程  
    pthread_t read_threads[3];  
    pthread_t write_thread;  
    for (long i = 0; i < 3; ++i) {  
        pthread_create(&read_threads[i], NULL, read_resource, (void *)i);  
    }  
    pthread_create(&write_thread, NULL, write_resource, (void *)4);  
      
    // 等待所有线程完成  
    for (int i = 0; i < 3; ++i) {  
        pthread_join(read_threads[i], NULL);  
    }  
    pthread_join(write_thread, NULL);  
      
    // 销毁读写锁  
    pthread_rwlock_destroy(&rwlock);  
      
    return 0;  
}

信号量(Semaphores)

信号量是一个计数器,用于控制对共享资源的访问。多个线程可以通过信号量来协调对资源的访问。例如,一个线程可能需要等待其他线程完成某个任务后才能继续执行。信号量可以用于实现这种等待/通知机制。

/*
在这个例子中,我们有一个共享资源shared_counter,它是一个整数,初始值为0。我们还定义了一个信号量semaphore,用于控制对shared_counter的访问。

我们创建了两个线程函数:increment_counter和decrement_counter。这两个函数分别用于增加和减少shared_counter的值。在每个函数中,我们首先调用sem_wait函数等待信号量变为可用状态(即信号量的值大于0)。一旦信号量可用,线程就可以安全地访问和修改共享资源。修改完成后,线程调用sem_post函数释放信号量,使得其他等待的线程可以获取资源。

在main函数中,我们初始化信号量,其初始值为1,表示只有一个资源可用。然后,我们创建两个线程,一个用于增加共享资源,另一个用于减少共享资源。最后,我们使用pthread_join函数等待两个线程完成,并销毁信号量。


*/
#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
#include <semaphore.h>  
  
// 共享资源  
int shared_counter = 0;  
  
// 用于控制对共享资源访问的信号量  
sem_t semaphore;  
  
// 线程函数,增加共享资源  
void *increment_counter(void *arg) {  
    for (int i = 0; i < 5; ++i) {  
        // 等待信号量可用  
        sem_wait(&semaphore);  
          
        // 访问和修改共享资源  
        shared_counter++;  
        printf("Incremented shared_counter to %d\n", shared_counter);  
          
        // 释放信号量  
        sem_post(&semaphore);  
    }  
    return NULL;  
}  
  
// 线程函数,减少共享资源  
void *decrement_counter(void *arg) {  
    for (int i = 0; i < 5; ++i) {  
        // 等待信号量可用  
        sem_wait(&semaphore);  
          
        // 访问和修改共享资源  
        if (shared_counter > 0) {  
            shared_counter--;  
            printf("Decremented shared_counter to %d\n", shared_counter);  
        }  
          
        // 释放信号量  
        sem_post(&semaphore);  
    }  
    return NULL;  
}  
  
int main() {  
    // 初始化信号量,初始值为1,表示只有一个资源可用  
    if (sem_init(&semaphore, 0, 1) != 0) {  
        perror("Semaphore initialization failed");  
        return 1;  
    }  
      
    // 创建两个线程,一个用于增加共享资源,另一个用于减少共享资源  
    pthread_t increment_thread, decrement_thread;  
    pthread_create(&increment_thread, NULL, increment_counter, NULL);  
    pthread_create(&decrement_thread, NULL, decrement_counter, NULL);  
      
    // 等待两个线程完成  
    pthread_join(increment_thread, NULL);  
    pthread_join(decrement_thread, NULL);  
      
    // 销毁信号量  
    sem_destroy(&semaphore);  
      
    return 0;  
}

条件变量(Condition Variables)

条件变量通常与互斥锁一起使用,允许线程在满足特定条件之前等待。线程首先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程会解锁互斥锁并等待条件变量。当其他线程改变了条件并通知等待的线程时,等待的线程会重新获取互斥锁并检查条件。这种机制允许线程在特定条件满足时继续执行。

/*
这个例子中,我们有一个共享资源shared_data,初始值为0。我们还有两个线程函数:wait_for_data和provide_data。wait_for_data函数中的线程会等待shared_data不为0的条件成立,而provide_data函数中的线程会设置shared_data的值并通知等待的线程。

在main函数中,我们创建了一个生产者线程producer和两个消费者线程consumer1和consumer2。生产者线程在休眠一秒后设置shared_data的值为42,并通过cv.notify_all()唤醒所有等待的线程。消费者线程在cv.wait()调用处阻塞,直到条件变量被通知并且条件(shared_data != 0)成立。

当运行这个程序时,你应该会看到生产者线程先输出"Data is ready: 42",然后两个消费者线程几乎同时输出它们得到了数据,并显示数据的值。这展示了条件变量如何允许线程同步访问和修改共享资源。
*/
#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
  
// 共享资源  
int shared_data = 0;  
  
// 互斥锁和条件变量  
std::mutex mtx;  
std::condition_variable cv;  
  
// 线程函数,等待条件成立(shared_data不为0)  
void wait_for_data(int id) {  
    std::unique_lock<std::mutex> lock(mtx);  
    std::cout << "Thread " << id << " waiting for data...\n";  
    cv.wait(lock, [] { return shared_data != 0; }); // 等待条件成立  
    std::cout << "Thread " << id << " got the data: " << shared_data << "\n";  
}  
  
// 线程函数,设置条件并通知等待的线程  
void provide_data(int value) {  
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作  
    std::cout << "Data is ready: " << value << "\n";  
  
    std::lock_guard<std::mutex> lock(mtx);  
    shared_data = value; // 设置共享资源  
    cv.notify_all(); // 通知所有等待的线程  
}  
  
int main() {  
    std::thread producer(provide_data, 42); // 生产者线程  
    std::thread consumer1(wait_for_data, 1); // 消费者线程1  
    std::thread consumer2(wait_for_data, 2); // 消费者线程2  
  
    producer.join();  
    consumer1.join();  
    consumer2.join();  
  
    return 0;  
}

屏障(Barriers)

屏障用于同步一组线程,确保所有线程都到达某个点后再一起继续执行。这常用于并行算法中的同步点,例如,在所有线程完成数据的局部处理后,再进行全局的数据合并。

/*
在这个例子中,我们定义了一个barrier_example函数,该函数将由每个线程执行。每个线程在到达屏障之前会打印一条消息,然后调用pthread_barrier_wait函数来等待其他线程。当所有线程都调用pthread_barrier_wait并到达屏障时,它们会同时被释放,并继续执行后面的代码。

main函数中创建了一个屏障,并初始化了计数器为线程数量。然后,它创建了指定数量的线程,每个线程都执行barrier_example函数。主线程使用pthread_join等待所有线程完成,然后销毁屏障并退出。
*/
#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
  
#define NUM_THREADS 5  
  
pthread_barrier_t barrier;  
  
void *barrier_example(void *threadid) {  
    long tid;  
    tid = (long)threadid;  
  
    printf("Thread #%ld is ready before the barrier\n", tid);  
  
    // 等待所有线程到达屏障点  
    pthread_barrier_wait(&barrier);  
  
    printf("Thread #%ld is past the barrier\n", tid);  
    pthread_exit(NULL);  
}  
  
int main() {  
    pthread_t threads[NUM_THREADS];  
    int rc;  
    long t;  
  
    // 初始化屏障,设置屏障的计数器为线程数量  
    pthread_barrier_init(&barrier, NULL, NUM_THREADS);  
  
    for (t = 0; t < NUM_THREADS; t++) {  
        printf("Creating thread %ld\n", t);  
        rc = pthread_create(&threads[t], NULL, barrier_example, (void *)t);  
        if (rc) {  
            printf("ERROR; return code from pthread_create() is %d\n", rc);  
            exit(-1);  
        }  
    }  
  
    // 等待所有线程完成  
    for (t = 0; t < NUM_THREADS; t++) {  
        pthread_join(threads[t], NULL);  
    }  
  
    // 销毁屏障  
    pthread_barrier_destroy(&barrier);  
  
    pthread_exit(NULL);  
}

编译并运行此程序将展示所有线程几乎同时通过了屏障点,输出类似于:

Creating thread 0  
Thread #0 is ready before the barrier  
Creating thread 1  
Thread #1 is ready before the barrier  
Creating thread 2  
Thread #2 is ready before the barrier  
Creating thread 3  
Thread #3 is ready before the barrier  
Creating thread 4  
Thread #4 is ready before the barrier  
Thread #0 is past the barrier  
Thread #1 is past the barrier  
Thread #2 is past the barrier  
Thread #3 is past the barrier  
Thread #4 is past the barrier

原子操作(Atomic Operations)

原子操作是不可中断的操作,即它们要么完全执行,要么完全不执行。Linux提供了一系列原子操作函数,用于确保对共享资源的访问是原子的。这意味着在执行原子操作期间,不会被其他线程打断,从而保证了数据的一致性。


/*
在这个例子中,我们定义了一个全局的原子整数counter,它初始化为0。我们还定义了一个increment_counter函数,该函数将作为多个线程的入口点,并接受一个参数increment_by,表示每个线程应该增加计数器的次数。

在main函数中,我们创建了num_threads个线程,并将它们添加到threads向量中。每个线程都会调用increment_counter函数,并传入increment_per_thread作为参数,这意味着每个线程将增加计数器increment_per_thread次。

由于counter.fetch_add是一个原子操作,因此即使多个线程同时尝试增加计数器,也不会出现数据竞争的情况。每个线程对计数器的增加都是原子的,并且不会被其他线程的操作所干扰。

最后,主线程等待所有子线程完成,并输出最终的计数器值。由于所有增加操作都是原子的,最终的计数器值将是每个线程增加次数的总和。

*/
#include <iostream>  
#include <thread>  
#include <atomic>  
#include <vector>  
  
// 使用原子整数  
std::atomic<int> counter(0);  
  
// 线程函数,增加原子计数器的值  
void increment_counter(int increment_by) {  
    for (int i = 0; i < increment_by; ++i) {  
        // 原子增加操作  
        counter.fetch_add(1, std::memory_order_relaxed);  
    }  
}  
  
int main() {  
    const int num_threads = 5;  
    const int increment_per_thread = 1000;  
    std::vector<std::thread> threads;  
  
    // 创建多个线程,每个线程都会增加计数器  
    for (int i = 0; i < num_threads; ++i) {  
        threads.emplace_back(increment_counter, increment_per_thread);  
    }  
  
    // 等待所有线程完成  
    for (auto& thread : threads) {  
        thread.join();  
    }  
  
    // 输出最终的计数器值  
    std::cout << "Final counter value: " << counter << std::endl;  
  
    return 0;  
}

消息队列(Message Queues)

虽然主要用于进程间通信(IPC),但消息队列也可以用于同步。进程可以通过发送和接收特定类型的消息来协调它们的行动。例如,一个进程可能需要等待另一个进程完成某个任务后才能继续执行。这可以通过消息队列来实现:一个进程发送一个消息表示任务已完成,另一个进程等待并接收这个消息后继续执行。

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <mqueue.h>  
#include <pthread.h>  
#include <string.h>  
#include <unistd.h>  
  
#define QUEUE_NAME "/my_message_queue"  
#define MAX_MSG_SIZE 1024  
#define MSG_PRIORITY 0  
  
// 消息队列属性  
struct mq_attr attr;  
attr.mq_flags = 0;  
attr.mq_maxmsg = 10;  
attr.mq_msgsize = MAX_MSG_SIZE;  
attr.mq_curmsgs = 0;  
  
// 消息内容  
char msg_text[] = "Hello, Message Queue!";  
  
// 发送消息的函数  
void* sender(void* arg) {  
    mqd_t mqdes;  
    struct mq_attr attr;  
  
    // 打开或创建消息队列  
    mqdes = mq_open(QUEUE_NAME, O_WRONLY);  
    if (mqdes == (mqd_t)-1) {  
        perror("mq_open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 发送消息  
    if (mq_send(mqdes, msg_text, strlen(msg_text), MSG_PRIORITY) == -1) {  
        perror("mq_send");  
        exit(EXIT_FAILURE);  
    }  
  
    printf("Message sent to queue %s\n", QUEUE_NAME);  
  
    // 关闭消息队列  
    if (mq_close(mqdes) == -1) {  
        perror("mq_close");  
        exit(EXIT_FAILURE);  
    }  
  
    return NULL;  
}  
  
// 接收消息的函数  
void* receiver(void* arg) {  
    mqd_t mqdes;  
    char buffer[MAX_MSG_SIZE];  
    ssize_t bytes_read;  
  
    // 打开消息队列  
    mqdes = mq_open(QUEUE_NAME, O_RDONLY);  
    if (mqdes == (mqd_t)-1) {  
        perror("mq_open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 接收消息  
    bytes_read = mq_receive(mqdes, buffer, MAX_MSG_SIZE, NULL);  
    if (bytes_read == -1) {  
        perror("mq_receive");  
        exit(EXIT_FAILURE);  
    }  
  
    printf("Message received from queue %s: %s\n", QUEUE_NAME, buffer);  
  
    // 关闭消息队列  
    if (mq_close(mqdes) == -1) {  
        perror("mq_close");  
        exit(EXIT_FAILURE);  
    }  
  
    return NULL;  
}  
  
int main() {  
    pthread_t sender_thread, receiver_thread;  
  
    // 创建消息队列  
    if (mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0644, &attr) == (mqd_t)-1) {  
        perror("mq_open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 创建并启动发送线程  
    if (pthread_create(&sender_thread, NULL, sender, NULL)) {  
        perror("pthread_create sender");  
        exit(EXIT_FAILURE);  
    }  
  
    // 创建并启动接收线程  
    if (pthread_create(&receiver_thread, NULL, receiver, NULL)) {  
        perror("pthread_create receiver");  
        exit(EXIT_FAILURE);  
    }  
  
    // 等待线程完成  
    pthread_join(sender_thread, NULL);  
    pthread_join(receiver_thread, NULL);  
  
    // 删除消息队列  
    if (mq_unlink(QUEUE_NAME) == -1) {  
        perror("mq_unlink");  
        exit(EXIT_FAILURE);  
    }  
  
    return 0;  
}

文件锁(File Locks)

文件锁用于控制对文件的并发访问。当一个进程需要对文件进行写操作时,它可以获取一个文件锁,从而阻止其他进程同时对该文件进行写操作(或读/写操作,取决于锁的类型)。这有助于确保文件的一致性和完整性。


/*
文件锁是一种同步机制,可以用来防止多个进程同时访问同一文件,从而避免数据不一致或损坏。在C/C++中,可以使用fcntl或lockf等系统调用来实现文件锁。

以下是一个使用fcntl实现文件锁的简单C语言例子。这个例子创建了两个进程,一个用于写入文件,另一个用于读取文件。写入进程首先获取一个写锁,然后写入数据。读取进程则等待写锁释放后,获取一个读锁来读取数据。
*/
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
  
#define LOCK_FILE "test.lock"  
  
int main(int argc, char *argv[]) {  
    int fd;  
    struct flock fl;  
  
    if (argc != 2) {  
        fprintf(stderr, "Usage: %s (read|write)\n", argv[0]);  
        exit(EXIT_FAILURE);  
    }  
  
    fd = open(LOCK_FILE, O_RDWR | O_CREAT, 0666);  
    if (fd == -1) {  
        perror("open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 如果是写入进程,先获取写锁  
    if (strcmp(argv[1], "write") == 0) {  
        fl.l_type = F_WRLCK;  
        fl.l_whence = SEEK_SET;  
        fl.l_start = 0;  
        fl.l_len = 0; // 锁整个文件  
  
        if (fcntl(fd, F_SETLKW, &fl) == -1) { // F_SETLKW会阻塞直到获取到锁  
            perror("fcntl F_SETLKW");  
            exit(EXIT_FAILURE);  
        }  
  
        // 写入数据  
        const char *data = "Hello, world!";  
        if (write(fd, data, strlen(data)) != strlen(data)) {  
            perror("write");  
            exit(EXIT_FAILURE);  
        }  
  
        // 释放锁  
        fl.l_type = F_UNLCK;  
        if (fcntl(fd, F_SETLK, &fl) == -1) {  
            perror("fcntl F_SETLK");  
            exit(EXIT_FAILURE);  
        }  
    }  
    // 如果是读取进程,等待写锁释放后获取读锁  
    else if (strcmp(argv[1], "read") == 0) {  
        fl.l_type = F_RDLCK;  
        fl.l_whence = SEEK_SET;  
        fl.l_start = 0;  
        fl.l_len = 0; // 锁整个文件  
  
        if (fcntl(fd, F_SETLKW, &fl) == -1) { // F_SETLKW会阻塞直到获取到锁  
            perror("fcntl F_SETLKW");  
            exit(EXIT_FAILURE);  
        }  
  
        // 读取数据  
        char buffer[1024];  
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);  
        if (n < 0) {  
            perror("read");  
            exit(EXIT_FAILURE);  
        }  
        buffer[n] = '\0';  
        printf("Read data: %s\n", buffer);  
  
        // 释放锁  
        fl.l_type = F_UNLCK;  
        if (fcntl(fd, F_SETLK, &fl) == -1) {  
            perror("fcntl F_SETLK");  
            exit(EXIT_FAILURE);  
        }  
    } else {  
        fprintf(stderr, "Invalid operation: %s\n", argv[1]);  
        exit(EXIT_FAILURE);  
    }  
  
    close(fd);  
    return 0;  
}
  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

稚肩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值