确保并发执行的安全性:探索多线程和锁机制以构建可靠的程序

简介: 在当今计算机系统中,多线程编程已成为常见的需求,然而,同时也带来了并发执行的挑战。为了避免数据竞争和其他并发问题,正确使用适当的锁机制是至关重要的。通过阅读本文,读者将了解到多线程和锁机制在并发编程中的重要性,以及如何避免常见的并发问题,确保程序的安全性和可靠性。通过实际案例和代码示例来说明如何正确地使用多线程和锁机制来构建可靠的程序。

一、多线程的使用

1.1、线程的创建

函数原型:

#include <pthread.h>intpthread_create(pthread_t*thread, constpthread_attr_t*attr,
void*(*start_routine) (void*), void*arg);
// Compile and link with -pthread.

描述:

pthread_create()函数在调用进程中启动一个新线程。新线程通过调用start_routine()开始执行;arg作为start_routine()的唯一参数传递。

新线程以以下方式之一终止:

(1)它调用pthread_exit(),指定一个退出状态值,该值可用于调用pthrread_join()的同一进程中的另一个线程,即pthrread_join()可以接收pthread_exit()返回的值。

(2)它从start_routine()返回。这相当于使用return语句中提供的值调用pthread_exit()。

(3)它被pthread_cancel()取消。

(4)进程中的任何线程都调用exit(),或者主线程执行main()的返回。这将导致进程中所有线程的终止。

参数介绍:

参数 含义
attr attr参数指向pthread_attr_t结构,其内容在线程创建时用于确定新线程的属性;使用pthread_attr_init()和相关函数初始化该结构。如果attr为空,则使用默认属性创建线程。
thread 在返回之前,成功调用pthread_create()将新线程的ID存储在thread指向的缓冲区中;此标识符用于在后续调用其他pthreads函数时引用线程。
start_routine 线程入口函数
arg 线程入口函数的参数

返回值:

成功时,返回0;出错时,它返回一个错误号,并且*thread的内容未定义。

错误号:

错误号 含义
EAGAIN 资源不足,无法创建另一个线程。
AGAIN A 遇到系统对线程数量施加的限制。可能触发此错误的限制有很多:已达到RLIMIT_NPROC软资源限制【通过setrlimit()设置】,该限制限制了真实用户ID的进程和线程数;已达到内核对进程和线程数的系统范围限制,即/proc/sys/kernel/threads max【请参阅proc()】;或者达到最大pid数/proc/sys/kernel/pid_max【见proc()】。
EINVAL 属性中的设置无效。
EPERM 没有设置attr中指定的调度策略和参数的权限。

其他:

新线程继承创建线程的信号掩码【pthread_sigmask()】的副本。新线程的挂起信号集为空【sigpending()】。新线程不继承创建线程的备用信号堆栈【sigaltstack()】。

新线程的CPU时间时钟的初始值为0【参见pthread_getcpuclockid()】。

示例代码:

