Linux/AndroidOS中进程间的通信线程间的同步 - 信号量

news2025/5/12 16:55:25

1 概述

本文将介绍 POSIX 信号量,它允许进程和线程同步对共享资源的访问。有两种类型的 POSIX 信号量:

  • 命名信号量:这种信号量拥有一个名字。通过使用相同的名字调用 sem_open(),不相关的进程能够访问同一个信号量。
  • 未命名信号量:这种信号量没有名字,相反,它位于内存中一个预先商定的位置处。未命名信号量可以在进程之间或一组线程之间共享。当在进程之间共享时,信号量必须位于一个共享内存区域中。当在线程之间共享时,信号量可以位于被这些线程共享的一块内存区域中(如在堆上或在一个全局变量中)。

POSIX 信号量是一个整数,其值是不能小于 0 的。如果一个进程试图将一个信号量的值减小到小于 0,那么取决于所使用的函数,调用会阻塞或返回一个表明当前无法执行相应操作的错误。
一些系统并没有完整地实现 POSIX 信号量,一个典型的约束是只支持未命名线程共享的信号量。只有在 Linux 2.6 以及带 NPTL 的 glibc 上,完整的POSIX 信号量实现才可用。

2 命名信号量

要使用命名信号量必须要使用下列函数。

  • sem_open()函数打开或创建一个信号量并返回一个句柄以供后续调用使用,如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。
  • sem_post(sem)和 sem_wait(sem)函数分别递增和递减一个信号量值。
  • sem_getvalue()函数获取一个信号量的当前值。
  • sem_close()函数删除调用进程与它之前打开的一个信号量之间的关联关系。
  • sem_unlink()函数删除一个信号量名字并将其标记为在所有进程关闭该信号量时删除该信号量。

POSIX 并没有规定如何实现命名信号量。一些 UNIX 实现将它们创建成位于标准文件系统上一个特殊位置处的文件。在 Linux 上,命名信号量被创建成小型 POSIX 共享内存对象,其名字的形式为 sem.name,这些对象将被放在一个挂载在/dev/shm 目录之下的专用 tmpfs 文件系统中。这个文件系统具备内核持久性——它所包含的信号量对象将会持久,即使当前没有进程打开它们,但如果系统被关闭的话,这些对象就会丢失。

在 Linux 上从内核 2.6 起开始支持命名信号量。

2.1 打开一个命名信号量

sem_open()函数创建和打开一个新的命名信号量或打开一个既有信号量。

#include <fcntl.h>			/* Defines 0*constants */
#include<sys/stat.h>		/* Defines mode constants */
#include <semaphore.h>

/*Returns pointer to semaphore on success, Or SEM_FAILED on error*/
sem_t *sem_open(const char *name, int oflag, ...
				/*mode t mode, unsigned int value */);

name 参数标识出了信号量。

oflag 参数是一个位掩码,它确定了是打开一个既有信号量 / 创建并打开一个新信号量。如果 oflag 为 0,那么将访问一个既有信号量。如果在 oflag 中指定了 O_CREAT,并且与给定的 name对应的信号量的不存在,那么就创建一个新信号量。如果在 oflag 中同时指定了 O_CREAT 和O_EXCL,并且与给定的 name 对应的信号量已经存在,那么 sem_open()就会失败。

如果 sem_open()被用来打开一个既有信号量,那么调用就只需要两个参数如果在 flags中指定了 O_CREAT,那么就还需要另外两个参数:mode 和 value。(如果与 name 对应的信号量已经存在,那么这两个参数会被忽略。)具体如下。

  • mode 参数是一个位掩码,它指定了施加于新信号量之上的权限。这个参数能取的位值与文件上的位值是一样的并且与 open()一样,mode 参数中的值会根据进程的 umask 来取掩码。Linux中,在打开一个信号量时会将访问模式默认成 O_RDWR,因为大多数使用信号量的应用程序都同时会用到sem_post()和 sem_wait(),从而需要读取和修改一个信号量的值。这意味着需要确保将读权限和写权限赋给每一类需要访问这个信号量的用户——owner、group 以及 other。
  • value 参数是一个无符号整数,它指定了新信号量的初始值。信号量的创建和初始化操作是原子的。

