赞
踩
大家都能看到的资源:公共资源
多线程中,如果有一个全局的变量,这个变量是被所有执行流共享的。
线程大部分资源都会直接或间接共享,而只要存在共享,就可能存在并发访问的问题。
任何一个时刻,都只允许一个执行流再进行流在进行访问的共享资源,叫做临界资源。
所以要对临界资源进行一定的保护。
临界资源是要通过代码访问的,凡是访问临界资源的代码,叫做临界区。
如果想让多个线程安全的访问临界资源,就可以采用加锁,也就是互斥访问。
任何一个时刻,都只允许一个执行流再进行共享资源的访问 (可通过加锁解决)。本质就是将临界资源独立使用。
下面一个小实验没有对临界资源做保护,存在并发访问的问题:
- //模拟抢票
-
- #include <iostream>
- #include <string>
- #include <pthread.h>
- #include <unistd.h>
- using namespace std;
-
- int tickets = 10000; //临界资源
-
- void* ThreadRoutine(void* args)
- {
- string name = static_cast<const char*>(args);
-
- while(true)
- {
- if(tickets > 0) //临界区
- {
- usleep(2000); //模拟抢票所花费时间
- cout << name << " get ticket: " << tickets-- << endl; //临界区,因为对数据进行++/--操作并不是原子的,所以可能存在并发访问问题。
-
- }
- else
- {
- break;
- }
- usleep(1000);
- }
-
-
- return nullptr;
- }
-
- int main()
- {
- //创建4个进程
- pthread_t tids[4];
- int size = sizeof(tids) / sizeof(tids[0]);
- for(int i = 0; i < size; i++)
- {
- char *name = new char[64];
- snprintf(name, 64, "thread-%d", i + 1);
- pthread_create(tids+i, nullptr, ThreadRoutine, name);
-
- }
-
- for(int i = 0; i < size; i++)
- {
- pthread_join(tids[i], nullptr);
- }
-
- return 0;
- }

可以看见没有对临界区做保护,票被买成了负数。
为什么可能无法获得正确结果?
if
语句判断条件为真以后,代码可以并发的切换到其他线程usleep
这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段--ticket
操作本身就不是一个原子操作-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中 update : 更新寄存器里面的值,执行-1操作 store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
初始化
动态分配
- #include <pthread.h>
- int pthread_mutex_init(pthread_mutex_t *restrict mutex,
- const pthread_mutexattr_t *restrict attr);
- //参数:
- //mutex:要初始化的条件变量
- //attr:NULL
静态分配
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- //**静态**或**全局**的锁可以用这个方式初始化,之后也不需要手动销毁
- //局部锁不可以使用!
销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁互斥量需要注意:
加锁
- int pthread_mutex_lock(pthread_mutex_t *mutex);
- //加锁失败会将当前执行流阻塞
调用 pthread_mutex_lock
时,可能会遇到以下情况:
pthread_mutex_lock
调用会陷入阻塞(执行流被挂起),等待互斥量解锁。- int pthread_mutex_trylock(pthread_mutex_t *mutex);
- //非阻塞版本
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
当上面的抢票小实验引入互斥锁之后,发现票数正常了,并且速度降低了很多,这是因为并发访问的问题不存在了。
- //模拟抢票
-
- #include <iostream>
- #include <string>
- #include <pthread.h>
- #include <unistd.h>
- using namespace std;
-
- int tickets = 10000; //临界资源,需要枷锁保证安全
- pthread_mutex_t mutex; //定义一个互斥锁
-
- void* ThreadRoutine(void* args)
- {
- string name = static_cast<const char*>(args);
-
- while(true)
- {
- pthread_mutex_lock(&mutex); //进入临界区前先加锁,所有线程都要遵守这个规则
- if(tickets > 0) //临界区
- {
- usleep(2000); //模拟抢票所花费时间
- cout << name << " get ticket: " << tickets-- << endl; //临界区
-
- pthread_mutex_unlock(&mutex); //退出临界区解锁
- }
- else
- {
- pthread_mutex_unlock(&mutex); //退出临界区解锁
- break;
- }
-
- usleep(1000); //充当抢票后做的后续动作
- }
-
-
- return nullptr;
- }
-
- int main()
- {
- pthread_mutex_init(&mutex, nullptr); //初始化互斥锁
-
- //创建4个进程
- pthread_t tids[4];
- int size = sizeof(tids) / sizeof(tids[0]);
- for(int i = 0; i < size; i++)
- {
- char *name = new char[64];
- snprintf(name, 64, "thread-%d", i + 1);
- pthread_create(tids+i, nullptr, ThreadRoutine, name);
-
- }
-
- for(int i = 0; i < size; i++)
- {
- pthread_join(tids[i], nullptr);
- }
-
- pthread_mutex_destroy(&mutex); //销毁互斥锁
- return 0;
- }

