[Multithreading] 20180517 - Race condition
A race condition occurs when two or more threads can access shared data and they try to change it at the same time
Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are "racing" to access/change the data.
In order to prevent race conditions from occurring, you would typically put a lock around the shared data to ensure only one thread can access the data at a time. This would mean something like this:
// Obtain lock for x
if (x == 5)
{
y = x * 2; // Now, nothing can change x until the lock is released.
// Therefore y = 10
}
// release lock for x
A "race condition" exists when multithreaded (or otherwise parallel) code that would access a shared resource could do so in such a way as to cause unexpected results.
Take this example:
for ( int i = 0; i < 10000000; i++ )
{
x = x + 1;
}
If you had 5 threads executing this code at once, the value of x WOULD NOT end up being 50,000,000. It would in fact vary with each run.
This is because, in order for each thread to increment the value of x, they have to do the following: (simplified, obviously)
Retrieve the value of xAdd 1 to this valueStore this value to xAny thread can be at any step in this process at any time, and they can step on each other when a shared resource is involved. The state of x can be changed by another thread during the time between x is being read and when it is written back.
Let's say a thread retrieves the value of x, but hasn't stored it yet. Another thread can also retrieve the same value of x (because no thread has changed it yet) and then they would both be storing the same value (x+1) back in x!
Example:
Thread 1: reads x, value is 7Thread 1: add 1 to x, value is now 8Thread 2: reads x, value is 7
Thread 1: stores 8 in xThread 2: adds 1 to x, value is now 8Thread 2: stores 8 in x
Race conditions can be avoided by employing some sort of locking mechanism before the code that accesses the shared resource:
for ( int i = 0; i < 10000000; i++ )
{
//lock x
x = x + 1;
//unlock x
}
Here, the answer comes out as 50,000,000 every time.
For more on locking, search for: mutex, semaphore, critical section, shared resource.
Here is the classical Bank Account Balance example which will help newbies to understand Threads in Java easily w.r.t. race conditions:
public class BankAccount {
/** * @param args*/
int accountNumber;
double accountBalance;
public synchronized boolean Deposit(double amount){
double newAccountBalance=0;
if(amount<=0){
return false;
}
else {
newAccountBalance = accountBalance+amount;
accountBalance=newAccountBalance;
return true;
}
}
public synchronized boolean Withdraw(double amount){
double newAccountBalance=0;
if(amount>accountBalance){
return false;
}
else{
newAccountBalance = accountBalance-amount;
accountBalance=newAccountBalance;
return true;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
BankAccount b = new BankAccount();
b.accountBalance=2000;
System.out.println(b.Withdraw(3000));
SELECT ... FOR UPDATE 就是用來避免這種情況的發生!
Oracle 、 PostgreSQL 、 MySQL 都有 SELECT ... FOR UPDATE 語法,但要特別注意的是 MySQL 只有 Storage Engine 是 InnoDB 的情況下才可以使用。下面就用 MySQL 語法示範使用 SELECT ... FOR UPDATE ︰
START TRANSACTION;
SELECT quantity FROM product WHERE product_id = 5 FOR UPDATE;
UPDATE product SET quantity = quantity - 1 WHERE product_id = 5;
COMMIT;
按照 MySQL 的官方文件,使用 SELECT ... FOR UPDATE 會在被讀取的 row 加上 exclusive lock ,而 exclusive lock 可以避免其他 thread 讀寫這些 row ,直到 COMMIT 為止。也就是當我執行 SELECT ... FOR UPDATE ,其他的 thread 必須等到我 COMMIT ,才能碰到這些被 SELECT ... FOR UPDATE 讀取的 row 。
而按造 PostgreSQL 的官方文件,其他的 thread 如果要 UPDATE 、 DELETE 、 SELECT ... FOR UPDATE 被我用 SELECT ... FOR UPDATE 鎖起來的 row ,必須等待我 COMMIT 。按照字面上的意思,在 PostgreSQL 裡其他 thread 仍能 SELECT 這些被鎖起來的 row ,這和 MySQL 有點不同,要特別注意。
Prevent Race Conditions by Locking a Row in Oracle
Several times we may need to make a lock/release at a database row level. Let me expose a simple situation so you can then relate it to your particular problem in our web service or software in general.
Suppose we have a web service and one of our features lets our subscribers buy Pay Per View (PPV) events. Then suppose our subscriber John has enough credit to buy only one PPV event. If our web service receives two petitions to buy 2 PPV events from subscriber John and the petitions are so close that the second one comes before the first one ends, we’ll have a problem, because both purchases will be accepted by our web service.
It becomes very clear we need to hold the second petition until the first one ends. Think also that we may act on John’s account from different services that don’t communicate with each other, so a solution at an application code level may not be a solution.
Fortunately, ORACLE gives us a solution. The SELECT FOR UPDATE sentence lets us make a lock/release of a table row. Let’s see an example of usage:
SELECT subscribers WHERE id = 23 FOR UPDATE WAIT 30
This query makes a lock of the registry with id = 23 in table ‘subscribers’. This lock has a timeout of 30 seconds but it can be released before that by committing or rollbacking the transaction.
Let’s see now in pseudo code an example of usage in our situation.
// lock the particular subscriber
LOCK = SELECT subscriber WHERE id = 23 WAIT 30 FOR UPDATE
if (LOCK) then
…
#all necessary code to add PPV event to the subscriber
…
#releasing the subscriber
commit;
end if
When the first petition of purchase arrives from our subscriber, the row on the table is locked. The second petition will also try to lock the row, but as it is already locked, the query will remain in a status of hold until the row is released. If the 30-second timeout is over, and the first petition has not released the row, ORACLE returns an error to all the sessions waiting for the release.
In this manner we can synchronize our services and forget all the race conditions even if our services are consumed by different systems acting on the same set of subscribers.
SELECT O.OBJECT_NAME, S.SID, S.SERIAL#, P.SPID, S.PROGRAM,S.USERNAME,
S.MACHINE,S.PORT , S.LOGON_TIME,SQ.SQL_FULLTEXT
FROM V$LOCKED_OBJECT L, DBA_OBJECTS O, V$SESSION S,
V$PROCESS P, V$SQL SQ
WHERE L.OBJECT_ID = O.OBJECT_ID
AND L.SESSION_ID = S.SID AND S.PADDR = P.ADDR
AND S.SQL_ADDRESS = SQ.ADDRESS;
alter system kill session `'sid,serial#';--sid` and `serial#` get from step 1
You can also look up the sql,username,machine,port information and get to the actual process which holds the connection
沒有留言:
張貼留言