不管是创建一个新信号量还是打开一个既有信号量,sem_open()都会返回一个指向一个sem_t 值的指针,而在后续的调用中则可以通过这个指针来操作这个信号量。sem_open()在发生错误时会返回 SEM_FAILED 值。(在Linux上,SEM_FAILED 被定义成了((sem_t *) 0) )。

POSIX 声称当在 sem_open()的返回值指向的 sem_t 变量的副本上执行操作(sem_post()、sem_wait()等)时结果是未定义的。换句话说,像下面这种使用 sem2 的做法是不允许的

sem_t *sp,sem2
sp = sem_open(...);
sem2 =*sp;
sem wait(&sem2);

通过 fork()创建的子进程会继承其父进程打开的所有命名信号量的引用。在 fork()之后,父进程和子进程就能够使用这些信号量来同步它们的动作了。

示例程序
程序为 sem_open()函数提供了一个命令行界面。在 usageError()函数中给出了这个程序的命令格式。
下面的 shell 会话日志演示了如何使用这个程序。首先使用 umask 命令来否决 other 用户的所有权限,然后互斥地创建一个信号量并查看包含该命名信号量的 Linux 特有的虚拟目录中的内容。

$ umask 007
$ ./psem_create -cx /demo 666
$ ls -l /dev/shm/sem.demo
-rw-rw---- 1 dockdroid dockdroid 32 May  6 17:23 /dev/shm/sem.demo

ls 命令的输出表明进程的 umask 覆盖了为 other 用户指定的 read+write 权限。
如果再次使用同样的名字来互斥地创建一个信号量,那么这个操作就会失败,因为这个名字已经存在了。

$ ./psem_create -cx /demo 666
ERROR [EEXIST File exists] sem_open
/*
psem/psem_create.c

   Create a POSIX named semaphore.

   Usage as shown in usageError().

   On Linux, named semaphores are supported with kernel 2.6 or later, and
   a glibc that provides the NPTL threading implementation.
*/

#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"

static void
usageError(const char *progName)
{
    fprintf(stderr, "Usage: %s [-cx] name [octal-perms [value]]\n", progName);
    fprintf(stderr, "    -c   Create semaphore (O_CREAT)\n");
    fprintf(stderr, "    -x   Create exclusively (O_EXCL)\n");
    exit(EXIT_FAILURE);
}

int
main(int argc, char *argv[])
{
    int flags, opt;
    mode_t perms;
    unsigned int value;
    sem_t *sem;

    flags = 0;
    while ((opt = getopt(argc, argv, "cx")) != -1) {
        switch (opt) {
        case 'c':   flags |= O_CREAT;           break;
        case 'x':   flags |= O_EXCL;            break;
        default:    usageError(argv[0]);
        }
    }

    if (optind >= argc)
        usageError(argv[0]);

    /* Default permissions are rw-------; default semaphore initialization
       value is 0 */

    perms = (argc <= optind + 1) ? (S_IRUSR | S_IWUSR) :
                getInt(argv[optind + 1], GN_BASE_8, "octal-perms");
    value = (argc <= optind + 2) ? 0 : getInt(argv[optind + 2], 0, "value");

    sem = sem_open(argv[optind], flags, perms, value);
    if (sem == SEM_FAILED)
        errExit("sem_open");

    exit(EXIT_SUCCESS);
}

2.2 关闭一个信号量

当一个进程打开一个命名信号量时,系统会记录进程与信号量之间的关联关系。sem_close()函数会终止这种关联关系(即关闭信号量),释放系统为该进程关联到该信号量之上的所有资源,并递减引用该信号量的进程数。

#include <semaphore.h>
int sem_close(sem_t *sem);

打开的命名信号量在进程终止或进程执行了一个 exec()时会自动被关闭。
关闭一个信号量并不会删除这个信号量,而要删除信号量则需要使用 sem_unlink()。