访问同一个临界资源的线程,都必须使用同一把锁进行加锁保护,所有线程必须遵守,没有例外。
每一个线程访问临界区之前都必须加锁,所以加锁的本质是给临界区加锁。加锁粒度尽量细一些,临界区越小越好,不然代码效率会降低。
所有线程访问临界区时,需要先加锁,也就是说所有线程都必须要先看到同一把锁。所以锁本身就是公共资源,如何保证锁自己的安全?加锁和解锁本身就是原子的!所以是安全的。
临界区可以是一条代码,也可以是一批代码。
线程在加锁后运行临界区代码时会被切换吗?
线程加锁后运行临界区代码时也有可能被切换,不要特殊化加锁解锁和临界区代码。
切换后会有影响吗,会有访问安全的问题吗?
不会,因为在申请了锁的线程不在的期间,任何其他线程都无法进入临界区,因为无法成功申请到锁
这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被当前线程申请(持有锁)、锁被当前线程释放(不持有锁),原子性就体现在这里。
为了实现互斥锁 操作,大多数体系结构都提供了swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
以下是pthread_mutex_lock
的汇编伪代码
- lock:
- movb $0, %a1 # 把0值放进寄存器a1里
- xchgb %a1, mutex # 交换a1寄存器的内容和锁的值(无线程使用锁时,metux的值为1)—— 加锁,只有一条汇编语句,原子性
- if (%a1 > 0)
- return 0; # 得到锁,加锁成功
- else
- 挂起等待;
- goto lock;
pthread_mutex_unlock的汇编伪代码
- unlock:
- movb $1 mutex #把1赋给锁
- 唤醒等待的线程;
- return 0;
使用自己封装的thread类运行售票程序
- //Thread.h
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <pthread.h>
-
- class Thread
- {
- typedef void (*FUNC_t)(void *);
- enum Thread_STATUS
- {
- NEW,
- RUNNING,
- EXITED
- };
-
- public:
- Thread(int num, FUNC_t func, void* args)
- :_args(args)
- ,_status(NEW)
- ,_func(func)
- {
- char name[64];
- snprintf(name, 64, "Thread-%d", num);
- _name = name;
- }
- ~Thread()
- {}
-
- pthread_t threadid()
- {
- if(_status == RUNNING)
- return _tid;
- else
- return 0;
- }
- std::string threadname()
- {
- return _name;
- }
- Thread_STATUS status()
- {
- return _status;
- }
-
- static void* FuncHelper(void* args)
- {
- Thread* td = static_cast<Thread*>(args);
-
- //td->_func(td->_args);
- (*td) ();
- }
- void operator() ()
- {
- _func(_args);
- }
-
- void run()
- {
- int n = pthread_create(&_tid, nullptr, FuncHelper, this);
- if(n != 0)
- {
- exit(1);
- }
- _status = RUNNING;
- }
-
- void join()
- {
- int n = pthread_join(_tid, nullptr);
- if(n != 0)
- {
- std::cerr << "thread join error" << std::endl;
- return;
- }
- _status = EXITED;
- }
-
- private:
- pthread_t _tid;
- std::string _name;
- void* _args;
- Thread_STATUS _status;
- FUNC_t _func;
- };

-
- #include "Thread.h"
- #include "LockGuard.hpp"
- #include <iostream>
- #include <string>
- #include <unistd.h>
- using namespace std;
-
- int tickets = 1000;
-
- void ThreadRoutine(void* args)
- {
- string name = static_cast<const char *>(args);
-
- while(true)
-
- if(tickets > 0)
- {
- usleep(2000);
- cout << name << " get a ticket : " << tickets-- << endl;
- }
- else
- {
- break;
- }
-
- }
- }
-
- int main()
- {
- Thread t1(1, ThreadRoutine, (void*)"thread-1");
- Thread t2(2, ThreadRoutine, (void*)"thread-2");
- Thread t3(3, ThreadRoutine, (void*)"thread-3");
- Thread t4(4, ThreadRoutine, (void*)"thread-4");
-
- t1.run();
- t2.run();
- t3.run();
- t4.run();
-
- t1.join();
- t2.join();
- t3.join();
- t4.join();
-
- return 0;
- }

加上自己封装的互斥锁
- //LockGuard.h
- #pragma once
-
- #include <iostream>
- #include <pthread.h>
-
- class Mutex
- {
- public:
- Mutex(pthread_mutex_t* mutex)
- :_mutex(mutex)
- {}
- ~Mutex()
- {}
-
- void lock()
- {
- pthread_mutex_lock(_mutex);
- }
- void unlock()
- {
- pthread_mutex_unlock(_mutex);
- }
-
- private:
- pthread_mutex_t* _mutex;
- };
-
- class lockGuard
- {
- public:
- lockGuard(pthread_mutex_t mutex)
- :mx(&mutex)
- {
- mx.lock();
- }
- ~lockGuard()
- {
- mx.unlock(); //离开区域调用析构自动解锁
- }
- private:
- Mutex mx;
- };