#include <pthread.h>#include <string.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>#include <ctype.h>#define handle_error_en(en, msg) \do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)#define handle_error(msg) \do { perror(msg); exit(EXIT_FAILURE); } while (0)structthread_info {    /* Used as argument to thread_start() */pthread_tthread_id;        /* ID returned by pthread_create() */intthread_num;       /* Application-defined thread # */char*argv_string;      /* From command-line argument */};
/* Thread start function: display address near top of our stack,and return upper-cased copy of argv_string */staticvoid*thread_start(void*arg)
{
structthread_info*tinfo=arg;
char*uargv, *p;
printf("Thread %d: top of stack near %p; argv_string=%s\n",
tinfo->thread_num, &p, tinfo->argv_string);
uargv=strdup(tinfo->argv_string);
if (uargv==NULL)
handle_error("strdup");
for (p=uargv; *p!='\0'; p++)
*p=toupper(*p);
returnuargv;
}
intmain(intargc, char*argv[])
{
ints, tnum, opt, num_threads;
structthread_info*tinfo;
pthread_attr_tattr;
intstack_size;
void*res;
/* The "-s" option specifies a stack size for our threads */stack_size=-1;
while ((opt=getopt(argc, argv, "s:")) !=-1) {
switch (opt) {
case's':
stack_size=strtoul(optarg, NULL, 0);
break;
default:
fprintf(stderr, "Usage: %s [-s stack-size] arg...\n",
argv[0]);
exit(EXIT_FAILURE);
        }
    }
num_threads=argc-optind;
/* Initialize thread creation attributes */s=pthread_attr_init(&attr);
if (s!=0)
handle_error_en(s, "pthread_attr_init");
if (stack_size>0) {
s=pthread_attr_setstacksize(&attr, stack_size);
if (s!=0)
handle_error_en(s, "pthread_attr_setstacksize");
    }
/* Allocate memory for pthread_create() arguments */tinfo=calloc(num_threads, sizeof(structthread_info));
if (tinfo==NULL)
handle_error("calloc");
/* Create one thread for each command-line argument */for (tnum=0; tnum<num_threads; tnum++) {
tinfo[tnum].thread_num=tnum+1;
tinfo[tnum].argv_string=argv[optind+tnum];
/* The pthread_create() call stores the thread ID intocorresponding element of tinfo[] */s=pthread_create(&tinfo[tnum].thread_id, &attr,
&thread_start, &tinfo[tnum]);
if (s!=0)
handle_error_en(s, "pthread_create");
    }
/* Destroy the thread attributes object, since it is nolonger needed */s=pthread_attr_destroy(&attr);
if (s!=0)
handle_error_en(s, "pthread_attr_destroy");
/* Now join with each thread, and display its returned value */for (tnum=0; tnum<num_threads; tnum++) {
s=pthread_join(tinfo[tnum].thread_id, &res);
if (s!=0)
handle_error_en(s, "pthread_join");
printf("Joined with thread %d; returned value was %s\n",
tinfo[tnum].thread_num, (char*) res);
free(res);      /* Free memory allocated by thread */    }
free(tinfo);
exit(EXIT_SUCCESS);
}

1.2、线程的终止

新线程以以下方式之一终止:

(1)它调用pthread_exit(),指定一个退出状态值,该值可用于调用pthrread_join()的同一进程中的另一个线程,即pthrread_join()可以接收pthread_exit()返回的值。

(2)它从start_routine()返回。这相当于使用return语句中提供的值调用pthread_exit()。

(3)它被pthread_cancel()取消。

(4)进程中的任何线程都调用exit(),或者主线程执行main()的返回。这将导致进程中所有线程的终止。

pthread_exit()函数原型:

#include <pthread.h>voidpthread_exit(void*retval);
// Compile and link with -pthread.

描述:

(1)pthread_exit()函数终止调用线程并通过retval返回一个值,该值(如果线程是可连接的)可用于调用pthrea_join()的同一进程中的另一个线程,即可被pthrea_join()接收返回值。

(2)任何由pthread_cleanup_push()建立的尚未弹出的清理处理程序都会弹出(与它们被推送的顺序相反)并执行。如果线程具有任何特定于线程的数据,则在执行清理处理程序后,将以未指定的顺序调用相应的析构函数。

(3)当线程终止时,进程共享资源(例如互斥体、条件变量、信号量和文件描述符)不会被释放,使用atexit()注册的函数也不会被调用。

(4)进程中的最后一个线程终止后,进程通过调用exit()终止,退出状态为零;因此,释放进程共享资源并调用使用atexit()注册的函数。

返回值:此函数不返回调用方。

错误:此函数始终成功。

注意:

(1)从除主线程之外的任何线程的start函数执行返回将导致隐式调用pthread_exit(),使用函数的返回值作为线程的退出状态。

(2)为了允许其他线程继续执行,主线程应该通过调用pthread_exit()而不是exit()来终止。