2.3 删除一个命名信号量

sem_unlink()函数删除通过 name 标识的信号量并将信号量标记成一旦所有进程都使用完这个信号量时就销毁该信号量(这可能立即发生,前提是所有打开过该信号量的进程都已经关闭了这个信号量)。

#include <semaphore.h>
int sem_unlink(const char *name);
/* psem/psem_unlink.c

   Unlink a POSIX named semaphore.

   On Linux, named semaphores are supported with kernel 2.6 or later, and
   a glibc that provides the NPTL threading implementation.
*/
#include <semaphore.h>
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    if (argc != 2 || strcmp(argv[1], "--help") == 0)
        usageErr("%s sem-name\n", argv[0]);

    if (sem_unlink(argv[1]) == -1)
        errExit("sem_unlink");
    exit(EXIT_SUCCESS);
}

3 信号量操作

POSIX 信号量也是一个整数并且系统不会允许其值小于 0。POSIX 信号量的操作具体包括:

  • 修改信号量值的函数———sem_post()和 sem_wait(),一次只操作一个信号量。
  • sem_post()和 sem_wait()函数只对信号量值加 1 和减 1。

3.1 等待一个信号量

sem_wait()函数会递减(减小 1)sem 引用的信号量的值。

#include <semaphore.h>
int sem_wait(sem_t *sem);
  • 如果信号量的当前值大于 0,那么 sem_wait()会立即返回。
  • 如果信号量的当前值等于 0,那么 sem_wait()会阻塞直到信号量的值大于 0 为止,当信号量值大于 0 时该信号量值就被递减并且 sem_wait()会返回。
  • 如果一个阻塞的 sem_wait()调用被一个信号处理器中断了,那么它就会失败并返回 EINTR错误,不管在使用 sigaction()建立这个信号处理器时是否采用了 SA_RESTART 标记。(在其他一些 UNIX 实现上,SA_RESTART 会导致 sem_wait()自动重启。)

下面示例程序为 sem_wait()函数提供了一个命令行界面,稍后就会演示如何使用这个程序。

/* psem/psem_wait.c

   Decrease the value of a POSIX named semaphore.

   See also psem_post.c.

   On Linux, named semaphores are supported with kernel 2.6 or later, and
   a glibc that provides the NPTL threading implementation.
   使用 sem_wait()来递减一个 POSIX 信号量 
*/
#include <semaphore.h>
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    sem_t *sem;

    if (argc < 2 || strcmp(argv[1], "--help") == 0)
        usageErr("%s sem-name\n", argv[0]);

    sem = sem_open(argv[1], 0);
    if (sem == SEM_FAILED)
        errExit("sem_open");

    if (sem_wait(sem) == -1)
        errExit("sem_wait");

    printf("%ld sem_wait() succeeded\n", (long) getpid());
    exit(EXIT_SUCCESS);
}

sem_trywait()函数是 sem_wait()的一个非阻塞版本。

#include <semaphore.h>
int sem_trywait(sem_t *sem);

如果递减操作无法立即被执行,那么 sem_trywait()就会失败并返回 EAGAIN 错误。

sem_timedwait()函数是 sem_wait()的另一个变体,它允许调用者为调用被阻塞的时间量指定一个限制。

#define _XOPEN_SOURCE 600
#include <semaphore.h>
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

如果 sem_timedwait()调用因超时而无法递减信号量,那么这个调用就会失败并返回ETIMEDOUT 错误。

abs_timeout 参数是一个结构,它将超时时间表示成了自新纪元到现在为止的秒数和纳秒数的绝对值。如果需要指定一个相对超时时间,那么就必须要使用 clock_gettime()获取 CLOCK_REALTIME 时钟的当前值并在该值上加上所需的时间量来生成一个适合在sem_timedwait()中使用的 timespec 结构。

3.2 发布一个信号量

sem_post()函数递增(增加 1)sem 引用的信号量的值。

#include <semaphore.h>
int sem_post(sem_t *sem);

