LINUX 入门 3

LINUX 入门 3

day6 20240429 耗时:180min

课程链接地址

上一章作业题:如何实现按name首字母顺序存储,接口层业务层不改,只改支持层存储结构,不用struct链表,用哈希

第3章 LINUX环境编程——并发锁方案

C基础

  1. <pthread.h>

    1. pthread_t:“pthread_t” 是 POSIX 线程库中定义的一种数据类型,用于表示线程标识符。在多线程编程中,每个线程都有一个唯一的标识符,可以用来控制、管理和操作线程。通常,使用 pthread 库创建线程时,会将线程的标识符存储在类型为 pthread_t 的变量中,并在需要时使用该标识符来执行线程相关的操作,如等待线程结束、取消线程等。
    2. pthread_create(&threadid[i], NULL, thread_callback, &count);
      • &threadid[i]:这是一个 pthread_t 类型的变量的地址,用于存储新创建的线程的标识符。i 可能是循环中的索引,用于跟踪多个线程的标识符。
      • NULL:这是一个指向线程属性的指针,通常可以使用 NULL 来使用默认属性。
      • thread_callback:这是一个函数指针,指向新线程将要执行的函数。线程被创建后,将会执行这个函数。 每个thread有自己callback
      • &count:主线程往子线程传的参数,这是一个指向某个数据的指针,是一个地址,它会被传递给线程回调函数 thread_callback的形参arg里面!!。这样,线程在执行时可以访问和操作这个数据。 count是被多个线程共享的资源,都可以计数
    3. void* thread_callback(void *arg) {
      • 函数参数 (void *arg):这是一个泛型指针,可以指向任何类型的数据。通常,它被用来传递数据到线程中,例如,如果你需要给线程传递一个或多个值,你可以定义一个结构体来包含这些值,然后传递一个指向这个结构体的指针
    4. #include <unistd.h> 头文件,这样就会包含 usleep 函数的声明,编译器就不会报错了
  2. 第二章

    1. #if 0 是一种条件编译指令,它告诉预处理器忽略接下来的代码段,直到遇到对应的 #endif 指令。这种用法通常用于临时注释掉一段代码,而不需要删除它们。

      在你的代码中,#if 0 后面的代码段被注释掉了,因此编译器会忽略这部分代码,直到遇到匹配的 #endif 指令。这样做可以方便地在调试或测试阶段临时禁用某些代码块编译才会用到,而不需要删除它们。

  3. 第三章

    pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);

    PTHREAD_PROCESS_SHARED: 这意味着多个进程可以使用这个锁来保护共享资源

  4. 第四章

    内联汇编是一种直接嵌入在C或C++代码中的汇编代码,可以对特定的硬件进行精细的控制和优化。

    通常,内联汇编的语法结构如下:

    __asm__ volatile(
        "汇编指令1;\n"
        "汇编指令2;\n"
        ...
        : 输出操作数列表
        : 输入操作数列表
        : 修改的寄存器列表
    );
    

    这里是一些需要注意的要点:

    • volatile 关键字确保编译器不会对内联汇编进行优化或重排。
    • 输出操作数列表是指那些在内联汇编中被修改并返回给C代码的变量。
    • 输入操作数列表是指内联汇编所需要使用的变量。
    • 修改的寄存器列表是指内联汇编所修改的寄存器。
    	int old;
    
    	__asm__ volatile(
    		"lock; xaddl %2, %1;"
    		: "=a" (old)
    		: "m" (*value), "a"(add)
    		: "cc", "memory"
    	);
    
    	return old;
    

    使用内联汇编(inline assembly)实现了一个原子的加法操作,具体来说是使用了xaddl指令。让我解释一下这段代码的作用和含义:

    1. lock; xaddl %2, %1;
      • lock; 是指在执行指令时使用锁定总线,确保指令的原子性。
      • xaddl %2, %1; 则是执行一个带有锁定的交换指令,将%2(即add变量的值)加到%1所指向的内存地址的值上,并将结果存储到%1所指向的内存地址中,同时返回旧的值到寄存器%eax中。
    2. : "=a" (old)
      • 表示使用寄存器%eax来存储返回的旧值old
    3. : "m" (*value), "a"(add)
      • "m" (*value) 表示value是一个内存操作数。
      • "a"(add) 表示将add的值放入寄存器%eax中。
    4. : "cc", "memory"
      • "cc" 表示修改了条件代码寄存器(condition code register)。
      • "memory" 表示内联汇编可能会对内存进行修改,因此需要通知编译器。

    这段代码的作用是将add的值原子地加到value所指向的内存地址中,并返回加法前的旧值。需要注意的是,内联汇编对底层硬件的操作细节非常敏感,一般情况下应当避免直接使用内联汇编,除非对硬件有深入了解并且确实需要优化特定的操作。

