本文分享自华为云社区《确保并发执行的安全性:探索多线程和锁机制以构建可靠的程序》,作者:Lion Long。
在当今计算机系统中,多线程编程已成为常见的需求,然而,同时也带来了并发执行的挑战。为了避免数据竞争和其他并发问题,正确使用适当的锁机制是至关重要的。通过阅读本文,读者将了解到多线程和锁机制在并发编程中的重要性,以及如何避免常见的并发问题,确保程序的安全性和可靠性。通过实际案例和代码示例来说明如何正确地使用多线程和锁机制来构建可靠的程序。
一、多线程的使用
1.1、线程的创建
函数原型:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_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()的返回。这将导致进程中所有线程的终止。
参数介绍:
返回值:
成功时,返回 0;出错时,它返回一个错误号,并且*thread 的内容未定义。
错误号:
其他:
新线程继承创建线程的信号掩码【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)
struct thread_info { /* Used as argument to thread_start() */
pthread_t thread_id; /* ID returned by pthread_create() */
int thread_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 */
static void *
thread_start(void *arg)
{
struct thread_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);
return uargv;
}
int
main(int argc, char *argv[])
{
int s, tnum, opt, num_threads;
struct thread_info *tinfo;
pthread_attr_t attr;
int stack_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(struct thread_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 into
corresponding 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 no
longer 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>
void pthread_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>
int pthread_cancel(pthread_t thread);
// 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>
int pthread_join(pthread_t thread, void **retval);
// Compile and link with -pthread.
复制代码
描述:
pthread_join()函数等待线程指定的线程终止。如果该线程已经终止,则 pthread_join()立即返回。thread 指定的线程必须是可连接的。
如果 retval 不为空,则 pthread_join()将目标线程的退出状态(即,目标线程提供给 pthrea_exit()的值)复制到 retval 所指向的位置。如果目标线程被取消,则 PTHREAD_CANCELED 被置于 retval 中。
如果多个线程同时尝试与同一线程联接,则结果是未定义的。如果调用 pthread_join()的线程被取消,那么目标线程将保持可连接状态(即,它不会被分离)。
返回值:成功时,返回 0;出错时,它返回错误号。
错误号:
1.4、线程的属性
函数原型:
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_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)
static void
display_pthread_attr(pthread_attr_t *attr, char *prefix)
{
int s, i;
size_t v;
void *stkaddr;
struct sched_param sp;
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);
}
static void *
thread_start(void *arg)
{
int s;
pthread_attr_t gattr;
/* pthread_getattr_np() is a non-standard GNU extension that
retrieves the attributes of the thread specified in its
first 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 */
}
int
main(int argc, char *argv[])
{
pthread_t thr;
pthread_attr_t attr;
pthread_attr_t *attrp; /* NULL or &attr */
int s;
attrp = NULL;
/* If a command-line argument was supplied, use it to set the
stack-size attribute and set a few other thread attributes,
and set attrp pointing to thread attributes object */
if (argc > 1) {
int stack_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 * 100000
void *func(void *arg) {
int *pcount = (int *)arg;
int i = 0;
while (i++ < 100000) {
(*pcount)++;
usleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t threadid[THREAD_SIZE] = { 0 };
int i = 0;
int count = 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);
}
return 0;
}
复制代码
上述代码执行结果理论上是 1000000,但是最后结果是 994656。也就是无原子操作下的执行结果小于理论值。
原因在于,执行 idx++时汇编代码是:
Mov [idx], %eax
Inc %eax
Mov %eax,[idx]
复制代码
也就是 c 语言是一条语句,但真正执行时是三条命令。在无原子操作时,就可能出现如下情况:
原意要自增两次,然而实际只自增了一次,因此无原子操作下的执行结果小于理论值。
三、互斥锁
让临界资源只允许在一个线程中执行。
pthread_mutex_init()
函数原型:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
复制代码
函数描述:
互斥锁的初始化。
pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数 attr 指定了新建互斥锁的属性。如果参数 attr 为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。
互斥锁的属性在创建锁的时候指定,在实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
返回:
成功会返回零,其他任何返回值都表示出现了错误。
成功后,互斥锁被初始化为未锁住态。
pthread_mutex_destroy()
用于注销一个互斥锁,函数原型:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex)
复制代码
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在 Linux 中,互斥锁并不占用任何资源,因此 pthread_mutex_destroy()仅仅检查锁状态(锁定状态则返回 EBUSY)。
pthread_mutex_lock()和 pthread_mutex_trylock()
函数原型:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
复制代码
描述:
互斥引用的互斥对象通过调用 pthread_mutex_lock()被锁定。如果互斥锁已被锁定,则调用线程将阻塞,直到互斥体变为可用。此操作将返回由处于锁定状态的互斥所引用的互斥对象,其中调用线程是其所有者。
函数 pthread_mutex_trylock()与 pthread_mutex_lock()相同,只是如果互斥引用的互斥对象当前被锁定(由任何线程,包括当前线程锁定),则调用将立即返回。
返回值:
如果成功,pthread_mutex_lock()和 pthread_mutex_unlock() 函数返回零。否则,将返回一个错误号以指示错误。
如果获取了互斥引用的互斥对象上的锁,则函数 pthread_mutex_trylock() 返回零。否则,将返回一个错误号以指示错误。
如果出现以下情况,pthread_mutex_lock()和 pthread_mutex_trylock()函数将失败:
这些函数不会返回错误代码 EINTR。
pthread_mutex_unlock()
函数原型:
#include <pthread.h>
int pthread_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_LOCK
pthread_mutex_t mutex;
#endif
// 10 * 100000
void *func(void *arg) {
int *pcount = (int *)arg;
int i = 0;
while (i++ < 100000) {
#if 0
(*pcount)++;
#elif ADD_MUTEX_LOCK
pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);
#endif
usleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t threadid[THREAD_SIZE] = { 0 };
#if ADD_MUTEX_LOCK
pthread_mutex_init(&mutex, NULL);
#endif
int i = 0;
int count = 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);
}
return 0;
}
复制代码
上述代码执行结果是 1000000。也就是互斥锁下的执行结果等于理论值。
五、自旋锁
自旋锁的接口和 mutex 类似。
函数原型:
#include <pthread.h>
// 1. 销毁自旋锁
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 2. 初始化自旋锁
int pthread_spin_init(pthread_spinlock_t *lock, int attr);
// 3. 自旋锁上锁(阻塞)
int pthread_spin_lock(pthread_spinlock_t *lock);
// 4. 自旋锁上锁(非阻塞)
int pthread_spin_trylock(pthread_spinlock_t *lock);
// 5. 自旋锁解锁
int pthread_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_LOCK
pthread_mutex_t mutex;
#endif
#if ADD_SPIN_LOCK
pthread_spinlock_t spinlock;
#endif
// 10 * 100000
void *func(void *arg) {
int *pcount = (int *)arg;
int i = 0;
while (i++ < 100000) {
#if 0
(*pcount)++;
#elif ADD_MUTEX_LOCK
pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);
#elif ADD_SPIN_LOCK
pthread_spin_lock(&spinlock);
(*pcount)++;
pthread_spin_unlock(&spinlock);
#endif
usleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t threadid[THREAD_SIZE] = { 0 };
#if ADD_MUTEX_LOCK
pthread_mutex_init(&mutex, NULL);
#elif ADD_SPIN_LOCK
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
#endif
int i = 0;
int count = 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);
}
return 0;
}
复制代码
上述代码执行结果是 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)
// 原子操作
int inc(int *value,int add)
{
int old;
__asm__ volatile(
"lock; xaddl %2, %1;"
: "=a" (old)
: "m" (*value),"a"(add)
: "cc","memory"
);
return old;
}
// 10 * 1000000
void *func(void *arg) {
int *pcount = (int *)arg;
int i = 0;
while (i++ < 1000000) {
inc(pcount, 1);
//usleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t threadid[THREAD_SIZE] = { 0 };
// 统计执行时间
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
int i = 0;
int count = 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);
}
#else
for (i = 0; i < THREAD_SIZE; i++) {
pthread_join(threadid[i], NULL); //
}
#endif
struct timeval tv_end;
gettimeofday(&tv_end, NULL);
int time_used = TIME_SUB_MS(tv_end, tv_start);
printf("time_used: %d\n", time_used);
return 0;
}
复制代码
总结
对临界资源操作时,常用原子操作和锁。
锁有互斥锁、自旋锁、读写锁等,其他应用程序实现的业务锁如悲观锁、乐观锁等。
在两种情况下容易陷入死锁:
(1)线程调用两次 lock,第一次已经获得锁,第二次发现锁已占用进入等待,而锁是被自己占用,进入无线等待的死锁。
(2)多个线程多个锁的情况,线程 1 获得锁 1 然后请求锁 2,线程 2 获得锁 2 然后请求锁 1,互相等待,进入锁。
原子操作就是通过一条指令解决问题,封装的 CAS 将多条执行命令变为一条执行命令,使其不可分割。
点击关注,第一时间了解华为云新鲜技术~
评论