如果在 sem_post()调用之前信号量的值为 0,并且其他某个进程(或线程)正在因等待递减这个信号量而阻塞,那么该进程会被唤醒,它的 sem_wait()调用会继续往前执行来递减这个信号量。如果多个进程(或线程)在 sem_wait()中阻塞了,并且这些进程的调度采用的是默认的循环时间分享策略,那么哪个进程会被唤醒并允许递减这个信号量是不确定的。(POSIX 信号量仅仅是一种同步机制,而不是一种排队机制。)
递增一个 POSIX 信号量对应于释放一些共享资源以供其他进程或线程使用。

示例程序为 sem_post()函数提供了一个命令行界面。

/* psem/psem_post.c

   Increase the value of a POSIX named semaphore.

   See also psem_wait.c.

   On Linux, named semaphores are supported with kernel 2.6 or later, and
   a glibc that provides the NPTL threading implementation.
   使用 sem_post()递增一个 POSIX 信号量
*/
#include <semaphore.h>
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    sem_t *sem;

    if (argc != 2)
        usageErr("%s sem-name\n", argv[0]);

    sem = sem_open(argv[1], 0);
    if (sem == SEM_FAILED)
        errExit("sem_open");

    if (sem_post(sem) == -1)
        errExit("sem_post");
    exit(EXIT_SUCCESS);
}

3.3 获取信号量的当前值

sem_getvalue()函数将 sem 引用的信号量的当前值通过 sval 指向的 int 变量返回。

#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);

如果一个或多个进程(或线程)当前正在阻塞以等待递减信号量值,那么 sval 中的返回值将取决于实现。POSIX 允许两种做法:0 或一个绝对值等于在 sem_wait()中阻塞的等待者数目的负数。Linux 和其他一些实现采用了第一种行为,而另一些实现则采用了后一种行为。

注意在 sem_getvalue()返回时,sval 中的返回值可能已经过时了。依赖于 sem_getvalue()返回的信息在执行后续操作时未发生变化的程序将会碰到检查时、使用时(time-of-check、time-of-use)的竞争条件。

示例程序使用了 sem_getvalue()来获取名字通过命令行参数指定的信号量的值,然后在标准输出上显示该值。

/* psem_getvalue.c

   Obtain the value of a POSIX named semaphore.

   On Linux, named semaphores are supported with kernel 2.6 or later, and
   a glibc that provides the NPTL threading implementation.
   使用 sem_getvalue()获取一个 POSIX 信号量的值
*/
#include <semaphore.h>
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    int value;
    sem_t *sem;

    if (argc != 2)
        usageErr("%s sem-name\n", argv[0]);

    sem = sem_open(argv[1], 0);
    if (sem == SEM_FAILED)
        errExit("sem_open");

    if (sem_getvalue(sem, &value) == -1)
        errExit("sem_getvalue");

    printf("%d\n", value);
    exit(EXIT_SUCCESS);
}

3.4 示例

下面的 shell 会话日志演示了如何使用本章中到目前为止给出的各个程序。首先创建了一个初始值为零的信号量,然后在后台启动一个递减这个信号量的程序。

$ ./psem_create -c /demo 600 0
$ ./psem_wait /demo &
[1] 3498686

后台命令将会阻塞,这是因为信号量的当前值为 0,从而无法递减这个信号量。接着获取这个信号量的值。

$ ./psem_getvalue /demo
0

从上面可以看到值 0。在其它系统上可能会看到值−1,表示存在一个进程正在等待这个信号量。
接着执行一个命令来递增这个信号量,这将会导致后台程序中被阻塞的 sem_wait()调用完成执行。

$ ./psem_post /demo
3498686 sem_wait() succeeded
[1]+  Done                    ./psem_wait /demo

(上面输出中的最后一行表明 shell 提示符会与后台作业的输出混合在一起。)
按下回车后就能看到下一个shell提示符,这也会导致shell报告已终止的后台作业的信息。接着在信号量上执行后续的操作。

$ ./psem_post /demo
$ ./psem_getvalue /demo
1
$ ./psem_unlink /demo

