LINUX 入门 3
day6 20240429 耗时:180min
课程链接地址
上一章作业题:如何实现按name首字母顺序存储,接口层业务层不改,只改支持层存储结构,不用struct链表,用哈希
第3章 LINUX环境编程——并发锁方案
C基础
-
<pthread.h>
- pthread_t:“pthread_t” 是 POSIX 线程库中定义的一种数据类型,用于表示线程标识符。在多线程编程中,每个线程都有一个唯一的标识符,可以用来控制、管理和操作线程。通常,使用 pthread 库创建线程时,会将线程的标识符存储在类型为 pthread_t 的变量中,并在需要时使用该标识符来执行线程相关的操作,如等待线程结束、取消线程等。
pthread_create(&threadid[i], NULL, thread_callback, &count);
&threadid[i]
:这是一个 pthread_t 类型的变量的地址,用于存储新创建的线程的标识符。i
可能是循环中的索引,用于跟踪多个线程的标识符。NULL
:这是一个指向线程属性的指针,通常可以使用 NULL 来使用默认属性。thread_callback
:这是一个函数指针,指向新线程将要执行的函数。线程被创建后,将会执行这个函数。 每个thread有自己callback&count
:主线程往子线程传的参数,这是一个指向某个数据的指针,是一个地址,它会被传递给线程回调函数thread_callback
的形参arg里面!!。这样,线程在执行时可以访问和操作这个数据。 count是被多个线程共享的资源,都可以计数
void* thread_callback(void *arg) {
- 函数参数 (
void *arg
):这是一个泛型指针,可以指向任何类型的数据。通常,它被用来传递数据到线程中,例如,如果你需要给线程传递一个或多个值,你可以定义一个结构体来包含这些值,然后传递一个指向这个结构体的指针。
- 函数参数 (
#include <unistd.h>
头文件,这样就会包含usleep
函数的声明,编译器就不会报错了
-
第二章
-
#if 0
是一种条件编译指令,它告诉预处理器忽略接下来的代码段,直到遇到对应的#endif
指令。这种用法通常用于临时注释掉一段代码,而不需要删除它们。在你的代码中,
#if 0
后面的代码段被注释掉了,因此编译器会忽略这部分代码,直到遇到匹配的#endif
指令。这样做可以方便地在调试或测试阶段临时禁用某些代码块,编译才会用到,而不需要删除它们。
-
-
第三章
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
PTHREAD_PROCESS_SHARED
: 这意味着多个进程可以使用这个锁来保护共享资源。 -
第四章
内联汇编是一种直接嵌入在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
指令。让我解释一下这段代码的作用和含义:lock; xaddl %2, %1;
:lock;
是指在执行指令时使用锁定总线,确保指令的原子性。xaddl %2, %1;
则是执行一个带有锁定的交换指令,将%2
(即add
变量的值)加到%1
所指向的内存地址的值上,并将结果存储到%1
所指向的内存地址中,同时返回旧的值到寄存器%eax
中。
: "=a" (old)
:- 表示使用寄存器
%eax
来存储返回的旧值old
。
- 表示使用寄存器
: "m" (*value), "a"(add)
:"m" (*value)
表示value
是一个内存操作数。"a"(add)
表示将add
的值放入寄存器%eax
中。
: "cc", "memory"
:"cc"
表示修改了条件代码寄存器(condition code register)。"memory"
表示内联汇编可能会对内存进行修改,因此需要通知编译器。
这段代码的作用是将
add
的值原子地加到value
所指向的内存地址中,并返回加法前的旧值。需要注意的是,内联汇编对底层硬件的操作细节非常敏感,一般情况下应当避免直接使用内联汇编,除非对硬件有深入了解并且确实需要优化特定的操作。
1 多线程并发锁的项目介绍
-
概念引入
- 临界资源:例子:买火车票,多个窗口共用一块资源,这里就是create函数里的count(都可以查座位号)
- 并发concurrency
- 多线程
-
code
10个火车票窗口——10个thread线程
定义thread标识符id——create thread——thread 的callback函数
-
编译
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
-
问题分析
-
**上一章问题:**最后卡在稳态很久971763,本来应该每个thread加10万,最后100万,但是不到 why???
-
核心代码是void* thread_back里的(*pcount) ++;
底层汇编
- mov [count], eax; //移count到寄存器
- inc eax; 自增
- mov eax, [count]
-
正常情况: 线程不乱跳
不正常:就是因为count是临界资源,但是底层汇编实现的寄存器eax并不是共用的临界资源,thread的eax都不一样,导致一旦乱跳的时候,基于eax的加操作白加
-
解决方案:对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,观察
}
-
什么时候mutex 什么时候spinlock
用锁保护对临界资源操作的内容 使临界资源只有一个thread可用
- Mutex(互斥锁):
- 当线程尝试获取一个被其他线程持有的互斥锁时,它会被放入睡眠状态(即阻塞),直到该锁可用为止。 会放弃CPU使用权
- 当线程成功获取到互斥锁后,其他线程必须等待该线程释放锁才能继续执行。
- Spinlock(自旋锁)
- 当线程尝试获取一个被其他线程持有的自旋锁时,它会在一个循环while(1)中不断地检查锁是否可用,而不会放弃CPU的使用权。核心区别
- 如果在短时间内可以获取到锁,自旋锁的效率可能会比互斥锁高,因为它避免了线程的上下文切换。
- 但是,如果锁的持有者长时间不释放锁,那么等待锁的线程会一直在循环中占用CPU资源,可能会导致性能下降。
当锁的内容少(临界区的持有时间短,线程不会长时间等待锁,切换CPU太耗时,不切了),用spinlock
锁的内容多(等待时间长,远大于线程切换时间,那就切换吧), mutex
- Mutex(互斥锁):
4 原子操作 atmoic
可不可以不加锁就把三条汇编CPU指令打包成一条CPU指令,就不会有中途跳来跳去情况
- mov [count], eax; //移count到寄存器
- inc eax; 自增
- 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区别
-
mutex:临界资源count被thread1占了,其他thread来了,直接切走CPU
-
spinlock:其他thread来了,等着直到释放,不切CPU
-
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 操作。