1 多线程并发锁的项目介绍

  1. 概念引入

    1. 临界资源:例子:买火车票,多个窗口共用一块资源,这里就是create函数里的count(都可以查座位号)
    2. 并发concurrency
    3. 多线程

    在这里插入图片描述

  2. code

    10个火车票窗口——10个thread线程

    定义thread标识符id——create thread——thread 的callback函数

  3. 编译

    gcc -o lock lock.c -lpthread
    

    在编译时使用 -lpthread 标志是为了告诉编译器链接 pthread 库。这样做是因为你的程序中使用了 pthread 库提供的函数,比如 pthread_create。在使用这些函数之前,需要确保在编译时链接了相应的库,以便程序能够正确地找到并使用这些函数。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_COUNT 10

void* thread_callback(void *arg){
    int *pcount = (int*) arg;
    int i =0;
    //用pcount对arg就是pthread_create里的count加十万次
    while(i++<100000){
        (*pcount)++;
        usleep(1);//休眠1us,观察
    }
}

int main(){
    pthread_t threadid[THREAD_COUNT] = {0};


    int i = 0, count = 0;
    for(; i <THREAD_COUNT; i++){
        pthread_create(&threadid[i],NULL, thread_callback,&count); //10个窗口对火车票count都加
    }

    
	for (i = 0;i < 100;i ++) {
		printf("count : %d\n", count);
		sleep(1);
	}
    
}

2 多线程并发锁的方案——互斥锁mutex

  1. 问题分析

    1. **上一章问题:**最后卡在稳态很久971763,本来应该每个thread加10万,最后100万,但是不到 why???

    2. 核心代码是void* thread_back里的(*pcount) ++;

      底层汇编

      1. mov [count], eax; //移count到寄存器
      2. inc eax; 自增
      3. mov eax, [count]

正常情况: 线程不乱跳

在这里插入图片描述

不正常:就是因为count是临界资源,但是底层汇编实现的寄存器eax并不是共用的临界资源,thread的eax都不一样,导致一旦乱跳的时候,基于eax的加操作白加在这里插入图片描述

  1. 解决方案:对count这种临界资源加锁

    互斥锁 mutex:工程上very very very常用!!!!

    执行+前加锁,执行完解锁

    //define后面
    pthread_mutex_t mutex;
    
    //main的线程create前初始化锁
    pthread_mutex_init(&mutex, NULL);
    
    //每个thread的callback里加锁解锁
    while(i++<100000){
    #if 0
            (*pcount)++;
    #else
            pthread_mutex_lock(&mutex);
    		(*pcount) ++; //
    		pthread_mutex_unlock(&mutex);
    #endif        
            usleep(1);//休眠1us,观察
        }
    

    这样线程1在count++时,count资源被锁住了,就是汇编那三条语句被打包保护了,不会中途打断乱跳到其他thread i!!! 试一下,10个thread可以同时对count加到100万了,成功啦啦啦啦啦啦

3 多线程并发锁的方案——自旋锁spinlock

//define后面
pthread_spinlock_t spinlock;

//main的线程create前初始化锁, 多个进程共用
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);

//每个thread的 void callback里加锁解锁
while(i++<100000){
#if 0
        (*pcount)++;
#elif 0
        pthread_mutex_lock(&mutex);
		(*pcount) ++; //
		pthread_mutex_unlock(&mutex);
#else
        pthread_spin_lock(&spinlock);
		(*pcount) ++; //
		pthread_spin_unlock(&spinlock);
#endif        
        usleep(1);//休眠1us,观察
    }
  1. 什么时候mutex 什么时候spinlock

    用锁保护对临界资源操作的内容 使临界资源只有一个thread可用

    1. Mutex(互斥锁)
      • 当线程尝试获取一个被其他线程持有的互斥锁时,它会被放入睡眠状态(即阻塞),直到该锁可用为止。 会放弃CPU使用权
      • 当线程成功获取到互斥锁后,其他线程必须等待该线程释放锁才能继续执行。
    2. Spinlock(自旋锁)
      • 当线程尝试获取一个被其他线程持有的自旋锁时,它会在一个循环while(1)中不断地检查锁是否可用而不会放弃CPU的使用权。核心区别
      • 如果在短时间内可以获取到锁,自旋锁的效率可能会比互斥锁高,因为它避免了线程的上下文切换。
      • 但是,如果锁的持有者长时间不释放锁,那么等待锁的线程会一直在循环中占用CPU资源,可能会导致性能下降。

    当锁的内容少(临界区的持有时间短,线程不会长时间等待锁,切换CPU太耗时,不切了),用spinlock

    锁的内容多(等待时间长,远大于线程切换时间,那就切换吧), mutex