4 未命名信号量

未命名信号量(也被称为基于内存的信号量)是类型为 sem_t 并存储在应用程序分配的内存中的变量。通过将这个信号量放在由几个进程或线程共性的内存区域中就能够使这个信号量对这些进程或线程可用。

操作未命名信号量所使用的函数与操作命名信号量使用的函数是一样的(sem_wait()、sem_post()以及 sem_getvalue()等)。此外,还需要用到另外两个函数(这些函数不应该被应用到命名信号量上)。

  • sem_init()函数对一个信号量进行初始化并通知系统该信号量会在进程间共享还是在单个进程中的线程间共享。
  • sem_destroy(sem)函数销毁一个信号量。

未命名与命名信号量对比
使用未命名信号量之后就无需为信号量创建一个名字了,这种做法在下列情况中是比较有用的。

  • 在线程间共享的信号量不需要名字。将一个未命名信号量作为一个共享(全局或堆上的)变量自动会使之对所有线程可访问。
  • 在相关进程间共享的信号量不需要名字。如果一个父进程在一块共享内存区域中(如一个共享匿名映射)分配了一个未命名信号量,那么作为 fork()操作的一部分,子进程会自动继承这个映射,从而继承这个信号量。
  • 如果正在构建的是一个动态数据结构(如二叉树),并且其中的每一项都需要一个关联的信号量,那么最简单的做法是在每一项中都分配一个未命名信号量。为每一项打开一个命名信号量需要为如何生成每一项中的信号量名字(唯一的)和管理这些名字设计一个规则(如当不再需要它们时就对它们进行断开链接操作)。

4.1 初始化一个未命名信号量

sem_init()函数使用 value 中指定的值来对 sem 指向的未命名信号量进行初始化。

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

pshared 参数表明这个信号量是在线程间共享 or 进程间共享

  • 如果 pshared 等于 0,那么信号量将会在调用进程中的线程间进行共享。在这种情况下,sem 通常被指定成一个全局变量的地址或分配在堆上的一个变量的地址。线程共享的信号量具备进程持久性,它在进程终止时会被销毁。
  • 如果 pshared 不等于 0,那么信号量将会在进程间共享。在这种情况下,sem 必须是共享内存区域(一个 POSIX 共享内存对象、一个使用 mmap()创建的共享映射)中的某个位置的地址。信号量的持久性与它所处的共享内存的持久性是一样的。(通过其中大部分技术创建的共享内存区域具备内核持久性。但共享匿名映射是一个例外,只要存在一个进程维持着这种映射,那么它就一直存在下去。)由于通过 fork()创建的子进程会继承其父进程的内存映射,因此进程共享的信号量会被通过 fork()创建的子进程继承,这样父进程和子进程也就能够使用这些信号量来同步它们的动作了。

之所以需要 pshared 参数是因为下列原因。

  • 一些实现不支持进程间共享的信号量。在这些系统上为 pshared 指定一个非零值会导致 sem_init()返回一个错误。Linux 直到内核 2.6 以及 NPTL 线程化技术的出现之后才开始支持未命名的进程间共享的信号量。
  • 在同时支持进程间共享信号量和线程间共享信号量的实现上,指定采用何种共享方式是有必要的,因为系统必须要执行特殊的动作来支持所需的共享方式。提供此类信息还使得系统能够根据共享的种类来执行优化工作。

未命名信号量不存在相关的权限设置(即 sem_init()中并不存在在 sem_open()中所需的mode 参数)。对一个未命名信号量的访问将由进程在底层共享内存区域上的权限来控制。
与命名信号量一样,在地址通过传入 sem_init()的 sem 参数指定的 sem_t 变量的副本上执行操作的结果是未定义的,因此应该总是只在“最初的”信号量上执行操作。

示例程序
可以使用互斥体来保护一个存在两个线程访问同一个全局变量的临界区的程序。但下面程序使用一个未命名线程共享的信号量解决了同样的问题。

