本文介绍了多线程并发下为了避免临界资源被抢占而出现的错误,引入了锁和原子操作 来解决。
一、问题分析
创建10个线程,每个线程实现往总进程加1万个数。则总进程会达到10万
#include<stdio.h>
#include <unistd.h>
#include<pthread.h>
#define THREAD_COUNT 10
void *thread_callback(void* arg){
int *pcount=(int *)arg;
int i=0;
while(i++ <10000){
(*pcount)++;
//usleep(1)是一个系统调用函数,它的作用是让当前进程暂停执行一微秒(即等待一微秒
usleep(1);
}
}
int main(){
pthread_t threadid[THREAD_COUNT]={0};
int i=0;
int count=0;
//创建10个线程
for (i=0;i<THREAD_COUNT;i++){
pthread_create(&threadid[i],NULL,thread_callback,&count);
}
for (i=0;i<100;i++){
printf("count :%d\n",count);
//sleep(n)是一个系统调用函数,它的作用是让当前进程暂停执行n秒钟(即等待n秒钟)
sleep(1);
}
}
但实现过程中,会发现最后达不到100万。这种现象的原因在于count是一个临界资源,即多个线程共用一个变量。我们分析一下count++的汇编指令
mov count,eax;
inc eax;
mov eax,count;
实际中却可能出现如下异常情况
解决办法是加锁,即打包锁住count++的三个汇编指令,不可分开,其他线程不可抢占。
二、 锁和原子操作
把count的三条汇编指令变成一条指令,就不需要加锁。这种称为原子操作。
原子操作是指在并发环境下对共享资源进行访问或修改时,能够保证该操作具有不可分割性和独占性的一种操作方式。通常情况下,原子操作是由硬件级别提供支持的,可以确保在执行过程中不会被中断或者其他线程干扰,从而避免了竞态条件等问题的发生。
#include<stdio.h>
#include <unistd.h>
#include<pthread.h>
#define THREAD_COUNT 10
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
//对value加上add
int inc(int *value,int add){
int old;
//__asm__是GCC的内嵌汇编语言指令
__asm__ volatile(
"lock;xaddl %2,%1;"// %2的值+%1的值,赋给%1;同时,在执行该指令期间,CPU会对总线进行锁定(lock),以确保整个操作是原子性地完成。
: "=a" (old)
: "m" (*value),"a"(add)
: "cc","memory"
);
return old;
}
void *thread_callback(void* arg){
int *pcount=(int *)arg;
int i=0;
while(i++ <10000){
#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);
}
}
int main(){
pthread_t threadid[THREAD_COUNT]={0};
pthread_mutex_init(&mutex,NULL);//初始化mutex
pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);//初始化spinlock,进程之间共享
int i=0;
int count=0;
//创建10个线程
for (i=0;i<THREAD_COUNT;i++){
pthread_create(&threadid[i],NULL,thread_callback,&count);
}
for (i=0;i<100;i++){
printf("count :%d\n",count);
//sleep(n)是一个系统调用函数,它的作用是让当前进程暂停执行n秒钟(即等待n秒钟)
sleep(1);
}
}
三、相关知识点补充
1、创建进程
pthread_t是一个线程标识符,它可以唯一地标识一个线程。在多线程编程中,我们通常使用pthread_create()函数来创建一个新的线程,并且该函数会返回一个pthread_t类型的值,以便我们在后续代码中对这个新线程进行操作。
pthread_create()是一个函数,用于创建一个新的线程。其原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
该函数接受四个参数:
- thread:指向pthread_t类型变量的指针,用于存储新线程的标识符。
- attr:指向pthread_attr_t类型变量的指针,用于设置新线程的属性(如栈大小、优先级等)。如果为NULL,则使用默认属性。
- start_routine:指向新线程要执行的函数的指针。该函数必须返回void*类型,并且只能有一个参数,即对应于该函数调用时传递给它的参数。
- arg: 传递给start_routine函数的参数。
例如,下面是一个简单的例子,演示了如何使用pthread_create()来创建一个新线程:
#include <pthread.h>
#include <stdio.h>
void* thread_func(void *arg) {
printf("New thread created\n");
}
int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
printf("Error creating thread\n");
return -1;
}
// Wait for the new thread to complete
pthread_join(tid, NULL);
return 0;
}
在上述代码中,我们定义了一个名为“thread_func”的函数作为新线程要执行的内容。在主函数中,我们调用了pthread_create()函数来创建一个新线程,并将该函数的返回值存储到变量tid中。然后,我们通过调用pthread_join()函数等待新线程执行完毕。在新线程内部,我们只是简单地打印一条消息以表明它已经被创建成功了。
2,条件编译指令
#if 0
(*pcount)++;
#else
pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);
#endif
这是一个预处理器指令,用于在编译时控制代码的执行。如果 #if 0 部分是注释掉的代码或者不需要执行的代码,则可以使用 #if 0 来暂时禁用它们,以免浪费系统资源和时间。类似地,#elif 0 可以用来替代一部分被注释掉的代码或者不需要执行的代码,而 #else 则是当所有上面的条件都不满足时要执行的语句块。
使用条件编译指令的好处是可以在编译时根据预处理器定义的宏来控制代码的执行,而不需要手动注释或取消注释一大段代码。这种方法在调试和测试代码时非常有用,因为可以轻松地开启或关闭某些特定功能的实现,从而简化了代码的维护和调试。