(3)retval指向的值不应位于调用线程的堆栈上,因为该堆栈的内容在线程终止后未定义。

pthread_cancel()函数原型:

#include <pthread.h>intpthread_cancel(pthread_tthread);
// Compile and link with -pthread.

描述:

pthread_cancel()函数向线程thread发送取消请求。目标线程是否以及何时响应取消请求取决于该线程控制的两个属性:其可取消性state和type。

由pthread_setcancelstate()设置线程的可取消状态可以启用(新线程的默认状态)或禁用。如果线程已禁用取消,则取消请求将保持排队状态,直到线程启用取消。如果线程已启用取消,则其可取消性类型决定何时取消。

由pthread_setcanceltype()确定的线程的取消类型可以是异步的或延迟的(新线程的默认值)。异步可取消性意味着线程可以随时取消(通常是立即取消,但系统不保证)。延迟可取消性意味着取消将被延迟,直到线程下一次调用作为取消点的函数。pthreads()中提供了作为或可能是取消点的函数列表。

执行取消请求时,线程将执行以下步骤(按顺序):

上述步骤相对于pthread_cancel()调用异步发生;pthread_cancel()的返回状态仅通知调用方取消请求是否已成功排队。

被取消的线程终止后,使用pthread_join()与该线程的连接将获得pthrea_canceled作为线程的退出状态。(使用线程连接是知道取消已完成的唯一方法。)

返回值:成功时,返回0;出错时,返回非零错误号。

错误:ESRCH,找不到ID为thread的线程。

1.3、线程的等待

函数原型:

#include <pthread.h>intpthread_join(pthread_tthread, void**retval);
// Compile and link with -pthread.

描述:

pthread_join()函数等待线程指定的线程终止。如果该线程已经终止,则pthread_join()立即返回。thread指定的线程必须是可连接的。

如果retval不为空,则pthread_join()将目标线程的退出状态(即,目标线程提供给pthrea_exit()的值)复制到retval所指向的位置。如果目标线程被取消,则PTHREAD_CANCELED被置于retval中。

如果多个线程同时尝试与同一线程联接,则结果是未定义的。如果调用pthread_join()的线程被取消,那么目标线程将保持可连接状态(即,它不会被分离)。

返回值:成功时,返回0;出错时,它返回错误号。

错误号:

错误号 含义
EDEADLK 检测到死锁(例如,两个线程试图彼此连接);或thread指定调用线程。
EINVAL 线程不是可连接线程。
EINVAL 另一个线程已在等待加入此线程。
ESRCH 找不到ID为线程的线程。

1.4、线程的属性

函数原型:

#include <pthread.h>intpthread_attr_init(pthread_attr_t*attr);
intpthread_attr_destroy(pthread_attr_t*attr);
// Compile and link with -pthread.

描述:

pthread_attr_init()函数使用默认属性值初始化attr指向的线程属性对象。在这个调用之后,可以使用各种相关函数(下方列出)设置对象的各个属性,然后可以在创建线程的一个或多个pthread_create()调用中使用该对象。

pthread_attr_setaffinity_np(), 
pthread_attr_setdetachstate(), 
pthread_attr_setguardsize(), 
pthread_attr_setinheritsched(), 
pthread_attr_setschedparam(), 
pthread_attr_setschedpolicy(),
pthread_attr_setscope(), 
pthread_attr_setstack(), 
pthread_attr_setstackaddr(), 
pthread_attr_setstacksize(), 
pthread_create(), 
pthread_getattr_np(), 
pthreads()

对已初始化的线程属性对象调用pthread_attr_init()会导致未定义的行为。

当不再需要线程属性对象时,应使用pthread_attr_destroy()函数将其销毁。 销毁线程属性对象对使用该对象创建的线程没有影响。

线程属性对象被销毁后,可以使用pthread_attr_init()对其重新初始化。任何其他使用已销毁线程属性对象的方法都会产生未定义的结果。

返回值:

成功时,这些函数返回0;出错时,它们返回一个非零错误号。