$ ./thread_incr_psem
glob = 20000000
/* thread_incr_psem.c

   Use a POSIX unnamed semaphore to synchronize access by two threads to
   a global variable.

   See also thread_incr.c and thread_incr_mutex.c.
   使用一个 POSIX 未命名信号量来保护对全局变量的访问 
*/
#include <semaphore.h>
#include <pthread.h>
#include "tlpi_hdr.h"

static int glob = 0;
static sem_t sem;

static void *                   /* Loop 'arg' times incrementing 'glob' */
threadFunc(void *arg)
{
    int loops = *((int *) arg);
    int loc, j;

    for (j = 0; j < loops; j++) {
        if (sem_wait(&sem) == -1)
            errExit("sem_wait");

        loc = glob;
        loc++;
        glob = loc;

        if (sem_post(&sem) == -1)
            errExit("sem_post");
    }

    return NULL;
}

int
main(int argc, char *argv[])
{
    pthread_t t1, t2;
    int loops, s;

    loops = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-loops") : 10000000;

    /* Initialize a semaphore with the value 1 */

    if (sem_init(&sem, 0, 1) == -1)
        errExit("sem_init");

    /* Create two threads that increment 'glob' */

    s = pthread_create(&t1, NULL, threadFunc, &loops);
    if (s != 0)
        errExitEN(s, "pthread_create");
    s = pthread_create(&t2, NULL, threadFunc, &loops);
    if (s != 0)
        errExitEN(s, "pthread_create");

    /* Wait for threads to terminate */

    s = pthread_join(t1, NULL);
    if (s != 0)
        errExitEN(s, "pthread_join");
    s = pthread_join(t2, NULL);
    if (s != 0)
        errExitEN(s, "pthread_join");

    printf("glob = %d\n", glob);
    exit(EXIT_SUCCESS);
}

4.2 销毁一个未命名信号量

sem_destroy()函数将销毁信号量 sem,其中 sem 必须是一个之前使用 sem_init()进行初始化的未命名信号量。只有在不存在进程或线程在等待一个信号量时才能够安全销毁这个信号量。

#include <semaphore.h>
int sem_destroy(sem_t *sem);

当使用 sem_destroy()销毁了一个未命名信号量之后就能够使用 sem_init()来重新初始化这个信号量了。
一个未命名信号量应该在其底层的内存被释放之前被销毁。例如,如果信号量一个自动分配的变量,那么在其宿主函数返回之前就应该销毁这个信号量。如果信号量位于一个 POSIX共享内存区域中,那么在所有进程都使用完这个信号量以及在使用 shm_unlink()对这个共享内存对象执行断开链接操作之前应该销毁这个信号量。
在一些实现上,省略 sem_destroy()调用不会导致问题的发生,但在其他实现上,不调用sem_destroy()会导致资源泄露。可移植的应用程序应该调用 sem_destroy()以避免此类问题的发生。

5 POSIX 信号量与 Pthreads 互斥体对比

POSIX 信号量和 Pthreads 互斥体都可以用来同步同一个进程中的线程的动作,并且它们的性能也是相近的。然而互斥体通常是首选方法,因为互斥体的所有权属性能够确保代码具有良好的结构性(只有锁住互斥体的线程才能够对其进行解锁)。与之形成对比的是,一个线程能够递增一个被另一个线程递减的信号量。这种灵活性会导致产生结构糟糕的同步设计。
(正是因为这个原因,信号量有时候会被称为并发式编程中的“goto”。)

互斥体在一种情况下是不能用在多线程应用程序中的,在这种情况下信号量可能就成了一种首选方法了。由于信号量是异步信号安全的,因此在一个信号处理器中可以使用 sem_post()函数来与另一个线程进行同步。而信号量就无法完成这项工作,因为操作互斥体的 Pthreads 函数不是异步信号安全的。然而通常处理异步信号的首选方法是使用sigwaitinfo()(或类似的函数)来接收这些信号,而不是使用信号处理器,因此信号量比互斥体在这一点上的优势很少有机会发挥出来。

6 信号量的限制

