1 伪命题
这本身是个伪命题。
多线程之间使用队列是一定需要做到同步的。也就是说一定是需要同步手段的,一定要在一个线程读写的时候,阻塞另一个线程。既然不然用锁,那就是用原子操作吧。
2 CAS
在多线程中,最怕的就是当你修改一个变量,但是修改还没有结束,线程调度,控制权转交,恰好别的线程也在修改这个数据。
当线程再次调度回来的时候,线程看到的值已经和之前的不一样了,但是他并不知道,因此仍然继续之前的修改,这样就会产生错误。
因此原子操作的含义就是,要不操作全部完成要么一点都不开始。这和数据库的事务不同,因为原子操作没有回滚。
也就是说原子操作,一旦开始某个操作,那么就必须要完全的完成。
CAS原子操作——Compare & Set,或是 Compare & Swap,
3 实现
队列,这里使用链表来实现
struct ListNode { ListNode *next; int val; ListNode(int x):val(x),ListNode(nullptr){} }; class UnlockQueue { public: void push(int x); int pop(); private: ListNode *push_start;// ListNode *pop_start; };
然后,考虑push,在push的时候,要在push_start的next上加节点,此时push_start->next是等于nullptr的。
void UnlockQueue::push(int x) { ListNode *n=new ListNode(x); ListNode *tmp=push_start; while(cas(push_start->next,nullptr,n)!=true) { tmp=push_start; } push_start=n; }
解释一下,创建要添加的节点以后,就用cas轮序push_start->next,当等于nullptr的时候,就将next设置为n。
因为是个原子操作,因此同一时刻,就只有这个线程在设置next。同时,当别的线程设置了next以后,那么next就不在指向nullptr,那么就会一直循环。
当设置完以后,就将push_start设置为n,此时其他线程,tmp指向改变,真正的指向尾部,因此其他线程就可在cas通过了。
因此本质上还是锁。但是这里使用了轮询。
原因在于,这个设置过程时间很短暂,所以没有必要使用锁。
这里,类的两个字段,可以不用加上volatile关键字修饰,因为别的线程会改变自己正在使用的数据。因为在cas的实现上,这两个数据成员所处的内存一直是由某个cpu独享的。因此不会有其他核心去改变这个数据。
看到这里估计也就明白了,这其实是一个自己实现的自旋锁,嗯,自旋锁,使用轮询的方式,不让进程阻塞。
对应的pop操作,同理,也是这样的思想。