错误:

在Linux上,这些函数总是成功的(但可移植和未来验证的应用程序应该处理可能的错误返回)。

pthread_attr_t类型应被视为不透明的:除通过pthreads函数外,对对象的任何访问都是不可移植的,并产生未定义的结果。

示例代码:

#define _GNU_SOURCE     /* To get pthread_getattr_np() declaration */#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>#define handle_error_en(en, msg) \do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)staticvoiddisplay_pthread_attr(pthread_attr_t*attr, char*prefix)
{
ints, i;
size_tv;
void*stkaddr;
structsched_paramsp;
s=pthread_attr_getdetachstate(attr, &i);
if (s!=0)
handle_error_en(s, "pthread_attr_getdetachstate");
printf("%sDetach state        = %s\n", prefix,
            (i==PTHREAD_CREATE_DETACHED) ?"PTHREAD_CREATE_DETACHED" :
            (i==PTHREAD_CREATE_JOINABLE) ?"PTHREAD_CREATE_JOINABLE" :
"???");
s=pthread_attr_getscope(attr, &i);
if (s!=0)
handle_error_en(s, "pthread_attr_getscope");
printf("%sScope               = %s\n", prefix,
            (i==PTHREAD_SCOPE_SYSTEM)  ?"PTHREAD_SCOPE_SYSTEM" :
            (i==PTHREAD_SCOPE_PROCESS) ?"PTHREAD_SCOPE_PROCESS" :
"???");
s=pthread_attr_getinheritsched(attr, &i);
if (s!=0)
handle_error_en(s, "pthread_attr_getinheritsched");
printf("%sInherit scheduler   = %s\n", prefix,
            (i==PTHREAD_INHERIT_SCHED)  ?"PTHREAD_INHERIT_SCHED" :
            (i==PTHREAD_EXPLICIT_SCHED) ?"PTHREAD_EXPLICIT_SCHED" :
"???");
s=pthread_attr_getschedpolicy(attr, &i);
if (s!=0)
handle_error_en(s, "pthread_attr_getschedpolicy");
printf("%sScheduling policy   = %s\n", prefix,
            (i==SCHED_OTHER) ?"SCHED_OTHER" :
            (i==SCHED_FIFO)  ?"SCHED_FIFO" :
            (i==SCHED_RR)    ?"SCHED_RR" :
"???");
s=pthread_attr_getschedparam(attr, &sp);
if (s!=0)
handle_error_en(s, "pthread_attr_getschedparam");
printf("%sScheduling priority = %d\n", prefix, sp.sched_priority);
s=pthread_attr_getguardsize(attr, &v);
if (s!=0)
handle_error_en(s, "pthread_attr_getguardsize");
printf("%sGuard size          = %d bytes\n", prefix, v);
s=pthread_attr_getstack(attr, &stkaddr, &v);
if (s!=0)
handle_error_en(s, "pthread_attr_getstack");
printf("%sStack address       = %p\n", prefix, stkaddr);
printf("%sStack size          = 0x%zx bytes\n", prefix, v);
}
staticvoid*thread_start(void*arg)
{
ints;
pthread_attr_tgattr;
/* pthread_getattr_np() is a non-standard GNU extension thatretrieves the attributes of the thread specified in itsfirst argument */s=pthread_getattr_np(pthread_self(), &gattr);
if (s!=0)
handle_error_en(s, "pthread_getattr_np");
printf("Thread attributes:\n");
display_pthread_attr(&gattr, "\t");
exit(EXIT_SUCCESS);         /* Terminate all threads */}
intmain(intargc, char*argv[])
{
pthread_tthr;
pthread_attr_tattr;
pthread_attr_t*attrp;      /* NULL or &attr */ints;
attrp=NULL;
/* If a command-line argument was supplied, use it to set thestack-size attribute and set a few other thread attributes,and set attrp pointing to thread attributes object */if (argc>1) {
intstack_size;
void*sp;
attrp=&attr;
s=pthread_attr_init(&attr);
if (s!=0)
handle_error_en(s, "pthread_attr_init");
s=pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (s!=0)
handle_error_en(s, "pthread_attr_setdetachstate");
s=pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
if (s!=0)
handle_error_en(s, "pthread_attr_setinheritsched");
stack_size=strtoul(argv[1], NULL, 0);
s=posix_memalign(&sp, sysconf(_SC_PAGESIZE), stack_size);
if (s!=0)
handle_error_en(s, "posix_memalign");
printf("posix_memalign() allocated at %p\n", sp);
s=pthread_attr_setstack(&attr, sp, stack_size);
if (s!=0)
handle_error_en(s, "pthread_attr_setstack");
    }
s=pthread_create(&thr, attrp, &thread_start, NULL);
if (s!=0)
handle_error_en(s, "pthread_create");
if (attrp!=NULL) {
s=pthread_attr_destroy(attrp);
if (s!=0)
handle_error_en(s, "pthread_attr_destroy");
    }
pause();    /* Terminates when other thread calls exit() */}