SUSv3 为信号量定义了两个限制。

  • SEM_NSEMS_MAX
    这是一个进程能够拥有的 POSIX 信号量的最大数目。SUSv3 要求这个限制至少为 256。在 Linux 上,POSIX 信号量数目实际上会受限于可用的内存。
  • SEM_VALUE_MAX
    这是一个 POSIX 信号量值能够取的最大值。信号量的取值可以为 0 到这个限制之间的任意一个值。SUSv3 要求这个限制至少为 32767,Linux 实现允许这个值最大为 INT_MAX(在Linux/x86-32 上是 2147483647)。

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

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

相关文章

Edge浏览器PDF字体显示错误

Edge浏览器PDF字体显示错误 软件版本信息 Edge Version: 136.0.3240.50 Word Version: Microsoft Office 专业增强版2021问题描述 在Word中使用多级列表自动编号, 并使用Word软件自带的导出为PDF文件功能, 在Word中显示正常的数字, 在Edge中查看PDF将会出现渲染错误的现象,…

Python训练营打卡——DAY22(2025.5.11)

复习日 学习参考如何使用kaggle平台&#xff0c;写下使用注意点&#xff0c;并对下述比赛提交代码 泰坦尼克号——来自灾难的机器学习 数据来源&#xff1a; kaggle泰坦里克号人员生还预测 挑战 泰坦尼克号沉没是历史上最臭名昭著的海难之一。 1912年4月15日&#xff0c;在被普…

实战项目4(05)

​目录 任务场景一 【sw1配置】 任务场景二 【sw1配置】 【sw2配置】 任务场景一 按照下图完成网络拓扑搭建和配置 任务要求&#xff1a; 1、在交换机SW1的E0/0/1端口进行设置&#xff0c;实现允许最多两个电脑可以正常进行通信。 2、在交换机SW1的E0/0/2端口进行设置&…

C++学习之STL学习

在经过前面的简单的C入门语法的学习后&#xff0c;我们开始接触C最重要的组成部分之一&#xff1a;STL 目录 STL的介绍 什么是STL STL的历史 UTF-8编码原理&#xff08;了解&#xff09; UTF-8编码原理 核心编码规则 规则解析 编码步骤示例 1. 确定码点范围 2. 转换为…

3. 仓颉 CEF 库封装

文章目录 1. capi 使用说明2. Cangjie CEF2. 1实现目标 3. 实现示例 1. capi 使用说明 根据上一节 https://blog.csdn.net/qq_51355375/article/details/147880718?spm1011.2415.3001.5331 所述&#xff0c; cefcapi 是libcef 共享库导出一个 C API, 而以源代码形式分发的 li…

LabVIEW多通道并行数据存储系统

在工业自动化监测、航空航天测试、生物医学信号采集等领域&#xff0c;常常需要对多个传感器通道的数据进行同步采集&#xff0c;并根据后续分析需求以不同采样率保存特定通道组合。传统单线程数据存储方案难以满足实时性和资源利用效率的要求&#xff0c;因此设计一个高效的多…

谷歌在即将举行的I/O大会之前,意外泄露了其全新设计语言“Material 3 Expressive”的细节

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

十三、基于大模型的在线搜索平台——整合function calling流程

基于大模型的在线搜索平台——整合function calling流程 一、function calling调用总结 上篇文章已经实现了信息抓取能力&#xff0c;并封装成了函数。现在最后一步将能力转换为大模型可以调用的能力&#xff0c;实现搜索功能就可以了。这篇主要实现大模型的function calling能…

力扣70题解

记录 2025.5.8 题目: 思路&#xff1a; 1.初始化&#xff1a;p 和 q 初始化为 0&#xff0c;表示到达第 0 级和第 1 级前的方法数。r 初始化为 1&#xff0c;表示到达第 1 级台阶有 1 种方法。 2.循环迭代&#xff1a;从第 1 级到第 n 级台阶进行迭代&#xff1a; p 更新为前…

电商双11美妆数据分析

