线程的同步与互斥基本概念
一、案例的引出
银行卡有主卡和副卡之分,假如男孩、女孩同一时间、同一时刻取钱,可以看成两个线程。两个线程对同一银行卡同一资源进行操作,会导致的问题。
atm_account.c
#include "atm_account.h"
/** 创建账户 */
atm_Account *atm_account_Create(int code, double balance)
{
atm_Account *account = (atm_Account *)malloc(sizeof(atm_Account));
if(NULL == account) {
return NULL;
}
account->code = code;
account->balance = balance;
return account;
}
/** 销毁账户 */
void atm_account_Destroy(atm_Account *account)
{
if(NULL == account){
return ;
}
free(account);
}
/** 取款: 成功,则返回取款金额 */
double atm_account_Withdraw(atm_Account *account, double amt)
{
if(NULL == account) {
return 0.0;
}
if(amt < 0 || amt > account->balance) {
return 0.0;
}
double balance_tmp = account->balance;
sleep(1);
balance_tmp -= amt;
account->balance = balance_tmp;
return amt;
}
/** 存款: 返回存款的金额 */
double atm_account_Desposit(atm_Account *account, double amt)
{
if(NULL == account){
return 0.0;
}
if(amt < 0){
return 0.0;
}
double balance_tmp = account->balance;
sleep(1);
balance_tmp += amt;
account->balance = balance_tmp;
return amt;
}
/** 查看账户余额 */
double atm_account_BalanceGet(atm_Account *account)
{
if(NULL == account){
return 0.0;
}
double balance_tmp = account->balance;
return balance_tmp;
}
atm_account.h
#ifndef __ATM_ACCOUNT_H__
#define __ATM_ACCOUNT_H__
#include <math.h>
#include <malloc.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/** 账户信息 */
typedef struct {
int code; ///< 银行账户的编码
double balance; ///< 账户余额
}atm_Account;
/** 创建账户 */
extern atm_Account *atm_account_Create(int code, double balance);
/** 销毁账户 */
extern void atm_account_Destroy(atm_Account *account);
/** 取款 */
extern double atm_account_Withdraw(atm_Account *account, double amt);
/** 存款 */
extern double atm_account_Desposit(atm_Account *account, double amt);
/** 查看账户余额 */
extern double atm_account_BalanceGet(atm_Account *account);
#endif
atm_handler.c
#include "atm_handler.h"
/** 定义取款操作的线程运行函数 */
void *atm_handler_Withdraw(void *arg)
{
atm_handler_t *handler_tmp = (atm_handler_t *)arg;
double amt = atm_account_Withdraw(handler_tmp->account, handler_tmp->amt);
printf("%10s(0x%lu) withdraw %f from account %d\n", handler_tmp->name, pthread_self(), amt, handler_tmp->account->code);
return (void *)0;
}
/** 定义存款操作的线程运行函数 */
void *atm_handler_Desposit(void *arg)
{
atm_handler_t *handler_tmp = (atm_handler_t *)arg;
double amt = atm_account_Desposit(handler_tmp->account, handler_tmp->amt);
printf("%10s(0x%lu) deposit %f from account %d\n", handler_tmp->name, pthread_self(), amt, handler_tmp->account->code);
return (void *)0;
}
/** 定义检查银行账户的线程运行函数 */
void *atm_handler_AccountCheck(void *arg)
{
return (void *)0;
}
/** 账户操作主函数 */
atm_error_t atm_handler_main(void)
{
int err;
pthread_t boy, girl;
atm_Account *account = atm_account_Create(1000001, 10000);
if(NULL == account){
return ATM_ERROR_ACCOUNT_CREATE;
}
atm_handler_t usr1, usr2;
strcpy(usr1.name, "boy");
usr1.account = account;
usr1.amt = 10000;
strcpy(usr2.name, "girl");
usr2.account = account;
usr2.amt = 10000;
/** 启动两个线程(boy 和 girl 线程)同时去操作同一个银行账户 */
if((err = pthread_create(&boy, NULL, atm_handler_Withdraw, (void *)&usr1)) != 0) {
perror("pthread create error");
}
if((err = pthread_create(&girl, NULL, atm_handler_Withdraw, (void *)&usr2)) != 0) {
perror("pthread create error");
}
/** 主线程阻塞 */
pthread_join(boy, NULL);
pthread_join(girl, NULL);
printf("account balance: %f\n", atm_account_BalanceGet(account));
atm_account_Destroy(account);
return ATM_ERROR_NONE;
}
atm_handler.h
#ifndef __ATM_HANDLER_H__
#define __ATM_HANDLER_H__
#include "atm_account.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum {
ATM_ERROR_NONE,
ATM_ERROR_ACCOUNT_CREATE
}atm_error_t;
/** 账户操作结构体 */
typedef struct {
char name[20]; ///< 操作人的姓名
atm_Account *account; ///< 操作的账户
double amt; ///< 操作的金额
}atm_handler_t;
extern atm_error_t atm_handler_main(void);
#endif
atm_test.c
#include "atm_handler.h"
int main(void)
{
atm_handler_main();
return 0;
}
Makefile
#PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
PROJECT_ROOT = $(shell pwd)
SRC_DIR = $(PROJECT_ROOT)/src
INCLUDE_DIR = $(PROJECT_ROOT)/include
OBJ_DIR = $(PROJECT_ROOT)/obj
BIN_DIR = $(PROJECT_ROOT)/bin
# 找出 src 目录下的所有 .c 文件
C_SRCS = $(wildcard $(SRC_DIR)/*.c)
# 将所有的 src 下的 .c 文件替换为 .o 文件
C_OBJS = $(patsubst %c, %o, $(C_SRCS))
TARGET = test
SHARE_LIB = libatm.a
C_SRC_MAIN = atm_test.c
CC = gcc
CCFLAGS += fPIC
LDFLAGS += -shared -fPIC
ASFLAGS +=
ARFLAGS = -crs
LIBS_FLAGS = -L$(BIN_DIR)
RM = rm -rf
CFLAGS += -Wall -g -I$(INCLUDE_DIR)
INCDIR += -I$(INCLUDE_DIR)
.PHONY: all clean test
all: $(TARGET)
cp $(SHARE_LIB) $(BIN_DIR)
cp $(SRC_DIR)/*.o $(OBJ_DIR)/
$(RM) $(SHARE_LIB) $(SRC_DIR)/*.o
$(TARGET): $(SHARE_LIB)
$(CC) $(C_SRC_MAIN) -o $(TARGET) $(CFLAGS) $(INCDIR) $(LIBS_FLAGS) -latm -lpthread
$(SHARE_LIB): $(C_OBJS)
$(AR) $(ARFLAGS) $(SHARE_LIB) $(C_OBJS)
cp $(SHARE_LIB) $(BIN_DIR)
$(C_OBJS) : %.o:%.c
$(CC) -c $(CFLAGS) $(INCDIR) -o $@ $< -lpthread
clean:
$(RM) mshell
$(RM) $(SHARE_LIB)
$(RM) $(OBJ_DIR)/$(OBJS)/*.o
$(RM) $(SRC_DIR)/*.o
案例的运行结果可知,男孩女孩同时对一银行账户操作,会出现都取出10000.00的现象,余额总共才10000.00。这样的程序设计必定存在问题。存在对共享资源的访问问题。
使用GCC命令进行编译:把atm_test.c移至src文件夹,这样比较简单,不需要太复杂的Makefile。
gcc -o bin/account_test -Iinclude src/atm_account.c src/atm_test.c src/atm_handler.c -lpthread
二、线程的互斥
多个线程之间有共享资源(shared resource)时会出现互斥现象。
线程互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
假设有若干线程共享某个变量,而且都对变量有修改。如果它们之间不考虑相互协调工作,就会产生混乱。比如,线程A和B共用变量x,都对x执行增1操作。由于A和B没有协调,两线程对x的读取、修改和写入操作相互交叉,可能两个线程读取相同个x值,一个线程将修改后的x新值写入到x后,另一个线程也把自己对x修改后的新值写入到x。这样,x只记录后一个线程的修改作用。
临界段:多线程互斥使用共享资源的程序段,在操作系统中称为临界段。临界段是一种加锁的机制,与多线程共享资源有关。
临界段的作用是在任何时刻一个共享资源只能供一个线程使用。当资源未被占用,线程可以进入处理这个资源的临界段,从而得到该资源的使用权;当线程执行完毕,便退出临界段。如果一个线程已进入某个共享资源,并且还没有使用结束,其他线程必须等待。
多个线程中涉及到同一个临界资源的临界区称为相关临界区。线程进入临界区的调度原则是:
- 如果有若干线程要求进入空闲的临界区,一次仅允许一个线程进入。
- 任何时候,处于临界区内的线程不可多于一个。如已有线程进入自己的临界区,则其它所有试图进入临界区的线程必须等待。
- 进入临界区的线程要在有限时间内退出,以便其它线程能及时进入自己的临界区。
- 如果线程不能进入自己的临界区,则应让出CPU,避免线程出现“忙等”现象。
三、 线程的同步
多线程之间除了有互斥情况外,还有线程同步。线程同步:是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
可以这样理解:当线程A使用某个对象,而此对象又需要线程B修改后才能符合本线程的需要,此时线程A就要等待线程B完成修改工作。这种线程相互等待称为线程的同步。