二、无原子操作

在多个线程中,对一个变量不断操作,如果没有原子操作会怎么样?

示例代码:

#include <stdio.h>#include <pthread.h>#include <unistd.h>#define THREAD_SIZE     10// 10 * 100000void*func(void*arg) {
int*pcount= (int*)arg;
inti=0;
while (i++<100000) {
        (*pcount)++;
usleep(1);
    }
}
intmain(intargc, char**argv)
{
pthread_tthreadid[THREAD_SIZE] = { 0 };
inti=0;
intcount=0;
for (i=0; i<THREAD_SIZE; i++) {
pthread_create(&threadid[i], NULL, func, &count);
    }
// 1000w for (i=0; i<10; i++) {
printf("count = %d\n", count);
sleep(1);
    }
return0;
}

上述代码执行结果理论上是1000000,但是最后结果是994656。也就是无原子操作下的执行结果小于理论值。

原因在于,执行idx++时汇编代码是:

Mov [idx], %eaxInc%eaxMov%eax,[idx]

也就是c语言是一条语句,但真正执行时是三条命令。在无原子操作时,就可能出现如下情况:

原意要自增两次,然而实际只自增了一次,因此无原子操作下的执行结果小于理论值。

三、互斥锁

让临界资源只允许在一个线程中执行。

pthread_mutex_init()

函数原型:

#include <pthread.h>intpthread_mutex_init(pthread_mutex_t*restrictmutex,constpthread_mutexattr_t*restrictattr);

函数描述:

互斥锁的初始化。

pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。

互斥锁的属性在创建锁的时候指定,在实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。

返回:

成功会返回零,其他任何返回值都表示出现了错误。

成功后,互斥锁被初始化为未锁住态。

pthread_mutex_destroy()

用于注销一个互斥锁,函数原型:

#include <pthread.h>intpthread_mutex_destroy(pthread_mutex_t*mutex)

销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此pthread_mutex_destroy()仅仅检查锁状态(锁定状态则返回EBUSY)。

pthread_mutex_lock()和pthread_mutex_trylock()

函数原型:

#include <pthread.h>intpthread_mutex_lock(pthread_mutex_t*mutex);
intpthread_mutex_trylock(pthread_mutex_t*mutex);

描述:

互斥引用的互斥对象通过调用 pthread_mutex_lock()被锁定。如果互斥锁已被锁定,则调用线程将阻塞,直到互斥体变为可用。此操作将返回由处于锁定状态的互斥所引用的互斥对象,其中调用线程是其所有者。

函数 pthread_mutex_trylock()与 pthread_mutex_lock()相同,只是如果互斥引用的互斥对象当前被锁定(由任何线程,包括当前线程锁定),则调用将立即返回。

