【Linux】:多线程(读写锁 自旋锁)

news2025/1/5 22:25:25

✨                                                 倘若南方知我意,莫将晚霞落黄昏      🌏 

 📃个人主页:island1314

🔥个人专栏:Linux—登神长阶

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


目录

1. 读写锁 🔏

1.1 基本概念

1.2 读写锁的工作原理及特点

1.3 读写锁的实现

🥝 读写锁相关函数

🥝 案例如下:

1.4 读写锁优缺点及应用场景

🍉 优点:

🍉 缺点:

🍉 读写锁应用场景:

1.5 性能开销:读写锁 VS 互斥锁

2. 自旋锁 🔐

2.1 基本概念

2.2 自旋锁的原理

2.3 自旋锁实现

2.4 自旋锁优缺点及应用场景

🥑 优点:

🥑 缺点:

🥑使用场景:

2.5 自旋锁 VS 互斥锁


1. 读写锁 🔏

1.1 基本概念

🔥 读写锁(Read-Write Lock)是一种用于多线程环境下同步访问共享资源的锁。它与传统的互斥锁(Mutex)有所不同,提供了更细粒度的控制,以便提高并发性能。它允许多个线程同时 读取 数据,但在写入数据时,必须确保只有一个线程可以进行写操作,并且在写操作期间,所有的读操作都必须等待。

💧 读写锁的核心思想是:读操作之间是可以并发执行的,而写操作是独占的,即不能与其他读操作或者写操作同时执行

1.2 读写锁的工作原理及特点

  • 即读锁允许多个线程同时获得,因为读操作本身是线程安全的;而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的

🐇 读写锁的特点:

  • 读读不互斥:多个读线程可以同时访问共享资源
  • 读写互斥:读操作和写操作互斥,即在写操作进行时,其他线程不能进行读或写操作
  • 写写互斥:多个写线程不能同时进行写操作

🐇 具体来说,读写锁的行为如下:

  • 读操作(共享锁)

    • 如果没有线程正在持有写锁,那么多个线程可以同时获得读锁并执行读取操作。
    • 读操作不会阻塞其他读操作,但会阻塞写操作。
  • 写操作(独占锁)

    • 写操作会阻塞所有其他的读操作和写操作。换句话说,在某个线程持有写锁期间,其他线程既无法获得读锁也无法获得写锁。
    • 写操作是独占的,确保在操作过程中共享数据的一致性

1.3 读写锁的实现

🥝 读写锁相关函数

在书写具体代码之前,我们先来了解一下其相关函数

① 初始化锁

原型:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

参数:
 rwlock: 指向 pthread_rwlock_t 类型的读写锁对象的指针。
 attr: 一个指向 pthread_rwlockattr_t 类型的指针,可以设置锁的属性。
     如果不需要特定的属性,通常可以将其设置为 NULL。

返回值:
返回 0 表示成功,返回错误码(如 EINVAL)表示初始化失败。

范例:
#include <pthread.h>
#include <stdio.h>

pthread_rwlock_t rwlock;