1、初步了解 2.2 缺失值处理 通过上面观察数据发现sale_count,comment_count 存在缺失值,先观察存在缺失值的行的基本情况 2.3 数据挖掘寻找新的特征 给出各个关键词的分类类别 由title新生成两列类别 对是否是男性专用进行分析并新增一列 对每个产品总销量新增销售额这一列

24、TypeScript:预言家之书——React 19 类型系统

一、预言家的本质 "TypeScript是魔法世界的预言家之书&#xff0c;用静态类型编织代码的命运轨迹&#xff01;" 霍格沃茨符文研究院的巫师挥动魔杖&#xff0c;类型注解与泛型的星轨在空中交织成防护矩阵。 ——基于《国际魔法联合会》第12号类型协议&#xff0c;Ty…

第8章-1 查询性能优化-优化数据访问

上一篇&#xff1a;《第7章-3 维护索引和表》 在前面的章节中&#xff0c;我们介绍了如何设计最优的库表结构、如何建立最好的索引&#xff0c;这些对于提高性能来说是必不可少的。但这些还不够——还需要合理地设计查询。如果查询写得很糟糕&#xff0c;即使库表结构再合理、索…

PCL点云按指定方向进行聚类(指定类的宽度)

需指定方向和类的宽度。测试代码如下&#xff1a; #include <iostream> #include <fstream> #include <vector> #include <string> #include <pcl/point_types.h> #include <pcl/point_cloud.h> #include <pcl/visualization/pcl_visu…

C#对SQLServer增删改查

1.创建数据库 2.SqlServerHelper using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks;namespace WindowsFormsApp1 {internal class SqlServerHelper{//…

模拟太阳系(C#编写的maui跨平台项目源码)

源码下载地址&#xff1a;https://download.csdn.net/download/wgxds/90789056 本资源为用C#编写的maui跨平台项目源码&#xff0c;使用Visual Studio 2022开发环境&#xff0c;基于.net8.0框架&#xff0c;生成的程序为“模拟太阳系运行”。经测试&#xff0c;生成的程序可运行…

蓝桥杯14届 数三角

问题描述 小明在二维坐标系中放置了 n 个点&#xff0c;他想在其中选出一个包含三个点的子集&#xff0c;这三个点能组成三角形。然而这样的方案太多了&#xff0c;他决定只选择那些可以组成等腰三角形的方案。请帮他计算出一共有多少种选法可以组成等腰三角形&#xff1f; 输…

HTML12:文本框和单选框

表单元素格式 属性说明type指定元素的类型。text、password、 checkbox、 radio、submit、reset、file、hidden、image 和button&#xff0c;默认为textname指定表单元素的名称value元素的初始值。type为radio时必须指定一个值size指定表单元素的初始宽度。当type为text 或pas…

机器人厨师上岗!AI在餐饮界掀起新风潮!

想要了解人工智能在其他各个领域的应用&#xff0c;可以查看下面一篇文章 《AI在各领域的应用》 餐饮业是与我们日常生活息息相关的行业&#xff0c;而人工智能&#xff08;AI&#xff09;正在迅速改变这个传统行业的面貌。从智能点餐到食材管理&#xff0c;再到个性化推荐&a…

MySQL开篇

文章目录 一、前置知识1. MySQL的安装2. 前置一些概念知识 二、MySQL数据库操作2.1 概念2.2 数据库的操作2.2.1创建数据库命令2.2.2 查看数据库2.2.3 选中数据库2.2.4 删除数据库 三、MySQL数据表操作3.1 概念3.2 数据表的操作3.2.1 创建表 一、前置知识 1. MySQL的安装 MySQ…

Linux电脑本机使用小皮面板集成环境开发调试WEB项目

开发调试WEB项目&#xff0c;有时开发环境配置繁琐&#xff0c;可以使用小皮面板集成环境。 小皮面板官网&#xff1a; https://www.xp.cn/1.可以使用小皮面板安装脚本一键安装。登陆小皮面板管理后台 2.在“软件商店”使用LNMP一键部署集成环境。 3.添加网站&#xff0c;本…