互斥类型 含义
PTHREAD_MUTEX_NORMAL 不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果线程尝试解锁它尚未锁定的互斥锁或已解锁的互斥体,则会导致未定义的行为。
PTHREAD_MUTEX_ERRORCHECK 提供错误检查。如果线程尝试重新锁定已锁定的互斥锁,则会返回错误。如果线程尝试解锁尚未锁定的互斥体或已解锁的互斥体,则将返回错误。
PTHREAD_MUTEX_RECURSIVE 互斥锁将保留锁定计数的概念。当线程首次成功获取互斥锁时,锁定计数将设置为 1。每次线程重新锁定此互斥锁时,锁定计数都会递增 1。每次线程解锁互斥体时,锁定计数都会减少 1。当锁定计数达到零时,互斥锁将可供其他线程获取。如果线程尝试解锁尚未锁定的互斥体或已解锁的互斥体,则将返回错误。
PTHREAD_MUTEX_DEFAULT 尝试递归锁定互斥会导致未定义的行为。如果互斥体未被调用线程锁定,则尝试解锁该互斥体会导致未定义的行为。如果互斥体未锁定,则尝试解锁互斥体会导致未定义的行为。

返回值:

如果成功,pthread_mutex_lock()和 pthread_mutex_unlock() 函数返回零。否则,将返回一个错误号以指示错误。

如果获取了互斥引用的互斥对象上的锁,则函数 pthread_mutex_trylock() 返回零。否则,将返回一个错误号以指示错误。

如果出现以下情况,pthread_mutex_lock()和pthread_mutex_trylock()函数将失败:

错误代码 含义
EINVAL 互斥体是使用具有值PTHREAD_PRIO_PROTECT的协议属性创建的,并且调用线程的优先级高于互斥体的当前优先级上限。
EBUSY 无法获取互斥体,因为它已被锁定。
EINVAL 互斥体指定的值不引用初始化的互斥体对象。
EAGAIN 无法获取互斥锁,因为已超过互斥锁的最大递归锁数。
EDEADLK 当前线程已拥有互斥体。
EPERM 当前线程不拥有互斥体。

这些函数不会返回错误代码EINTR。

pthread_mutex_unlock()

函数原型:

#include <pthread.h>intpthread_mutex_unlock(pthread_mutex_t*mutex);

描述:

pthread_mutex_unlock() 函数释放互斥引用的互斥对象。释放互斥体的方式取决于互斥体的 type 属性。如果在调用 pthread_mutex_unlock()时,互斥所引用的互斥对象上存在阻塞的线程,从而导致互斥体变为可用,则调度策略用于确定哪个线程应获取互斥。(在PTHREAD_MUTEX_RECURSIVE互斥锁的情况下,当计数达到零并且调用线程不再对此互斥锁时,互斥锁将变为可用)。

如果信号被传递到等待互斥体的线程,则在信号处理程序返回时,线程将恢复等待互斥体,就好像它没有被中断一样。

返回值:

如果成功,返回零。否则,将返回一个错误号以指示错误。

示例代码

#include <stdio.h>#include <pthread.h>#include <unistd.h>#define THREAD_SIZE     10#define ADD_MUTEX_LOCK  1#if ADD_MUTEX_LOCKpthread_mutex_tmutex;
#endif// 10 * 100000void*func(void*arg) {
int*pcount= (int*)arg;
inti=0;
while (i++<100000) {
#if 0        (*pcount)++;
#elif ADD_MUTEX_LOCKpthread_mutex_lock(&mutex);
        (*pcount)++;
pthread_mutex_unlock(&mutex);
#endifusleep(1);
    }
}
intmain(intargc, char**argv)
{
pthread_tthreadid[THREAD_SIZE] = { 0 };
#if ADD_MUTEX_LOCKpthread_mutex_init(&mutex, NULL);
#endifinti=0;
intcount=0;
for (i=0; i<THREAD_SIZE; i++) {
pthread_create(&threadid[i], NULL, func, &count);
    }
// 1000w for (i=0; i<50; i++) {
printf("count = %d\n", count);
sleep(1);
    }
return0;
}