int main() {
    // 初始化读写锁
    if (pthread_rwlock_init(&rwlock, NULL) != 0) {
        perror("pthread_rwlock_init");
        return 1;
    }

    // 后续可以使用 rwlock 执行读写操作

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

🎈 pthread_rwlock_init 用于初始化一个读写锁。该函数会创建一个 pthread_rwlock_t 类型的读写锁变量,使其处于初始化状态,供后续的线程操作使用

② 获取锁

a. 读锁

原型:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

参数:
rwlock: 指向要获取读锁的 pthread_rwlock_t 类型的读写锁对象。
返回值:

返回 0 表示成功,返回错误码表示失败(例如 EBUSY 表示写锁被持有,当前线程无法获得读锁)

范例:
#include <pthread.h>
#include <stdio.h>

pthread_rwlock_t rwlock;

void* read_data(void* arg) {
    pthread_rwlock_rdlock(&rwlock);  // 获取读锁
    printf("Reading data...\n");
    pthread_rwlock_unlock(&rwlock);  // 释放读锁
    return NULL;
}

int main() {
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t threads[3];
    for (int i = 0; i < 3; i++) {
        pthread_create(&threads[i], NULL, read_data, NULL);
    }

    for (int i = 0; i < 3; i++) {
        pthread_join(threads[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);  // 销毁读写锁
    return 0;
}
// 在上述代码中,多个线程可以同时获得读锁并执行读取操作,而无需相互阻塞。

🎈 pthread_rwlock_rdlock 用于获取读锁,即共享锁。多个线程可以同时持有读锁进行读取操作,但在任何时刻,写锁无法被获得,直到所有的读锁都被释放。

b. 写锁

原型:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

参数:
rwlock: 指向要获取写锁的 pthread_rwlock_t 类型的读写锁对象。

返回值:
返回 0 表示成功,返回错误码表示失败如: EBUSY 表示有其他线程持有读锁或写锁,当前线程无法获得写锁

范例:
#include <pthread.h>
#include <stdio.h>

pthread_rwlock_t rwlock;

void* write_data(void* arg) {
    pthread_rwlock_wrlock(&rwlock);  // 获取写锁
    printf("Writing data...\n");
    pthread_rwlock_unlock(&rwlock);  // 释放写锁
    return NULL;
}

int main() {
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t threads[2];
    for (int i = 0; i < 2; i++) {
        pthread_create(&threads[i], NULL, write_data, NULL);
    }

    for (int i = 0; i < 2; i++) {
        pthread_join(threads[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);  // 销毁读写锁
    return 0;
}

// 在上面的示例中,write_data 函数每次都获取写锁,并在完成写操作后释放写锁。
// 在写操作期间,不允许其他线程获取读锁或写锁

🎈 pthread_rwlock_wrlock 用于获取写锁。写锁是独占的,即任何一个线程持有写锁时,其他线程不能获得读锁或写锁。只有当所有线程都释放了读锁,写锁才能被获取。

③ 释放锁

原型:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

参数:
rwlock: 指向需要释放锁的 pthread_rwlock_t 类型的读写锁对象。

返回值:
返回 0 表示成功,返回错误码表示失败(例如 EINVAL 表示锁没有被当前线程持有)

🎈 每当一个线程完成对共享资源的读或写操作时,它需要释放相应的锁。pthread_rwlock_unlock 用于释放由 pthread_rwlock_rdlock 或 pthread_rwlock_wrlock 获得的锁。

🥝 案例如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cstdlib>
#include <ctime>

// 共享资源
int shared_data = 0;

// 读者锁
pthread_rwlock_t rwlock;

// 读者线程函数
void *Reader(void *arg)
{
    // slee(1); // 读者优先, 一旦读者进入 && 读者很多,写者就进不去了
    int number = *(int*)arg;
    while(true)
    {
        pthread_rwlock_rdlock(&rwlock);     // 读者加锁
        std::cout << "读者- " << number << " 正在读取数据, 数据是: " << shared_data << std::endl;
        sleep(1);                           // 模拟读取操作
        pthread_rwlock_unlock(&rwlock);     // 解锁
    }
    delete (int*)arg;
    return NULL;
}

// 写者线程函数
void *Writer(void *arg)
{
    int number = *(int*)arg;
    while(true)
    {
        pthread_rwlock_wrlock(&rwlock); // 写者加锁
        shared_data = rand() % 100;     // 修改共享数据
        std::cout << "写者- " << number << " 正在写入, 新的数据是: " << shared_data << std::endl;
        sleep(2);                       // 模拟写入操作
        pthread_rwlock_unlock(&rwlock); // 解锁
    }
    delete (int*)arg;
    return NULL;
}


int main()
{
    srand(time(nullptr)^getpid());
    pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁

    // 可以提高读写数量配比,观察现象
    const int reader_num = 2;
    const int writer_num = 2;
    const int total = reader_num + writer_num;
    pthread_t threads[total]; // 假设读者和写者数量相等

    // 创建读者线程
    for(int i = 0; i < reader_num; ++i)
    {
        int *id = new int(i);
        pthread_create(&threads[i], NULL, Reader, id);
    }

    // 创建写者线程
    for(int i = reader_num; i < total; ++i)
    {
        int *id = new int(i - reader_num);
        pthread_create(&threads[i], NULL, Writer, id);
    }

    // 等待所有线程完成
    for(int i = 0; i < total; ++i)
    {
        pthread_join(threads[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);    // 销毁读写锁

    return 0;
}

输出结果如下:

分析:

  • 上面我们创建了 两个读者、两个写者,读者线程每次获取读锁并且读取数据后休眠一秒,而写者线程每次获取写锁后休眠 2 s,这种不对称休眠可能会使写者长时间处于等待
  • 写锁,它是独占的,意味着任何时候只能有 一个线程 获得写锁。即使你创建了两个写者线程,只有一个线程能在某一时刻获得写锁,另一个线程必须等待,直到当前持有写锁的线程释放它。
  • 因此大部分情况,我们都只看到读锁读取数据,少部分情况出现写锁写入

那么我们做出一些修改,比如让读锁在加锁前就休眠 1 s

则出现大部分情况都是写者正在写入,读者很难读取到

同样地,我们也可以修改读者、写者数量来改变输出情况,大家也可以自己试试,我就不多尝试了

1.4 读写锁优缺点及应用场景

🍉 优点:
  • 并发性:读操作的并发执行大大提高了系统的吞吐量,尤其是当读操作远多于写操作时,读写锁能够显著提高性能。
  • 资源共享:多个线程可以在不干扰彼此的情况下同时读取共享资源,提高了资源利用率。
🍉 缺点:

虽然读写锁提高了并发性能,但它也有一些潜在的缺点:

  1. 写操作的饥饿问题:如果系统中有大量的读操作而写操作很少发生,可能会导致写线程长期得不到执行。这种现象被称为写操作的 饥饿(starvation)。一些实现会通过公平策略来解决这个问题。

  2. 实现复杂性:相比普通的互斥锁,读写锁的实现更复杂,需要管理多个线程的访问请求,可能会导致死锁或者性能下降,尤其是在高并发环境下。

  3. 性能瓶颈:在某些场景下,读写锁的性能提升可能并不显著,特别是在写操作占主导地位时,锁的争用可能导致性能瓶颈

🍉 读写锁应用场景:
  • 读操作频繁,写操作较少:如果你的应用中读操作远多于写操作,使用读写锁可以显著提高性能,因为多个线程可以同时执行读操作,而写操作则需要独占锁,避免了不必要的阻塞。示例:数据库缓存、Web 服务器的请求处理等。
  • 不需要严格顺序的读操作:读操作之间不需要顺序或依赖关系时,多个线程可以并发执行读操作,提高并发性。

1.5 性能开销:读写锁 VS 互斥锁

  • 读写锁的性能开销与普通互斥锁相比,通常情况下读写锁的单次加锁开销大于互斥锁。
    • 这是因为读写锁需要进行额外的引用计数和加锁读写性质判别,这些操作增加了其复杂性和性能开销。
  • 在设计上,读写锁比互斥锁更复杂,其内部加锁和解锁的逻辑也更为复杂,需要更新读者和写者的数量,而互斥锁则无需这样的操作。
  • 然而,读写锁的性能优势在于其在读多写少的场景下表现更佳。
    • 当读操作远多于写操作时,读写锁可以允许多个读线程同时访问共享资源,从而提高并发性能。
    • 相反,在写操作频繁的情况下,读写锁的性能会比互斥锁差,因为写操作必须互斥进行,读写锁需要处理额外的逻辑来避免写锁“饥饿”

1.6 优先策略

🦋 读者优先(Reader-Preference)

  • 在这种策略中, 系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据) , 而不会优先考虑写者。 这意味着当有读者正在读取时, 新到达的读者会立即被允许进入读取区, 而写者则会被阻塞, 直到所有读者都离开读取区。 读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限) , 特别是当读者频繁到达时。

🦋 写者优先(Writer-Preference)

  • 在这种策略中, 系统会优先考虑写者。 当写者请求写入权限时, 系统会尽快地让写者进入写入区, 即使此时有读者正在读取。 这通常意味着一旦有写者到达, 所有后续的读者都会被阻塞, 直到写者完成写入并离开写入区。 写者优先策略可以减少写者等待的时间, 但可能会导致读者饥饿(即读者长时间无法获得读取权限) , 特别是当写者频繁到达时。

2. 自旋锁 🔐

2.1 基本概念

🔥 自旋锁(Spinlock)是一种简单的同步机制,用于在多线程或多核系统中防止并发访问共享资源。在获取锁时,如果锁被其他线程占用,线程并不会进入休眠状态,而是不断地重复检查锁是否可用,这个过程就被称为“自旋”

2.2 自旋锁的原理

🐳 自旋锁通常使用一个共享的标志位(如一个布尔值)来表示锁的状态。当标志位为true 时,表示锁已被某个线程占用;当标志位为false 时,表示锁可用。当一个线程尝试获取自旋锁时,它会不断检查标志位:
如果标志位为 false,表示锁可用,线程将设置标志位为true,表示自己占用了锁,并进入临界区。
如果标志位为 true(即锁已被其他线程占用),线程会在一个循环中不断自旋等待,直到锁被释放。

2.3 自旋锁实现

自旋锁的实现通常使用原子操作来保证操作的原子性,常用的软件实现方式是通过 CAS(Compare-And-Swap)指令实现。以下是一个简单的自旋锁实现示例(伪代码):

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#include <unistd.h>
// 使用原子标志来模拟自旋锁
atomic_flag spinlock = ATOMIC_FLAG_INIT; // ATOMIC_FLAG_INIT 是 0
// 尝试获取锁
void spinlock_lock() {
	while (atomic_flag_test_and_set(&spinlock)) {
		// 如果锁被占用,则忙等待
	}
}
// 释放锁
void spinlock_unlock() {
	atomic_flag_clear(&spinlock);
}


typedef _Atomic struct
{
#if __GCC_ATOMIC_TEST_AND_SET_TRUEVAL == 1
	_Bool __val;
#else
	unsigned char __val;
#endif
}atomic_flag;

功能描述

  • atomic_flag_test_and_set 函数检查 atomic_flag 的当前状态。
  • 如果atomic_flag 之前没有被设置过(即其值为 false 或“未设置”状态),则函数会将其设置为 true(或“设置”状态),并返回先前的值(在这种情况下为 false)。
  • 如果atomic_flag 之前已经被设置过(即其值为 true),则函数不会改变其状态,但会返回 true。

原子性

  • 这个操作是原子的,意味着在多线程环境中,它保证了对 atomic_flag 的读取和修改是不可分割的。当一个线程调用此函数时,其他线程无法看到这个操作的任何中间状态,这确保了操作的线程安全性。

Linux 提供的自旋锁系统调用

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

注意事项

  • 使用自旋锁时,需要确保锁被释放的时间尽可能短,以避免 CPU 资源的浪费。
  • 在多 CPU 环境下,自旋锁可能不如其他锁机制高效,因为它可能导致线程在不同的 CPU 上自旋等待。

结论

  • 自旋锁是一种适用于短时间内锁竞争情况的同步机制,它通过减少线程切换的开销来提高锁操作的效率。然而,它也存在 CPU 资源浪费和可能引起活锁等缺点。
  • 在使用自旋锁时,需要根据具体的应用场景进行选择,并确保锁被释放的时间尽可能短。

案例: 抢票卖票

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

int ticket = 1000;
pthread_spinlock_t lock;

void *routine(void *arg)
{
    char *id = (char*)arg;
    while(1){
        if (ticket > 0){
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else{
            break;
        }
    }
    return nullptr;
}

int main(){
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, routine, (void *) "thread-1");
    pthread_create(&t2, NULL, routine, (void *) "thread-2");
    pthread_create(&t3, NULL, routine, (void *) "thread-3");
    pthread_create(&t4, NULL, routine, (void *) "thread-4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

还记得我们之前写的买票问题嘛,这个最后由于竞争问题,导致最后会出现负值,如下:

加上自旋锁,修改如下:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

int ticket = 1000;
pthread_spinlock_t lock;

void *routine(void *arg)
{
    char *id = (char*)arg;
    while(1)
    {
        pthread_spin_lock(&lock);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_spin_unlock(&lock);
        }
        else{
            pthread_spin_unlock(&lock);
            break;
        }
    }
    return nullptr;
}


int main()
{
    pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, NULL, routine, (void *) "thread-1");
    pthread_create(&t2, NULL, routine, (void *) "thread-2");
    pthread_create(&t3, NULL, routine, (void *) "thread-3");
    pthread_create(&t4, NULL, routine, (void *) "thread-4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

相比于之前的互斥锁,这里我们把 加锁放到了 while 循环内部,因为需要一直自检锁,此时结果如下:

2.4 自旋锁优缺点及应用场景

🥑 优点:
  1. 高效的短期锁竞争:如果锁持有时间很短,自旋锁可以非常高效,因为它避免了线程上下文切换的开销。
  2. 实现简单:自旋锁的实现通常非常简单,基本上只需要一个标志位(flag)和原子操作
  3. 低延迟:自旋锁适用于短时间内的锁竞争情况,因为它不会让线程进入休眠状
    态,从而避免了线程切换的开销,提高了锁操作的效率。
  4. 减少系统调度开销:等待锁的线程不会被阻塞,不需要上下文切换,从而减少了
    系统调度的开销
🥑 缺点:
  1. 高 CPU 使用率:如果线程需要等待很长时间才能获取锁,自旋锁会造成 CPU 时间片的浪费,因为线程会在忙等期间占用 CPU 资源。
  2. 可能导致优先级反转:如果一个低优先级的线程持有锁,而高优先级线程自旋等待,会造成优先级反转的问题(此问题可以通过其他技术如优先级继承来解决)。
  3. 不适用于长时间锁持有:如果锁的持有时间较长,自旋锁并不适合,因为自旋等待会导致极大的性能问题
  4. 可能引起活锁:当多个线程同时自旋等待同一个锁时,如果没有适当的退避策略,可能会导致所有线程都在不断检查锁状态而无法进入临界区,形成活锁
🥑使用场景:
  1. 短暂等待的情况:适用于锁被占用时间很短的场景,如多线程对共享数据进行简单的读写操作。
  2. 多线程锁使用:通常用于系统底层,同步多个 CPU 对共享资源的访问
  3. 锁的持有时间非常短:例如,对于某些非常简单的操作(如计数器的增减),自旋锁可以有效减少线程上下文切换的开销。
  4. 锁争用较少:如果多个线程竞争同一个锁的概率较小,则使用自旋锁可以减少等待时间,避免上下文切换的开销。

2.5 自旋锁 VS 互斥锁

🔥 与传统的互斥锁(Mutex)不同,互斥锁通常会让线程在无法获得锁时进入休眠状态,减少 CPU 的浪费,而自旋锁则在锁被占用时不断轮询,直到获取到锁。这种方式通常用于锁持有时间较短的场景,因为自旋锁避免了线程切换的开销,但是不合理的使用,可能也会浪费 CPU 资源


3. 共勉 🔥

 

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2270575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SELECT的使用

目录 1、SQL的查询命令 1.1 SELECT语句 1.2 投影查询 1.3 选择查询 1.4 聚合函数查询 1.5 简单分组查询(GROUP BY) 1.6 内连接查询 1.7 外连接查询 1.8 子查询 1. 无关子查询 2. 相关子查询 带exists的相关子查询&#xff1a; 1.9 集合查询 1. UNION(并) 2. INT…

Vue项目结构推荐(复杂国际化项目与一般项目结构)

Vue项目结构推荐 一、一般项目结构二、复杂国际化项目结构总结/建议 下面结构是基于Vue和TypeScript开发的项目结构下src包下的结构&#xff0c;若只用到vue与js。则去掉typescript部分的包即可。 一、一般项目结构 assets&#xff1a;存放静态资源&#xff0c;如图片、字体、样…

BOC调制信号matlab性能仿真分析,对比功率谱,自相关性以及抗干扰性

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff09…

电影院售票 - 策略模式(Strategy Pattern)

策略模式&#xff08;Strategy Pattern&#xff09; 策略模式&#xff08;Strategy Pattern&#xff09;策略模式概述策略模式结构图策略模式主要包含的角色 talk is cheap&#xff0c; show you my code总结 策略模式&#xff08;Strategy Pattern&#xff09; 策略模式&…

重学 Android 自定义 View 系列(十):带指针的渐变环形进度条

前言 该篇文章根据前面 重学 Android 自定义 View 系列(六)&#xff1a;环形进度条 拓展而来。 最终效果如下&#xff1a; 1. 扩展功能 支持进度顺时针或逆时针显示在进度条末尾添加自定义指针图片使用线性渐变为进度条添加颜色效果 2. 关键技术点解析 2.1 进度方向控制的…

【北京迅为】iTOP-4412全能版使用手册-第七十章 Linux内核移植

iTOP-4412全能版采用四核Cortex-A9&#xff0c;主频为1.4GHz-1.6GHz&#xff0c;配备S5M8767 电源管理&#xff0c;集成USB HUB,选用高品质板对板连接器稳定可靠&#xff0c;大厂生产&#xff0c;做工精良。接口一应俱全&#xff0c;开发更简单,搭载全网通4G、支持WIFI、蓝牙、…

CG顶会论文阅读|《科技论文写作》硕士课程报告

文章目录 一、基本信息1.1 论文基本信息1.2 课程基本信息1.3 博文基本信息 二、论文评述&#xff08;中英双语&#xff09;2.1 研究问题&#xff08;Research Problem&#xff09;2.2 创新点&#xff08;Innovation/Contribution&#xff09;2.3 优点&#xff08;Why this pape…

.NET周刊【12月第4期 2024-12-22】

国内文章 dotnet 简单使用 ICU 库进行分词和分行 https://www.cnblogs.com/lindexi/p/18622917 本文将和大家介绍如何使用 ICU 库进行文本的分词和分行。 dotnet 简单聊聊 Skia 里的 SKFontMetrics 的各项属性作用 https://www.cnblogs.com/lindexi/p/18621674 本文将和大…

git 问题解决记录

在用git上传文件到仓库中出现了2个问题 第一个问题&#xff1a; 需要修改git的代理端口与电脑自己的代理服务器设置中的端口和VPN的端口保持一致&#xff0c; 比如我的端口是7897&#xff0c;就设置 git config --global http.proxy http://127.0.0.1:7897 git config --glo…

XML结构快捷转JSON结构API集成指南

XML结构快捷转JSON结构API集成指南 引言 在当今的软件开发世界中&#xff0c;数据交换格式的选择对于系统的互操作性和效率至关重要。JSON&#xff08;JavaScript Object Notation&#xff09;和XML&#xff08;eXtensible Markup Language&#xff09;是两种广泛使用的数据表…

Oracle 创建本地用户,授予权限,创建表并插入数据

目录 一. 用户的种类二. 切换session为PDB三. 创建用户并授予权限四. 创建表空间五. 为用户分配默认表空间并指定表空间配额六. 通过创建的用户进行登录七. 创建脚本&#xff0c;简化登录八. 查看用户信息九. 创建表&#xff0c;并插入数据9.1 查看当前用户的schema9.2 插入数据…

系统设计——大文件传输方案设计

摘要 大文件传输是指通过网络将体积较大的文件从一个位置发送到另一个位置的过程。这些文件可能包括高清视频、大型数据库、复杂的软件安装包等&#xff0c;它们的大小通常超过几百兆字节&#xff08;MB&#xff09;甚至达到几个吉字节&#xff08;GB&#xff09;或更大。大文…

【老白学 Java】简单位移动画

简单位移动画 文章来源&#xff1a;《Head First Java》修炼感悟。 上一篇文章中&#xff0c;老白利用内部类的特性完成了多个事件的处理&#xff0c;感觉还不错。 为了更深入理解内部类&#xff0c;本篇文章继续使用内部类创建一个画板&#xff0c;完成简单的位移动画&#x…

彻底解决 Selenium ChromeDriver 不匹配问题:Selenium ChromeDriver 最新版本下载安装教程

在 Python 的 Selenium 自动化测试中&#xff0c;ChromeDriver 是不可或缺的工具。它作为代码与 Chrome 浏览器的桥梁&#xff0c;但如果版本不匹配&#xff0c;就会导致各种报错&#xff0c;尤其是以下常见问题&#xff1a; selenium.common.exceptions.SessionNotCreatedExc…

[CTF/网络安全] 攻防世界 warmup 解题详析

查看页面源代码&#xff0c;发现source.php 得到一串代码&#xff0c;进行代码审计&#xff1a; <?phpclass emmm{public static function checkFile(&$page){$whitelist ["source">"source.php","hint">"hint.php"];…

基于fMRI数据计算脑脊液(CSF)与全脑BOLD信号的时间耦合分析

一、前言 笔者之前的文章《基于Dpabi和spm12的脑脊液(csf)分割和提取笔记》,介绍了如何从普通的fMRI数据中提取CSF信号。首先是基础的预处理,包括时间层校正、头动校正,再加上0.01-0.1Hz的带通滤波。接着用SPM12分割出CSF区域,设置一个比较严格的0.9阈值,确保提取的真是…

游泳溺水识别数据集,对25729张图片进行YOLO,COCO JSON, VOC XML 格式的标注,溺水平均识别率在89.9%

游泳溺水识别数据集&#xff0c;对25729张图片进行YOLO&#xff0c;COCO JSON, VOC XML 格式的标注&#xff0c;溺水识别率在92&#xff05; 训练结果 数据集和标签 验证 游泳测试视频 根据测试的视频来获取检测结果&#xff1a; 游泳测试视频的置信度设置60% 检测结果如下&…

STM32 拓展 电源控制

目录 电源控制 电源框图 VDDA供电区域 VDD供电区域 1.8V低电压区域 后备供电区域 电压调节器 上电复位和掉电复位 可编程电压检测器(PVD) 低功耗 睡眠模式(只有CUP(老板)睡眠) 进入睡眠模式 退出睡眠模式 停机(停止)模式(只留核心区域(上班)) 进入停…

Mac M2 Pro安装MySQL 8.4.3

絮絮叨叨 MacBook Pro&#xff0c;芯片&#xff1a;Apple M2 Pro, macOS: Sonoma 14.0一直知道很多软件对Mac M1或M2的支持不好&#xff0c;但没想到在安装MySQL 8.x上也让我吃尽了苦头本文除了介绍如何安装MySQL 8.4.3外&#xff0c;还会记录笔者遇到的一些问题以及解决方法 …

闻泰科技涨停-操盘训练营实战-选股和操作技术解密

如上图&#xff0c;闻泰科技&#xff0c;今日涨停&#xff0c;这是前两天分享布局的一个潜伏短线的标的。 选股思路&#xff1a; 1.主图指标三条智能辅助线粘合聚拢&#xff0c;即将选择方向 2.上图红色框住部分&#xff0c;在三线聚拢位置&#xff0c;震荡筑底&#xff0c;…