4 原子操作 atmoic

可不可以不加锁就把三条汇编CPU指令打包成一条CPU指令,就不会有中途跳来跳去情况

  1. mov [count], eax; //移count到寄存器
  2. inc eax; 自增
  3. mov eax, [count]
int inc(int *value, int add) {
    // 把pcount地址传入value

	int old;

	__asm__ volatile(
		"lock; xaddl %2, %1;"  %2是add要加的数,%1是value被加的pcount原始值,把2加到1 返回到1: "=a" (old)
		: "m" (*value), "a"(add)
		: "cc", "memory"
	);

	return old;    
}


//每个thread的 void callback里加锁解锁
while(i++<100000){
#if 0
        (*pcount)++;
#elif 0
        pthread_mutex_lock(&mutex);
		(*pcount) ++; //
		pthread_mutex_unlock(&mutex);
#elif 0
        pthread_spin_lock(&spinlock);
		(*pcount) ++; //
		pthread_spin_unlock(&spinlock);
#else 
		inc(pcount,1);
#endif        
        usleep(1);//休眠1us,观察
    }

mutex spinlock atomic区别

  1. mutex:临界资源count被thread1占了,其他thread来了,直接切走CPU

  2. spinlock:其他thread来了,等着直到释放,不切CPU

  3. atomic:最优,从源头解决问题,直接把多条指令打包成一条指令,根本不会出现这种多条指令没执行完就乱跳的情况

    原子操作是不可被中断的操作,它要么完全执行,要么完全不执行,不会在执行过程中被其他线程或中断打断。在多线程环境中,原子操作对于确保数据的一致性和正确性非常重要。

    在编程中,原子操作通常用于实现同步机制和并发控制,特别是在多线程并发访问共享资源时。常见的原子操作包括原子加减、原子比较交换(CAS)等。这些操作通常由硬件提供支持,或者通过锁定机制来实现。

    在C/C++中,可以使用特定的原子操作函数或者语言扩展来实现原子操作。例如,在C11标准中,提供了 <stdatomic.h> 头文件,其中包含了一系列的原子操作函数,如 atomic_load, atomic_store, atomic_fetch_add 等。

    在底层,一些处理器提供了硬件级别的原子操作指令,如 x86 架构的 lock 前缀指令,用于实现原子操作。除了硬件支持,编译器也可以通过生成适当的指令序列来确保原子性。总的来说,原子操作在并发编程中扮演着至关重要的角色,能够确保数据的一致性和正确性,避免了竞态条件和数据竞争的发生。

    cas常用工程例子:

    CAS(Compare and Swap)是一种原子操作,用于实现多线程并发控制。它通常用于解决并发环境下的数据竞争问题,特别是在多线程环境下对共享数据的原子更新。

    CAS 操作包含三个参数:一个内存位置(通常是一个地址),期望的原值和新值。它的行为是:如果这个位置的值等于期望的原值(说明p一直oldvalue没被其他thread改变过,安全的),则将这个位置的值更新为新值,否则不做任何操作。

    在伪代码中,CAS 操作通常表示为:

    bool CAS(地址 p, 期望值 oldval, 新值 newval) {
        if (*p == oldval) {
            *p = newval;
            return true;
        } else {
            return false;
        }
    }
    

    CAS 操作可以用来实现各种并发数据结构,例如无锁队列、无锁栈等,它**可以避免使用锁来保护共享资源,从而提高并发性能和减少线程阻塞。**在实际编程中,CAS 操作通常由硬件提供支持,也可以通过特定的库函数或语言扩展来实现。在 C++11 中,标准提供了 std::atomic_compare_exchange_* 等原子操作函数来支持 CAS 操作。

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值