上述代码执行结果是1000000。也就是互斥锁下的执行结果等于理论值。

五、自旋锁

自旋锁的接口和mutex类似。

函数原型:

#include <pthread.h>// 1. 销毁自旋锁intpthread_spin_destroy(pthread_spinlock_t*lock);
// 2. 初始化自旋锁intpthread_spin_init(pthread_spinlock_t*lock, intattr);
// 3. 自旋锁上锁(阻塞)intpthread_spin_lock(pthread_spinlock_t*lock);
// 4. 自旋锁上锁(非阻塞)intpthread_spin_trylock(pthread_spinlock_t*lock);
// 5. 自旋锁解锁intpthread_spin_unlock(pthread_spinlock_t*lock);
以上函数成功都返回0.

示例代码

#include <stdio.h>#include <pthread.h>#include <unistd.h>#define THREAD_SIZE     10#define ADD_MUTEX_LOCK  0#define ADD_SPIN_LOCK   1#if ADD_MUTEX_LOCKpthread_mutex_tmutex;
#endif#if ADD_SPIN_LOCKpthread_spinlock_tspinlock;
#endif// 10 * 100000void*func(void*arg) {
int*pcount= (int*)arg;
inti=0;
while (i++<100000) {
#if 0        (*pcount)++;
#elif ADD_MUTEX_LOCKpthread_mutex_lock(&mutex);
        (*pcount)++;
pthread_mutex_unlock(&mutex);
#elif ADD_SPIN_LOCKpthread_spin_lock(&spinlock);
        (*pcount)++;
pthread_spin_unlock(&spinlock);
#endifusleep(1);
    }
}
intmain(intargc, char**argv)
{
pthread_tthreadid[THREAD_SIZE] = { 0 };
#if ADD_MUTEX_LOCKpthread_mutex_init(&mutex, NULL);
#elif ADD_SPIN_LOCKpthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
#endifinti=0;
intcount=0;
for (i=0; i<THREAD_SIZE; i++) {
pthread_create(&threadid[i], NULL, func, &count);
    }
// 1000w for (i=0; i<50; i++) {
printf("count = %d\n", count);
sleep(1);
    }
return0;
}

上述代码执行结果是1000000。也就是自旋锁下的执行结果等于理论值。

互斥锁与自旋锁的区别:

  • 互斥锁与自旋锁的接口类似,但是底层实现有一定差异。
  • mutex在发现锁已经被占用时,会让出CPU资源,然后等待有解锁时唤醒去抢锁。
  • spin在发现锁已经被占用时,会一直等着,直到抢到锁。

死锁,死锁的两种情况:

(1)如果两个线程先后调用两次lock,第二次调用lock时,由于锁已被占用,该线程会挂起等待别的线程释放锁,然后锁正是被自己占用着的,该线程又被挂起不能释放锁,因此就永远处于挂起等待状态了,进入死锁。

(2)线程1和线程2。线程1获得锁1,线程2获得锁2,此时线程1调用lock企图获得锁2,结果是需要挂起等待线程2释放锁2,而此时线程2也调用了lock企图获得锁1,结果是线程2挂起等待线程1释放锁1,进入死锁。

避免死锁:

(1)共享资源操作前一定要获得锁。

(2)完成操作以后一定要释放锁。

(3)尽量短时间地占用锁。

(4)有多锁, 如获得顺序是abc连环扣, 释放顺序也应该是abc。

(5)线程错误返回时应该释放它所获得的锁。

(6)写程序是尽量避免同时获得多个锁。如果一定要这么做,所有线程在需要多个锁时都按相同的先后顺序获得锁,则不会出现死锁。

六、原子操作

原子操作就是用一条指令解决问题;多条执行命令变为一条执行命令,使其不可分割。

CAS,全称Compare And Swap。翻译过来就是先比较再赋值,顺序不可变;也就是先对比,如果值一致再赋值,如果不一致就不赋值。

常见的原子操作:

(1)加,add

(2)减,sub