- //BuyTickets.cpp
- #include "Thread.h"
- #include "LockGuard.hpp"
- #include <iostream>
- #include <string>
- #include <unistd.h>
- using namespace std;
-
- int tickets = 1000;
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义并初始化互斥锁
-
- void ThreadRoutine(void* args)
- {
- string name = static_cast<const char *>(args);
-
- while(true)
- {
- lockGuard lg(&mutex); //加锁,离开循环区域调用析构自动解锁
- if(tickets > 0)
- {
- usleep(2000);
- cout << name << " get a ticket : " << tickets-- << endl;
- }
- else
- {
- break;
- }
-
- usleep(1000);
- }
- }
-
- int main()
- {
- Thread t1(1, ThreadRoutine, (void*)"thread-1");
- Thread t2(2, ThreadRoutine, (void*)"thread-2");
- Thread t3(3, ThreadRoutine, (void*)"thread-3");
- Thread t4(4, ThreadRoutine, (void*)"thread-4");
- cout << "name: " << t1.threadname() << ", tid: " << t1.threadid() << ", status: " << t1.status() << endl;
- cout << "name: " << t2.threadname() << ", tid: " << t2.threadid() << ", status: " << t2.status() << endl;
- cout << "name: " << t3.threadname() << ", tid: " << t3.threadid() << ", status: " << t3.status() << endl;
- cout << "name: " << t4.threadname() << ", tid: " << t4.threadid() << ", status: " << t4.status() << endl;
-
- t1.run();
- t2.run();
- t3.run();
- t4.run();
-
- t1.join();
- t2.join();
- t3.join();
- t4.join();
-
- return 0;
- }
-

线程安全:多个线程并发执行同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
常见的线程安全的情况
常见不可重入的情况
常见可重入的情况
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用且不会释放的资源而处于的一种永久等待状态。
核心思想:破坏死锁的4个必要条件任意一个。
测试锁能否被其他线程解锁
- #include <iostream>
- #include <string>
- #include <cstdio>
- #include <pthread.h>
- #include <unistd.h>
- using namespace std;
-
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
- void *threadRoutine(void *args)
- {
- cout << "I am a new thread " << endl;
-
- pthread_mutex_lock(&mutex);
- cout << "I got a mutex!" << endl;
-
- pthread_mutex_lock(&mutex); // 申请锁的问题,它会停下来
- cout << "I alive again" << endl;
-
- return nullptr;
- }
-
- int main()
- {
- pthread_t tid;
- pthread_create(&tid, nullptr, threadRoutine, nullptr);
-
- sleep(3);
- cout << "main thread run begin" << endl;
- pthread_mutex_unlock(&mutex);
- cout << "main thread unlock..." << endl;
-
- sleep(3);
- return 0;
- }

结论:其他的进程也可以解锁
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
初始化条件变量
- #include <pthread.h>
- int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
- attr);
- //参数:
- //cond:要初始化的条件变量
- //attr:NULL
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁
- #include <pthread.h>
-
- int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满足
- #include <pthread.h>
-
- int pthread_cond_wait(pthread_cond_t *restrict cond,
- pthread_mutex_t *restrict mutex);
- //参数:
- //cond:要在这个条件变量上等待
- //mutex:互斥量,要被释放的互斥量
为什么等待条件满足需要传一个互斥锁呢?
因为条件变量是在加锁之后使用的,pthread_cond_wait
在调用时,会自动释放锁。
线程在等待结束后,继续从pthread_cond_wait
继续向后运行,并重新申请锁。
唤醒等待
- int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒等待队列中的全部进程
- int pthread_cond_signal(pthread_cond_t *cond); //唤醒等待队列中的一个进程
简单案例
- #include <iostream>
- #include <pthread.h>
- #include <unistd.h>
-
- using namespace std;
-
- const int num = 4;
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //条件变量
-
- void* active(void* args)
- {
- const char* name = static_cast<const char *> (args);
-
- while(true)
- {
- pthread_mutex_lock(&mutex);
- pthread_cond_wait(&cond, &mutex); //等待条件满足
- cout << name << " 活动中" << endl;
- pthread_mutex_unlock(&mutex);
- }
-
- return nullptr;
- }
-
- int main()
- {
- pthread_t tids[num];
- for(int i = 0; i < num; i++)
- {
- char* name = new char[64];
- snprintf(name, 64, "thread-%d", i + 1);
- pthread_create(tids + 1, nullptr, active, name);
- }
-
- sleep(3);
-
- while(true)
- {
- cout << "主线程唤醒线程...." << endl;
- pthread_cond_signal(&cond); //唤醒线程
- sleep(1);
- }
-
-
- for(int i = 0; i < num; i++)
- {
- pthread_join(tids[i], nullptr);
- }
-
- return 0;
- }

可以发现线程一个一个按顺序被唤醒运行
如果将
- while(true)
- {
- cout << "主线程唤醒线程...." << endl;
- pthread_cond_signal(&cond); //唤醒线程
- sleep(1);
- }
改成
- while(true)
- {
- cout << "主线程唤醒线程...." << endl;
- pthread_cond_signal(&cond); //唤醒线程
- sleep(1);
- }
主线程一次唤醒多个线程,并按顺序运行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。