(3)自增,inc

(4)自减,dec

(5)比较赋值,cas

注意,c语言的一条语句执行可能有副作用,但原子操作是没有副作用的。

示例代码:

#include <stdio.h>#include <pthread.h>#include <unistd.h>#define THREAD_SIZE     10#include <sys/time.h>#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)// 原子操作intinc(int*value,intadd) 
{
intold;
__asm__volatile(
"lock; xaddl %2, %1;"        : "=a" (old)
        : "m" (*value),"a"(add)
        : "cc","memory"        );
returnold;
}
// 10 * 1000000void*func(void*arg) {
int*pcount= (int*)arg;
inti=0;
while (i++<1000000) {
inc(pcount, 1);
//usleep(1);    }
}
intmain(intargc, char**argv)
{
pthread_tthreadid[THREAD_SIZE] = { 0 };
// 统计执行时间structtimevaltv_start;
gettimeofday(&tv_start, NULL);
inti=0;
intcount=0;
for (i=0; i<THREAD_SIZE; i++) {
pthread_create(&threadid[i], NULL, func, &count);
    }
#if 0// 1000w for (i=0; i<50; i++) {
printf("count = %d\n", count);
sleep(1);
    }
#elsefor (i=0; i<THREAD_SIZE; i++) {
pthread_join(threadid[i], NULL); //    }
#endifstructtimevaltv_end;
gettimeofday(&tv_end, NULL);
inttime_used=TIME_SUB_MS(tv_end, tv_start);
printf("time_used: %d\n", time_used);
return0;
}

总结

对临界资源操作时,常用原子操作和锁。

锁有互斥锁、自旋锁、读写锁等,其他应用程序实现的业务锁如悲观锁、乐观锁等。

在两种情况下容易陷入死锁:

(1)线程调用两次lock,第一次已经获得锁,第二次发现锁已占用进入等待,而锁是被自己占用,进入无线等待的死锁。

(2)多个线程多个锁的情况,线程1获得锁1然后请求锁2,线程2获得锁2然后请求锁1,互相等待,进入锁。

原子操作就是通过一条指令解决问题,封装的CAS将多条执行命令变为一条执行命令,使其不可分割。

欢迎关注公众号《Lion 莱恩呀》学习技术,每日推送文章。

目录
相关文章
|
1月前
|
资源调度 算法 Linux
Linux进程/线程的调度机制介绍:详细解析Linux系统中进程/线程的调度优先级规则
Linux进程/线程的调度机制介绍:详细解析Linux系统中进程/线程的调度优先级规则
101 0
|
2天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
8天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
9天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
10天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
7 0
|
11天前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
14 2
|
12天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
43 2
|
13天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
14天前
|
Java API 调度
安卓多线程和并发处理:提高应用效率
【4月更文挑战第13天】本文探讨了安卓应用中多线程和并发处理的优化方法,包括使用Thread、AsyncTask、Loader、IntentService、JobScheduler、WorkManager以及线程池。此外,还介绍了RxJava和Kotlin协程作为异步编程工具。理解并恰当运用这些技术能提升应用效率,避免UI卡顿,确保良好用户体验。随着安卓技术发展,更高级的异步处理工具将助力开发者构建高性能应用。
|
21天前
|
安全 Java 调度
深入理解Java中的线程安全与锁机制
【4月更文挑战第6天】 在并发编程领域,Java语言提供了强大的线程支持和同步机制来确保多线程环境下的数据一致性和线程安全性。本文将深入探讨Java中线程安全的概念、常见的线程安全问题以及如何使用不同的锁机制来解决这些问题。我们将从基本的synchronized关键字开始,到显式锁(如ReentrantLock),再到读写锁(ReadWriteLock)的讨论,并结合实例代码来展示它们在实际开发中的应用。通过本文,读者不仅能够理解线程安全的重要性,还能掌握如何有效地在Java中应用各种锁机制以保障程序的稳定运行。

相关实验场景

更多